SDWebImage 4.0源碼學習

SDWebImage源碼學習基于版本4.0

源碼注釋: SDWebImage4.0

以前看見別人的輪子感覺太高深,給了自己懶惰的借口~~無奈一次次的碰壁讓我覺得,I'm just a little bit caught in the middle.

閱讀之前對于SDWebImage的了解僅僅處于UIImageView+WebCache這個類別加載圖片方法(sd_setImageWithURL:...)的使用和SDWebImageDownloader對于圖片進行下載(downloadImageWithURL...)操作,然后就是為了面試膚淺的對于其緩存機制的實現(xiàn)原理的了解.

學習源碼的心得體驗:

  1. 圖片壓縮,解碼
  2. 網(wǎng)絡請求,多線程
  3. 緩存機制
  4. 框架構建思想

SDWebImage簡介

SDWebImage是一個異步的圖片下載框架,它支持緩存,并使用了類別(UIImageView, UIButton, MKAnnotationView)可以很方便的進行使用.

  1. 類別(UIImageView, UIButton, MKAnnotationView)用來加載網(wǎng)絡圖片并且對網(wǎng)絡圖片的緩存進行管理
  2. 采用異步方式來下載網(wǎng)絡圖片
  3. 采用異步方式,使用memory+disk來緩存網(wǎng)絡圖片元旬,自動管理緩存匀归。
  4. 后臺進行圖片解壓縮
  5. 保證相同URL的網(wǎng)絡圖片不會被重復下載
  6. 保證失效的URL不會被無限重試
  7. 不會阻塞主線程
  8. 性能
  9. 使用GCD和ARC
  10. 支持多種圖片格式(JPEG,PNG,GIF,WebP,TIFF...)

頭文件

SDWebImageCompat
一般項目我們都會定義一個頭文件,包含一些宏定義,常量,或者常用的方法,它存在的意義無需多說.
SDWebImageCompat中定義了一些相關平臺適配的宏,簡單的一些常量和內聯(lián)函數(shù),block等.
關于宏定義的知識:

  1. #if 表達式 程序段1 #else 程序段2 #endif 含義:如果表達式成立,執(zhí)行程序段1,否則執(zhí)行程序段2
  2. #if 表達式1 #elif 表達式2 #endif 類似于if(){} else if() {}
  3. 防止重復聲明,頭文件重復包含和編譯 #ifndef 標識符 #define 標識符 程序段1 #else 程序段2 #endif 含義:如果標識符沒有定義過(if not define) 進行宏定義,執(zhí)行程序段1,否則執(zhí)行程序段2.
  4. 取消宏定義:#undef
  5. 枚舉:從枚舉定義來看袱贮,NS_ENUM和NS_OPTIONS本質是一樣的攒巍,僅僅從字面上來區(qū)分其用途窑业。NS_ENUM是通用情況,NS_OPTIONS一般用來定義具有位移操作或特點的情況搀擂。

枚舉宏定義:


#ifndef NS_ENUM
#define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type
#endif

//使用
typedef NS_ENUM(int,EnumName){
    EnumNameType1,
    EnumNameType2
};
//展開
typedef enum EnumName:int EnumName;
enum EnumName:int {
    EnumNameType1,
    EnumNameType2
};

內聯(lián)函數(shù)處理圖片尺寸
extern UIImage *SDScaledImageForKey(NSString *key, UIImage *image);

inline UIImage *SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image) {
    if (!image) {
        return nil;
    }
    
#if SD_MAC
    return image;
#elif SD_UIKIT || SD_WATCH
    //動圖
    if ((image.images).count > 0) {
        NSMutableArray<UIImage *> *scaledImages = [NSMutableArray array];

        for (UIImage *tempImage in image.images) {
            //遞歸調用
            [scaledImages addObject:SDScaledImageForKey(key, tempImage)];
        }
        //用一組圖片創(chuàng)建一個動態(tài)圖片
        return [UIImage animatedImageWithImages:scaledImages duration:image.duration];
    }
    else {
#if SD_WATCH
        if ([[WKInterfaceDevice currentDevice] respondsToSelector:@selector(screenScale)]) {
#elif SD_UIKIT
        if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
#endif
            //根據(jù)后綴給scale賦值
            CGFloat scale = 1;
            if (key.length >= 8) {
                NSRange range = [key rangeOfString:@"@2x."];
                if (range.location != NSNotFound) {
                    scale = 2.0;
                }
                
                range = [key rangeOfString:@"@3x."];
                if (range.location != NSNotFound) {
                    scale = 3.0;
                }
            }
            //使用initWithCGImage來根據(jù)Core Graphics的圖片構建UIImage。
            UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
            image = scaledImage;
        }
        return image;
    }
#endif
}

線程安全:dispatch_main_async_safe

/*判斷當前隊列是否是主隊列,如果是直接執(zhí)行,不是就通過dispatch_async(dispatch_get_main_queue(), block)執(zhí)行,在主線程中執(zhí)行dispatch_async(dispatch_get_main_queue(), block)有可能會crash.*/
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

圖片解碼

相關類或文件

  1. NSData+ImageContentType
  2. UIImage+GIF
  3. UIImage+MultiFormat
  4. SDWebImageDecoder
  5. NSImage+WebCache(MAC_OS)

圖片格式:NSData+ImageContentType

當文件使用二進制流作為傳輸時威恼,需要制定一套規(guī)范箫措,用來區(qū)分該文件到底是什么類型的斤蔓。實際上每個文件的前幾個字節(jié)都標識著文件的類型,對于一般的圖片文件驾锰,通過第一個字節(jié)(WebP需要12字節(jié))可以辨識出文件類型椭豫。

  1. JPEG (jpg)捻悯,文件頭:FFD8FFE1
  2. PNG (png)今缚,文件頭:89504E47
  3. GIF (gif)姓言,文件頭:47494638
  4. TIFF tif;tiff 0x49492A00
  5. TIFF tif;tiff 0x4D4D002A
  6. RAR Archive (rar)囱淋,文件頭:52617221
  7. WebP : 524946462A73010057454250

這個方法的實現(xiàn)思路是這樣的:
1.取data的第一個字節(jié)的數(shù)據(jù)妥衣,辨識出JPG/JPEG税手、PNG芦倒、GIF兵扬、TIFF這幾種圖片格式器钟,返回其對應的MIME類型。
2.如果第一個字節(jié)是數(shù)據(jù)為0x52狞谱,需要進一步檢測跟衅,因為以0x52為文件頭的文件也可能會是rar等類型(可以在文件頭查看)伶跷,而webp的前12字節(jié)有著固定的數(shù)據(jù):

頭信息
// 根據(jù)二進制文件頭的第一個字節(jié)來判斷文件的類型()
+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
    if (!data) {
        return SDImageFormatUndefined;
    }
    //獲取一個字節(jié)的數(shù)據(jù)
    uint8_t c;
    [data getBytes:&c length:1];
    //一個字節(jié)兩個字符
    switch (c) {
        case 0xFF:
            return SDImageFormatJPEG;
        case 0x89:
            return SDImageFormatPNG;
        case 0x47:
            return SDImageFormatGIF;
        case 0x49:
        case 0x4D:
            return SDImageFormatTIFF;
        case 0x52:
            //WEBP
            // R as RIFF for WEBP
            if (data.length < 12) {
                return SDImageFormatUndefined;
            }
            
            NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
            if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                return SDImageFormatWebP;
            }
    }
    return SDImageFormatUndefined;
}

支持顯示GIF:(只是顯示圖片的第一幀)UIImage+GIF

當圖片源有多個(gif格式)的時候,通過CGImageSourceRef獲取圖片的第一幀返回,這里主要涉及<Image I/0>框架的內容

+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
    if (!data) {
        return nil;
    }
    //獲取圖片源CGImageSourceRef
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    
    //獲取圖片源數(shù)量
    size_t count = CGImageSourceGetCount(source);

    UIImage *staticImage;

    if (count <= 1) {
        //只有一個圖片源,說明不是動畫,直接實例化返回
        staticImage = [[UIImage alloc] initWithData:data];
    } else {
        
        //這里僅僅繪制gif的第一幀內容,如果支持GIF播放可以使用FLAnimatedImageView這個類別
#if SD_WATCH
        CGFloat scale = 1;
        scale = [WKInterfaceDevice currentDevice].screenScale;
#elif SD_UIKIT
        CGFloat scale = 1;
        scale = [UIScreen mainScreen].scale;
#endif
        
        //獲取第一幀的CGImage
        CGImageRef CGImage = CGImageSourceCreateImageAtIndex(source, 0, NULL);
#if SD_UIKIT || SD_WATCH
        //繪制
        UIImage *frameImage = [UIImage imageWithCGImage:CGImage scale:scale orientation:UIImageOrientationUp];
        staticImage = [UIImage animatedImageWithImages:@[frameImage] duration:0.0f];
#elif SD_MAC
        staticImage = [[UIImage alloc] initWithCGImage:CGImage size:NSZeroSize];
#endif
        CGImageRelease(CGImage);
    }
    //釋放資源
    CFRelease(source);

    return staticImage;
}

UIImage <->NSData的相互轉換:UIImage+MultiFormat

  • (nullable UIImage *)sd_imageWithData:(nullable NSData *)data;
+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data {
    if (!data) {
        return nil;
    }
    UIImage *image;
    //格式判斷
    SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:data];
    if (imageFormat == SDImageFormatGIF) {
        //gif 返回第一幀圖像
        image = [UIImage sd_animatedGIFWithData:data];
    }
#ifdef SD_WEBP
    else if (imageFormat == SDImageFormatWebP)
    { //webp
        image = [UIImage sd_imageWithWebPData:data];
    }
#endif
    else {
        image = [[UIImage alloc] initWithData:data];
#if SD_UIKIT || SD_WATCH
        //獲取方向信息
        UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];
        //實例化  UIImageOrientationUp為默認,直接返回即可
        if (orientation != UIImageOrientationUp) {
            image = [UIImage imageWithCGImage:image.CGImage
                                        scale:image.scale
                                  orientation:orientation];
        }
#endif
    }
    return image;
}

<Image I/O>獲取圖片信息(方向)

#if SD_UIKIT || SD_WATCH
//獲取圖片的方向
+(UIImageOrientation)sd_imageOrientationFromImageData:(nonnull NSData *)imageData {
    UIImageOrientation result = UIImageOrientationUp;
    //獲取圖片源數(shù)據(jù)
    CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
    if (imageSource) {
        //首幀圖片的屬性信息
        CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
        if (properties) {
            CFTypeRef val;
            int exifOrientation;
            //獲取屬性字典中的方向信息
            val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
            if (val) {
                CFNumberGetValue(val, kCFNumberIntType, &exifOrientation);
                //轉換
                result = [self sd_exifOrientationToiOSOrientation:exifOrientation];
            } // else - if it's not set it remains at up
            CFRelease((CFTypeRef) properties);
        } else {
            //NSLog(@"NO PROPERTIES, FAIL");
        }
        CFRelease(imageSource);
    }
    return result;
}

UIImage -> NSData
-(nullable NSData *)sd_imageData;
-(nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat;
CGImageAlphaInfo是一個枚舉,表示alpha分量的位置及顏色分量是否做預處理:

  • kCGImageAlphaLast:alpha分量存儲在每個像素中最不顯著的位置,如RGBA拢肆。
  • kCGImageAlphaFirst:alpha分量存儲在每個像素中最顯著的位置郭怪,如ARGB鄙才。
  • kCGImageAlphaPremultipliedLast:alpha分量存儲在每個像素中最不顯著的位置攒庵,但顏色分量已經(jīng)乘以了alpha值。
  • kCGImageAlphaPremultipliedFirst:alpha分量存儲在每個像素中最顯著的位置,同時顏色分量已經(jīng)乘以了alpha值糖驴。
  • kCGImageAlphaNoneSkipLast:沒有alpha分量贮缕。如果像素的總大小大于顏色空間中顏色分量數(shù)目所需要的空間,則最不顯著位置的位將被忽略定嗓。
  • kCGImageAlphaNoneSkipFirst:沒有alpha分量宵溅。如果像素的總大小大于顏色空間中顏色分量數(shù)目所需要的空間恃逻,則最顯著位置的位將被忽略寇损。
  • kCGImageAlphaNone:等于kCGImageAlphaNoneSkipLast矛市。
- (nullable NSData *)sd_imageData {
    return [self sd_imageDataAsFormat:SDImageFormatUndefined];
}

//將UIImage對象轉換成二進制,有透明通道的返回PNG,否則返回JPEG
- (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat {
    NSData *imageData = nil;
    if (self) {
#if SD_UIKIT || SD_WATCH
        int alphaInfo = CGImageGetAlphaInfo(self.CGImage);
        //透明通道
        BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
                          alphaInfo == kCGImageAlphaNoneSkipFirst ||
                          alphaInfo == kCGImageAlphaNoneSkipLast);
        
        BOOL usePNG = hasAlpha;
        
        // the imageFormat param has priority here. But if the format is undefined, we relly on the alpha channel
        if (imageFormat != SDImageFormatUndefined) {
            usePNG = (imageFormat == SDImageFormatPNG);
        }
        
        if (usePNG) {
            imageData = UIImagePNGRepresentation(self);
        } else {
            imageData = UIImageJPEGRepresentation(self, (CGFloat)1.0);
        }
#else
        NSBitmapImageFileType imageFileType = NSJPEGFileType;
        if (imageFormat == SDImageFormatGIF) {
            imageFileType = NSGIFFileType;
        } else if (imageFormat == SDImageFormatPNG) {
            imageFileType = NSPNGFileType;
        }
        
        imageData = [NSBitmapImageRep representationOfImageRepsInArray:self.representations
                                                             usingType:imageFileType
                                                            properties:@{}];
#endif
    }
    return imageData;
}

SDWebImageDecoder解碼

SDWebImageDecoder文件中是一個對UIImage添加的分類,主要針對內存較小的設備進行圖片解碼和壓縮處理.

常量

//用來說明每個像素占用內存多少個字節(jié)憨愉,在這里是占用4個字節(jié)配紫。(圖像在iOS設備上是以像素為單位顯示的)
static const size_t kBytesPerPixel = 4;
//表示每一個組件占多少位。舉例植袍,比方說RGBA于个,其中R(紅色)G(綠色)B(藍色)A(透明度)是4個組件厅篓,每個像素由這4個組件組成,那么我們就用8位來表示著每一個組件档押,所以這個RGBA就是8*4 = 32位令宿。
static const size_t kBitsPerComponent = 8;

/*
 * 最大支持壓縮圖像源的大小
 * Suggested value for iPad1 and iPhone 3GS: 60.
 * Suggested value for iPad2 and iPhone 4: 120.
 * Suggested value for iPhone 3G and iPod 2 and earlier devices: 30.
 */
static const CGFloat kDestImageSizeMB = 60.0f;

/*
 * 原圖方塊的大小
 * Suggested value for iPad1 and iPhone 3GS: 20.
 * Suggested value for iPad2 and iPhone 4: 40.
 * Suggested value for iPhone 3G and iPod 2 and earlier devices: 10.
 */
static const CGFloat kSourceImageTileSizeMB = 20.0f;
//1M有多少字節(jié)
static const CGFloat kBytesPerMB = 1024.0f * 1024.0f;
//1M有多少個像素
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel;
//目標總像素
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB;
//原圖放寬總像素
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB;
//重疊像素大小
static const CGFloat kDestSeemOverlap = 2.0f;   // the numbers of pixels to overlap the seems where tiles meet.

私有方法

  • 判斷是否需要解碼
  • 判斷是否需要壓縮處理
  • 獲取顏色空間
//gif png 不解碼
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
    // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
    if (image == nil) {
        return NO;
    }
    // do not decode animated images
    //動畫不進行解碼
    if (image.images != nil) {
        return NO;
    }
    
    CGImageRef imageRef = image.CGImage;
    
    //透明通道信息
    CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
    BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
                     alpha == kCGImageAlphaLast ||
                     alpha == kCGImageAlphaPremultipliedFirst ||
                     alpha == kCGImageAlphaPremultipliedLast);
    // do not decode images with alpha
    if (anyAlpha) {
        //有透明通道不進行解碼
        return NO;
    }
    
    return YES;
}
//是否進行壓縮
+ (BOOL)shouldScaleDownImage:(nonnull UIImage *)image {
    BOOL shouldScaleDown = YES;
        
    CGImageRef sourceImageRef = image.CGImage;
    CGSize sourceResolution = CGSizeZero;
    sourceResolution.width = CGImageGetWidth(sourceImageRef);
    sourceResolution.height = CGImageGetHeight(sourceImageRef);
    
    float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
    //判斷目標總像素與最大支持壓縮的像素比(60MB)
    float imageScale = kDestTotalPixels / sourceTotalPixels;
    if (imageScale < 1) {
        shouldScaleDown = YES;
    } else {
        shouldScaleDown = NO;
    }
    return shouldScaleDown;
}
//獲取顏色空間
+ (CGColorSpaceRef)colorSpaceForImageRef:(CGImageRef)imageRef {
    // current
    CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
    CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);
    //不支持顏色空間
    BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
                                  imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
                                  imageColorSpaceModel == kCGColorSpaceModelCMYK ||
                                  imageColorSpaceModel == kCGColorSpaceModelIndexed);
    if (unsupportedColorSpace) {
        //使用RGB模式的顏色空間
        colorspaceRef = CGColorSpaceCreateDeviceRGB();
        CFAutorelease(colorspaceRef);
    }
    return colorspaceRef;
}

解碼:

+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
    if (![UIImage shouldDecodeImage:image]) {
        return image;
    }
    
    // autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
    // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
    @autoreleasepool{
        
        CGImageRef imageRef = image.CGImage;
        CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];
        
        size_t width = CGImageGetWidth(imageRef);
        size_t height = CGImageGetHeight(imageRef);
        //每行的像素占用字節(jié)數(shù)
        size_t bytesPerRow = kBytesPerPixel * width;
        
        //這里創(chuàng)建的contexts是沒有透明因素的革娄。在UI渲染的時候拦惋,實際上是把多個圖層按像素疊加計算的過程首尼,需要對每一個像素進行 RGBA 的疊加計算软能。當某個 layer 的是不透明的查排,也就是 opaque 為 YES 時,GPU 可以直接忽略掉其下方的圖層砂代,這就減少了很多工作量刻伊。這也是調用 CGBitmapContextCreate 時 bitmapInfo 參數(shù)設置為忽略掉 alpha 通道的原因。
        CGContextRef context = CGBitmapContextCreate(NULL,
                                                     width,
                                                     height,
                                                     kBitsPerComponent,
                                                     bytesPerRow,
                                                     colorspaceRef,
                                                     kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
        if (context == NULL) {
            return image;
        }
        
        // Draw the image into the context and retrieve the new bitmap image without alpha
        //繪制圖像 得到?jīng)]有透明通道的圖片
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
        CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
        UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
                                                         scale:image.scale
                                                   orientation:image.imageOrientation];
        
        CGContextRelease(context);
        CGImageRelease(imageRefWithoutAlpha);
        
        return imageWithoutAlpha;
    }
}

壓縮

+ (nullable UIImage *)decodedAndScaledDownImageWithImage:(nullable UIImage *)image {
    //檢測圖像能否解碼
    if (![UIImage shouldDecodeImage:image]) {
        return image;
    }
   //檢查圖像應不應該壓縮撩鹿,原則是:如果圖像大于目標尺寸才需要壓縮
    if (![UIImage shouldScaleDownImage:image]) {
        return [UIImage decodedImageWithImage:image];
    }
    
    CGContextRef destContext;
    
    // autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
    // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
    @autoreleasepool {
        //拿到數(shù)據(jù)信息 sourceImageRef
        CGImageRef sourceImageRef = image.CGImage;
        
        //計算原圖的像素 sourceResolution
        CGSize sourceResolution = CGSizeZero;
        sourceResolution.width = CGImageGetWidth(sourceImageRef);
        sourceResolution.height = CGImageGetHeight(sourceImageRef);
        //計算原圖總像素 sourceTotalPixels
        float sourceTotalPixels = sourceResolution.width * sourceResolution.height;
        // Determine the scale ratio to apply to the input image
        // that results in an output image of the defined size.
        // see kDestImageSizeMB, and how it relates to destTotalPixels.
        
        //計算壓縮比例 imageScale
        float imageScale = kDestTotalPixels / sourceTotalPixels;
        //計算目標像素 destResolution
        CGSize destResolution = CGSizeZero;
        destResolution.width = (int)(sourceResolution.width*imageScale);
        destResolution.height = (int)(sourceResolution.height*imageScale);
        
        // current color space
        //獲取當前的顏色空間 colorspaceRef
        CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:sourceImageRef];
        
        
        //計算并創(chuàng)建目標圖像的內存 destBitmapData
        size_t bytesPerRow = kBytesPerPixel * destResolution.width;
        
        // Allocate enough pixel data to hold the output image.
        void* destBitmapData = malloc( bytesPerRow * destResolution.height );
        if (destBitmapData == NULL) {
            return image;
        }
        
        // kCGImageAlphaNone is not supported in CGBitmapContextCreate.
        // Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
        // to create bitmap graphics contexts without alpha info.
        //創(chuàng)建目標上下文 destContext
        destContext = CGBitmapContextCreate(destBitmapData,
                                            destResolution.width,
                                            destResolution.height,
                                            kBitsPerComponent,
                                            bytesPerRow,
                                            colorspaceRef,
                                            kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
        
        if (destContext == NULL) {
            free(destBitmapData);
            return image;
        }
        //設置壓縮質量
        CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh);
        
        // Now define the size of the rectangle to be used for the
        // incremental blits from the input image to the output image.
        // we use a source tile width equal to the width of the source
        // image due to the way that iOS retrieves image data from disk.
        // iOS must decode an image from disk in full width 'bands', even
        // if current graphics context is clipped to a subrect within that
        // band. Therefore we fully utilize all of the pixel data that results
        // from a decoding opertion by achnoring our tile size to the full
        // width of the input image.
        //計算第一個原圖方塊 sourceTile,這個方塊的寬度同原圖一樣吼鳞,高度根據(jù)方塊容量計算
        CGRect sourceTile = CGRectZero;
        sourceTile.size.width = sourceResolution.width;
        // The source tile height is dynamic. Since we specified the size
        // of the source tile in MB, see how many rows of pixels high it
        // can be given the input image width.
        sourceTile.size.height = (int)(kTileTotalPixels / sourceTile.size.width );
        sourceTile.origin.x = 0.0f;
        // The output tile is the same proportions as the input tile, but
        // scaled to image scale.
        
        //計算目標圖像方塊 destTile
        CGRect destTile;
        destTile.size.width = destResolution.width;
        destTile.size.height = sourceTile.size.height * imageScale;
        destTile.origin.x = 0.0f;
        
        //計算原圖像方塊與方塊重疊的像素大小 sourceSeemOverlap
        // The source seem overlap is proportionate to the destination seem overlap.
        // this is the amount of pixels to overlap each tile as we assemble the ouput image.
        float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height);
        CGImageRef sourceTileImageRef;
        
        
//        計算原圖像需要被分割成多少個方塊 iterations
        // calculate the number of read/write operations required to assemble the
        // output image.
        int iterations = (int)( sourceResolution.height / sourceTile.size.height );
        // If tile height doesn't divide the image height evenly, add another iteration
        // to account for the remaining pixels.
        int remainder = (int)sourceResolution.height % (int)sourceTile.size.height;
        if(remainder) {
            iterations++;
        }
        
        //根據(jù)重疊像素計算原圖方塊的大小后,獲取原圖中該方塊內的數(shù)據(jù)音诫,把該數(shù)據(jù)寫入到相對應的目標方塊中
        // Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations.
        float sourceTileHeightMinusOverlap = sourceTile.size.height;
        sourceTile.size.height += sourceSeemOverlap;
        destTile.size.height += kDestSeemOverlap;
        for( int y = 0; y < iterations; ++y ) {
            @autoreleasepool {
                sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap;
                destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap);
                sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile );
                if( y == iterations - 1 && remainder ) {
                    float dify = destTile.size.height;
                    destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale;
                    dify -= destTile.size.height;
                    destTile.origin.y += dify;
                }
                CGContextDrawImage( destContext, destTile, sourceTileImageRef );
                CGImageRelease( sourceTileImageRef );
            }
        }
        
        //返回目標圖像
        CGImageRef destImageRef = CGBitmapContextCreateImage(destContext);
        CGContextRelease(destContext);
        if (destImageRef == NULL) {
            return image;
        }
        UIImage *destImage = [UIImage imageWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation];
        CGImageRelease(destImageRef);
        if (destImage == nil) {
            return image;
        }
        return destImage;
    }
}

緩存機制

緩存配置信息SDImageCacheConfig

/**
 * Decompressing images that are downloaded and cached can improve performance but can consume lot of memory.
 * Defaults to YES. Set this to NO if you are experiencing a crash due to excessive memory consumption.
 是否解壓縮圖片,默認為YES
 */
@property (assign, nonatomic) BOOL shouldDecompressImages;

/**
 *  disable iCloud backup [defaults to YES]
 是否禁用iCloud備份香罐, 默認為YES
 */
@property (assign, nonatomic) BOOL shouldDisableiCloud;

/**
 * use memory cache [defaults to YES]
  是否緩存到內存中庇茫,默認為YES
 */
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;

/**
 * The maximum length of time to keep an image in the cache, in seconds
  最大的緩存不過期時間, 單位為秒顷霹,默認為一周的時間
 */
@property (assign, nonatomic) NSInteger maxCacheAge;

/**
 * The maximum size of the cache, in bytes.
 最大的緩存尺寸淋淀,單位為字節(jié)
 */
@property (assign, nonatomic) NSUInteger maxCacheSize;

緩存SDImageCache

SDImageCache是一個緩存的抽象,主要功能包括緩存信息的配置,設置緩存路徑,進行緩存,查詢緩存,清除緩存等.它主要使用memory進行緩存,當然也可以選擇同時緩存在磁盤.緩存在磁盤的操作是異步進行的,不用擔心造成UI線程的延遲.

緩存在磁盤中的路徑默認是在沙盒路徑的Library/Caches/default/com.hackemist.SDWebImageCache.default路徑下,當然我們也可以自定義存儲路徑,圖片文件名是經(jīng)過圖片的URL進行MD5加密處理的字符串.


存儲路徑

2
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
    const char *str = key.UTF8String;
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]];

    return filename;
}

文件讀寫操作全部是在同一個串行隊列中進行的,保證了讀寫的安全性

//檢查隊列l(wèi)abel,保證IO操作在IOQueue隊列中執(zhí)行
- (void)checkIfQueueIsIOQueue {
    const char *currentQueueLabel = dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL);
    const char *ioQueueLabel = dispatch_queue_get_label(self.ioQueue);
    if (strcmp(currentQueueLabel, ioQueueLabel) != 0) {
        NSLog(@"This method should be called from the ioQueue");
    }
}

緩存操作:memory通過AutoPurgeCache進行緩存,其實就是NSCache的子類,只是在這里增加了內存緊張的監(jiān)聽,用于及時清理緩存.磁盤中的操作就是通過文件管理器NSFileManage寫入.

//最終方法
- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock {
    if (!image || !key) {
        //為空
        if (completionBlock) {
            completionBlock();
        }
        return;
    }
    //允許memory緩存
    if (self.config.shouldCacheImagesInMemory) {
        //緩存,計算大小,cache
        NSUInteger cost = SDCacheCostForImage(image);
        [self.memCache setObject:image forKey:key cost:cost];
    }
    
    if (toDisk) {
        //緩存到磁盤
        dispatch_async(self.ioQueue, ^{
            NSData *data = imageData;
            
            if (!data && image) {
                //圖片格式,轉換為二進制
                SDImageFormat imageFormatFromData = [NSData sd_imageFormatForImageData:data];
                data = [image sd_imageDataAsFormat:imageFormatFromData];
            }
            
            [self storeImageDataToDisk:data forKey:key];
            if (completionBlock) {
                //回到主線程 進行回調
                dispatch_async(dispatch_get_main_queue(), ^{
                    completionBlock();
                });
            }
        });
    } else {
        //不進行磁盤緩存 直接回調
        if (completionBlock) {
            completionBlock();
        }
    }
}

///存儲到磁盤
- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
    if (!imageData || !key) {
        return;
    }
    //檢查當前隊列是否是IOQueue
    [self checkIfQueueIsIOQueue];
    
    if (![_fileManager fileExistsAtPath:_diskCachePath]) {
        [_fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
    }
    
    // get cache Path for image key
    NSString *cachePathForKey = [self defaultCachePathForKey:key];
    // transform to NSUrl
    NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];
    
    [_fileManager createFileAtPath:cachePathForKey contents:imageData attributes:nil];
    
    // disable iCloud backup
    if (self.config.shouldDisableiCloud) {
        [fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
    }
}

磁盤緩存管理:清除過期的緩存文件和計算所有緩存文件的大小,通過NSDirectoryEnumerator迭代器遍歷獲取文件的信息進行處理和計算

//刪除過期的緩存
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock {
    dispatch_async(self.ioQueue, ^{
        NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
        
        //真正的目錄(boolean NSNumber),資源的最后修改時間(NSDate),占用大小(NSNumber)
        NSArray<NSString *> *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

        // This enumerator prefetches useful properties for our cache files.
        NSDirectoryEnumerator *fileEnumerator = [_fileManager enumeratorAtURL:diskCacheURL
                                                   includingPropertiesForKeys:resourceKeys
                                                                      options:NSDirectoryEnumerationSkipsHiddenFiles
                                                                 errorHandler:NULL];
        
        //過期時間
        NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.config.maxCacheAge];
        NSMutableDictionary<NSURL *, NSDictionary<NSString *, id> *> *cacheFiles = [NSMutableDictionary dictionary];
        NSUInteger currentCacheSize = 0;
        
        //遍歷所有緩存目錄的文件,兩個目的:
        //1:通過文件信息刪除過期的緩存
        //2.計算所有文件的大小,根據(jù)最大緩存大小進行清除
        //需要刪除的文件列表
        NSMutableArray<NSURL *> *urlsToDelete = [[NSMutableArray alloc] init];
        for (NSURL *fileURL in fileEnumerator) {
            NSError *error;
            
            //指定文件信息類型的字典
            NSDictionary<NSString *, id> *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error];

            // Skip directories and errors.
            if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) {
                continue;
            }

            // Remove files that are older than the expiration date;
            //根據(jù)文件修改時間將過期的文件添加到刪除列表中
            NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
            if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
                [urlsToDelete addObject:fileURL];
                continue;
            }

            // Store a reference to this file and account for its total size.
            NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
            //累加計算文件占用總大小
            currentCacheSize += totalAllocatedSize.unsignedIntegerValue;
            //儲存剩下文件的文件信息 NSURL:NSDictionay
            cacheFiles[fileURL] = resourceValues;
        }
        
        for (NSURL *fileURL in urlsToDelete) {
            [_fileManager removeItemAtURL:fileURL error:nil];
        }

        // If our remaining disk cache exceeds a configured maximum size, perform a second
        // size-based cleanup pass.  We delete the oldest files first.
        
        //所有文件占用大小超出
        if (self.config.maxCacheSize > 0 && currentCacheSize > self.config.maxCacheSize) {
            // Target half of our maximum cache size for this cleanup pass.
            //以最大限制大小的一半為基準
            const NSUInteger desiredCacheSize = self.config.maxCacheSize / 2;

            // Sort the remaining cache files by their last modification time (oldest first).
            //根據(jù)文件最后修改時間進行排序
            NSArray<NSURL *> *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
                                                                     usingComparator:^NSComparisonResult(id obj1, id obj2) {
                                                                         return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
                                                                     }];

            // Delete files until we fall below our desired cache size.
            for (NSURL *fileURL in sortedFiles) {
                if ([_fileManager removeItemAtURL:fileURL error:nil]) {
                    NSDictionary<NSString *, id> *resourceValues = cacheFiles[fileURL];
                    NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
                    currentCacheSize -= totalAllocatedSize.unsignedIntegerValue;

                    if (currentCacheSize < desiredCacheSize) {
                        break;
                    }
                }
            }
        }
        if (completionBlock) {
            dispatch_async(dispatch_get_main_queue(), ^{
                completionBlock();
            });
        }
    });
}

下載操作

  • SDWebImageOperation
  • SDWebImageDownloader
  • SDWebImageDownloaderOperation

SDWebImageOperation

//協(xié)議 用于發(fā)送取消操作
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end

SDWebImageDownloaderOperation

首先是定義一些通知,用于監(jiān)聽下載操作的全過程

//任務開始
extern NSString * _Nonnull const SDWebImageDownloadStartNotification;
//接收到數(shù)據(jù)
extern NSString * _Nonnull const SDWebImageDownloadReceiveResponseNotification;
//暫停
extern NSString * _Nonnull const SDWebImageDownloadStopNotification;
//完成
extern NSString * _Nonnull const SDWebImageDownloadFinishNotification;

然后是定義了一個接口(協(xié)議)SDWebImageDownloaderOperationInterface,由操作對象(NSOperation的子類)進行實現(xiàn).必須遵循這個協(xié)議.

@protocol SDWebImageDownloaderOperationInterface<NSObject>

//使用NSURLRequest,NSURLSession和SDWebImageDownloaderOptions初始化
- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options;

//可以為每一個NSOperation自由的添加相應對象
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

///設置是否需要解壓圖片
- (BOOL)shouldDecompressImages;
- (void)setShouldDecompressImages:(BOOL)value;

//設置是否需要設置憑證
- (nullable NSURLCredential *)credential;
- (void)setCredential:(nullable NSURLCredential *)value;

@end

SDWebImageDownloaderOperation是NSOperation的子類,一般重寫其Start方法,執(zhí)行我們需要進行的業(yè)務.我們也可以自定義一個NSOperation的子類,但在這里需要注意的是,該類必須遵循SDWebImageDownloaderOperationInterface協(xié)議,用于配置一些信息和添加回調.

//開啟下載任務
- (void)start {
    @synchronized (self) {
      // 如果該任務已經(jīng)被設置為取消了,那么就無需開啟下載任務了,重置。
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

#if SD_UIKIT
        
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    [sself cancel];
                    
                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        //task開啟前的準備工作
        NSURLSession *session = self.unownedSession;
        if (!self.unownedSession) {
            //新建網(wǎng)絡會話對象
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            //delegateQueue設置為nil.代理方法將會在一個串行操作隊列中執(zhí)行
            self.ownedSession = [NSURLSession sessionWithConfiguration:sessionConfig
                                                              delegate:self
                                                         delegateQueue:nil];
            session = self.ownedSession;
        }
       //開啟task 并處理回調
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }
    
    [self.dataTask resume];

    if (self.dataTask) {
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            //下載進度
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            //發(fā)送開始下載通知
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
        });
    } else {
        //任務開啟失敗
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}]];
    }

#if SD_UIKIT
    ////開啟后,確保關閉后臺任務
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
        UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
        [app endBackgroundTask:self.backgroundTaskId];
        self.backgroundTaskId = UIBackgroundTaskInvalid;
    }
#endif
}

dispatch_barrier_async,這個方法是向數(shù)組中添加數(shù)據(jù),barrier是柵欄的意思,當一個隊列中通過dispatch_barrier_async|sync添加一個任務后,這個任務就起到了一個攔截的作用,之后的任務必須等barrier_async|sync之前添加的任務完成才能夠執(zhí)行,而dispatch_barrier_async與dispatch_barrier_sync的區(qū)別也很簡單和直觀,dispatch_barrier_async不用等這個任務返回,就能夠執(zhí)行隊列后的任務,而dispatch_barrier_sync必須等自身返回,才能夠執(zhí)行隊列后的任務.往數(shù)組中添加數(shù)據(jù)截碴,對順序沒什么要求,采取dispatch_barrier_async就已經(jīng)能保證數(shù)據(jù)添加的安全性了隐岛。

//添加響應者
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    
    //回調信息字典
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];

     dispatch_barrier_async(self.barrierQueue, ^{
        [self.callbackBlocks addObject:callbacks];
    });
    return callbacks;
}

網(wǎng)絡請求:NSURLSession,主要是一些代理方法,和根據(jù)圖片數(shù)據(jù)的下載速度來進行圖片的繪制,可用于圖片的漸進式加載顯示.

- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
 completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler {
    
    //沒有收到響應碼或者響應碼小于400,但不等于304的時候,視為成功
    if (![response respondsToSelector:@selector(statusCode)] || (((NSHTTPURLResponse *)response).statusCode < 400 && ((NSHTTPURLResponse *)response).statusCode != 304)) {
        //成功
        
        NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
        self.expectedSize = expected;
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, expected, self.request.URL);
        }
        
        self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
        self.response = response;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:self];
        });
    }
    else {
        NSUInteger code = ((NSHTTPURLResponse *)response).statusCode;
        
        //stateCode=304,這個響應沒有變化齐帚,我們可以取消這個操作和返回緩存中的圖片數(shù)據(jù).
        if (code == 304) {
            [self cancelInternal];
        } else {
            [self.dataTask cancel];
        }
        dispatch_async(dispatch_get_main_queue(), ^{
            //停止下載
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
        });
        //下載失敗
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:((NSHTTPURLResponse *)response).statusCode userInfo:nil]];

        [self done];
    }
    
    if (completionHandler) {
        completionHandler(NSURLSessionResponseAllow);
    }
}


- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    
    //拼接數(shù)據(jù)
    [self.imageData appendData:data];

    if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0) {

        // 獲取已經(jīng)下載的全部數(shù)據(jù)大小
        const NSInteger totalSize = self.imageData.length;

        // 創(chuàng)建圖片源對象
        CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
        
        //沒有進行過賦值操作
        if (width + height == 0) {
            //獲取圖片信息
            CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
            if (properties) {
                NSInteger orientationValue = -1;
                //像素高
                CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
                if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
                //像素寬
                val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
                if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
                //方向
                val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
                if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
                CFRelease(properties);
                
#if SD_UIKIT || SD_WATCH
                //Core graphics繪制時,會丟失方向信息.通過initWithCGImage獲取的對象的方向是錯誤的,所以在這里保存方向信息,而initWithData不會
                //方向轉換
                orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
#endif
            }
        }
        
        //繪制圖片
        if (width + height > 0 && totalSize < self.expectedSize) {
            // Create the image
            CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);

#if SD_UIKIT || SD_WATCH
            // Workaround for iOS anamorphic image
            if (partialImageRef) {
                const size_t partialHeight = CGImageGetHeight(partialImageRef);
                CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
                CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
                CGColorSpaceRelease(colorSpace);
                if (bmContext) {
                    CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
                    CGImageRelease(partialImageRef);
                    partialImageRef = CGBitmapContextCreateImage(bmContext);
                    CGContextRelease(bmContext);
                }
                else {
                    CGImageRelease(partialImageRef);
                    partialImageRef = nil;
                }
            }
#endif

            if (partialImageRef) {
#if SD_UIKIT || SD_WATCH
                UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
#elif SD_MAC
                UIImage *image = [[UIImage alloc] initWithCGImage:partialImageRef size:NSZeroSize];
#endif
                NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
                UIImage *scaledImage = [self scaledImageForKey:key image:image];
                if (self.shouldDecompressImages) {
                    image = [UIImage decodedImageWithImage:scaledImage];
                }
                else {
                    image = scaledImage;
                }
                CGImageRelease(partialImageRef);
                
                [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO];
            }
        }

        CFRelease(imageSource);
    }

    for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
        progressBlock(self.imageData.length, self.expectedSize, self.request.URL);
    }
}

下載器SDWebImageDownloader

SDWebImageDownloader內部主要管理者一個操作隊列,用于執(zhí)行圖片下載的任務.比如設置隊列的執(zhí)行順序FIFO(先進先出),LIFO(后進先出),任務的最大并發(fā)量
還可以配置網(wǎng)絡請求參數(shù),設置請求頭,證書,網(wǎng)絡超時時間等

//下載操作 主要方法
//返回一個SDWebImageDownloadToken對象,可以用來取消下載任務
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;

    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        
        //創(chuàng)建操作對象完成之后 進行網(wǎng)絡參數(shù)配置和處理操作列表的執(zhí)行順序(添加依賴)
        
        
        __strong __typeof (wself) sself = wself;
        //超時時間
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }
        //為了防止?jié)撛诘闹貜瓦M行緩存(NSURLCache + SDImageCache),禁用了image requests
        //SDWebImageDownloaderUseNSURLCache -> NSURLRequestUseProtocolCachePolicy
        
        // cachePolicy:創(chuàng)建的request所使用的緩存策略剪菱,默認使用`NSURLRequestUseProtocolCachePolicy`旗们,該策略表示如果緩存不存在上渴,直接從服務端獲取稠氮。如果緩存存在隔披,會根據(jù)response中的Cache-Control字段判斷,下一步操作锹锰,如: Cache-Control字段為must-revalidata, 則 詢問服務端該數(shù)據(jù)是否有更新,無更新話直接返回給用戶緩存數(shù)據(jù)渺蒿,若已更新怠蹂,則請求服務端.
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        
        //HTTPShouldHandleCookies表示是否應該給request設置cookie并隨request一起發(fā)送出去,如果設置HTTPShouldHandleCookies為YES城侧,就處理存儲在NSHTTPCookieStore中的cookies
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        
        // HTTPShouldUsePipelining表示receiver(client客戶端)的下一個信息是否必須等到上一個請求回復才能發(fā)送嫌佑。如果為YES表示可以屋摇,NO表示必須等receiver收到先前的回復才能發(fā)送下個信息炮温。
        request.HTTPShouldUsePipelining = YES;
        
        //請求頭
        if (sself.headersFilter) {
            //將block做為參數(shù),對請求頭信息進行過濾或操作
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        
        //根據(jù)操作配置,請求參數(shù),網(wǎng)絡會話創(chuàng)建下載操作對象
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        //是否允許壓縮
        operation.shouldDecompressImages = sself.shouldDecompressImages;
        
        //認證
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            //新建認證對象
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        //操作對象優(yōu)先級
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }
        //添加到操作隊列
        [sself.downloadQueue addOperation:operation];
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            
            //如果是后進先出,為上一個添加的操作對象添加依賴為當前的操作對象,然后重新賦值
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}

SDWebImageManager核心類

SDWebImageManager是整個框架的核心管理者,內部管理著緩存處理和圖片下載任務.

主要接口

//全局單例對象
+ (nonnull instancetype)sharedManager;

//構造函數(shù)
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader NS_DESIGNATED_INITIALIZER;


//下載圖片,返回遵循SDWebImageOperation協(xié)議的NSOperation對象,默認為SDWebImageDownloaderOperation
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                             progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                            completed:(nullable SDInternalCompletionBlock)completedBlock;


//緩存
- (void)saveImageToCache:(nullable UIImage *)image forURL:(nullable NSURL *)url;


//取消所有當前的操作
- (void)cancelAll;


//操作是否在運行
- (BOOL)isRunning;


//異步檢查Memory中是否有緩存,block在主線程中執(zhí)行
- (void)cachedImageExistsForURL:(nullable NSURL *)url
                     completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;


//異步檢查磁盤中是否有緩存,block在主線程中執(zhí)行
- (void)diskImageExistsForURL:(nullable NSURL *)url
                   completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;


核心方法


- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {
    
    //沒有回調block是無意義的的操作 拋異常
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
    
    
    //傳NSString(不會報錯) -> NSURL
    if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
    }

    // 用NSNull代替一個非NSURL對象
    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }

    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    BOOL isFailedUrl = NO;
    if (url) {
        //檢查是否在失效URL列表中
        @synchronized (self.failedURLs) {
            isFailedUrl = [self.failedURLs containsObject:url];
        }
    }

    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        //文件不存在
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil] url:url];
        return operation;
    }

    @synchronized (self.runningOperations) {
        //添加到操作列表中
        [self.runningOperations addObject:operation];
    }
    
    NSString *key = [self cacheKeyForURL:url];

    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        if (operation.isCancelled) {
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }

        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
            if (cachedImage && options & SDWebImageRefreshCached) {
                // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
                // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
                [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }

            // download if no image or requested to refresh anyway, and download allowed by delegate
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
            
            if (cachedImage && options & SDWebImageRefreshCached) {
                // force progressive off if image already cached but forced refreshing
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // ignore image read from NSURLCache if image if cached but force refreshing
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            
            
            //下載
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                    // Do nothing if the operation was cancelled
                    // See #699 for more details
                    // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
                } else if (error) {
                    //下載失敗,回調曬白信息,添加到失效列表中
                    [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];

                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
                    if ((options & SDWebImageRetryFailed)) {
                        //禁用失效
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        
                        //下載成功 && (不是gif或者SDWebImageTransformAnimatedImage) &&實現(xiàn)代理
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                // pass nil if the image was transformed, so we can recalculate the data from the image
                                
                                //儲存緩存
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            //回調
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        if (downloadedImage && finished) {
                            //緩存到內存中(內部同時緩存在磁盤中)
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }
                        //回調
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }

                if (finished) {
                    //安全的移除操作對象
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];
            operation.cancelBlock = ^{
                [self.imageDownloader cancel:subOperationToken];
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                [self safelyRemoveOperationFromRunning:strongOperation];
            };
        } else if (cachedImage) {
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        } else {
            // Image not in cache and download disallowed by delegate
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }
    }];

    return operation;
}

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末祖很,一起剝皮案震驚了整個濱河市假颇,隨后出現(xiàn)的幾起案子笨鸡,更是在濱河造成了極大的恐慌形耗,老刑警劉巖激涤,帶你破解...
    沈念sama閱讀 216,919評論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件倦踢,死亡現(xiàn)場離奇詭異辱挥,居然都是意外死亡晤碘,警方通過查閱死者的電腦和手機哼蛆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,567評論 3 392
  • 文/潘曉璐 我一進店門肥矢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來甘改,“玉大人十艾,你說我怎么就攤上這事忘嫉∏烀幔” “怎么了访递?”我有些...
    開封第一講書人閱讀 163,316評論 0 353
  • 文/不壞的土叔 我叫張陵拷姿,是天一觀的道長响巢。 經(jīng)常有香客問我,道長灾炭,這世上最難降的妖魔是什么蜈出? 我笑而不...
    開封第一講書人閱讀 58,294評論 1 292
  • 正文 為了忘掉前任铡原,我火速辦了婚禮燕刻,結果婚禮上卵洗,老公的妹妹穿的比我還像新娘过蹂。我一直安慰自己酷勺,他們只是感情好脆诉,可當我...
    茶點故事閱讀 67,318評論 6 390
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著潜的,像睡著了一般。 火紅的嫁衣襯著肌膚如雪字管。 梳的紋絲不亂的頭發(fā)上啰挪,一...
    開封第一講書人閱讀 51,245評論 1 299
  • 那天,我揣著相機與錄音嘲叔,去河邊找鬼亡呵。 笑死,一個胖子當著我的面吹牛锰什,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 40,120評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼汁胆,長吁一口氣:“原來是場噩夢啊……” “哼梭姓!你這毒婦竟也來了?” 一聲冷哼從身側響起嫩码,我...
    開封第一講書人閱讀 38,964評論 0 275
  • 序言:老撾萬榮一對情侶失蹤誉尖,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后铸题,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體铡恕,經(jīng)...
    沈念sama閱讀 45,376評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,592評論 2 333
  • 正文 我和宋清朗相戀三年丢间,在試婚紗的時候發(fā)現(xiàn)自己被綠了探熔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,764評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡烘挫,死狀恐怖诀艰,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情墙牌,我是刑警寧澤涡驮,帶...
    沈念sama閱讀 35,460評論 5 344
  • 正文 年R本政府宣布,位于F島的核電站喜滨,受9級特大地震影響捉捅,放射性物質發(fā)生泄漏。R本人自食惡果不足惜虽风,卻給世界環(huán)境...
    茶點故事閱讀 41,070評論 3 327
  • 文/蒙蒙 一棒口、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧辜膝,春花似錦无牵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,697評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至忱辅,卻和暖如春七蜘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背墙懂。 一陣腳步聲響...
    開封第一講書人閱讀 32,846評論 1 269
  • 我被黑心中介騙來泰國打工橡卤, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人损搬。 一個月前我還...
    沈念sama閱讀 47,819評論 2 370
  • 正文 我出身青樓碧库,卻偏偏與公主長得像柜与,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子嵌灰,可洞房花燭夜當晚...
    茶點故事閱讀 44,665評論 2 354

推薦閱讀更多精彩內容