iOS:第三方库之 SDWebImage

介绍

github地址:

https://github.com/rs/SDWebImage

简介

一个异步图片下载及缓存的库。

特性

  • 一个扩展 UIImageView 分类的库,支持加载网络图片并缓存图片。
  • 异步图片下载器。
  • 异步图片缓存和自动图片有效期管理。
  • 支持 GIF 动态图片。
  • 支持 WebPage。
  • 背景图片减压。
  • 保证同一个 URl 不会再次下载
  • 保证无效的 URL 不会重新加载。
  • 保证主线程不会死锁。
  • 性能优越。
  • 使用 GCD 和 ARC。
  • 支持 ARM64 位处理器。

其他

  • 3.0后不再支持5.1.1
  • 支持 Cocoapods

原理

只要有图片的 url,就能下载到图片,使用 SDWebImage 的好处就是缓存机制,每次取图片先判断是否在内存中,如果没有,再到缓存中查找,找到了就直接加载,在缓存中找不到才重新下载,url 也会记录,记录是否是失效的 url,是则不会再尝试。
下载到的图片会进行缓存,用于下次可以直接加载。图片下载、解码、转换都是异步进行,不会阻塞主线程。

类的作用

//  设置缓存的类型、方式、路径等
SDImageCache
//  兼容类(Compat),定义了很多宏和一个转换图片的方法
SDWebImageCompat
//  解码器(Decoder),让图片色彩转换(涉及 color space)
SDWebImageDecoder
//  下载器(Download),设置下载相关,要用到 SDWebImageDownloaderOperation
SDWebImageDownloader
//  下载器的操作(Operation)
SDWebImageDownloaderOperation
//  管理(Manager)图片下载、取消操作,判断 url 是否已缓存等
SDWebImageManager
//  图片操作(Operation),很多类都需要用到
SDWebImageOperation
//  预抓取器(Prefetcher),预先下载 url 中的图片
SDWebImagePrefetcher
//  按钮图片的缓存
UIButton+WebCache
//  缓存 GIF
UIImage+GIF
//  判断图片类型,png/jpeg/gif/webp
NSData+ImageContentType
//  缓存多种格式的图片,要用到 NSData+ImageContentType 的判断图片类型方法和 UIImage+GIF 的判断是否为 gif 图的方法,以及 ImageIO 里面的方法
UIImage+MultiFormat
//  缓存高亮图片
UIImageView+HighlightedWebCache
//  加载及缓存 UIImageView 的图片
UIImageView+WebCache
//  缓存的操作(Operation),缓存,取消操作,移除缓存
UIView+WebCacheOperation

源码解析

1.SDWebImageOperation

图片操作,只有头文件,定义了协议 SDWebImageOperation,里面也只有取消方法。
这个类后面很多类都要用到。

@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end

2.NSData+IMageContentType

这个文件是 NSData 的分类,只有一个方法,传入图片数据,根据图片的头标识来确定图片的类型。头标识都不一样,只需获取文件头字节,对比十六进制信息,判断即可。

图片文件 头标识 十六进制头字节
jpeg/jpg FFD8 0xFF
png 8950 0x89
gif 4749 0x47
tiff 4D4D / 4949 0x49/0x4D

最新(2020.2.23)现已支持多种图片格式

static const SDImageFormat SDImageFormatUndefined = -1;
static const SDImageFormat SDImageFormatJPEG      = 0;
static const SDImageFormat SDImageFormatPNG       = 1;
static const SDImageFormat SDImageFormatGIF       = 2;
static const SDImageFormat SDImageFormatTIFF      = 3;
static const SDImageFormat SDImageFormatWebP      = 4;
static const SDImageFormat SDImageFormatHEIC      = 5;
static const SDImageFormat SDImageFormatHEIF      = 6;

Webp 格式开头是 0x52,但是还有可能是其他类型文件,所以要识别

  • 前缀 52 49 46 46 对应 RIFF
  • 后缀 57 45 42 50 对应 WEBP

符合这些条件的才是 webp 图片文件。

+ (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data {
    if (!data) {
        return SDImageFormatUndefined;
    }
    
    // File signatures table: http://www.garykessler.net/library/file_sigs.html
    uint8_t c;
    [data getBytes:&c length:1];
    switch (c) {
        case 0xFF:
            return SDImageFormatJPEG;
        case 0x89:
            return SDImageFormatPNG;
        case 0x47:
            return SDImageFormatGIF;
        case 0x49:
        case 0x4D:
            return SDImageFormatTIFF;
        case 0x52: {
            if (data.length >= 12) {
                //RIFF....WEBP
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
                if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
                    return SDImageFormatWebP;
                }
            }
            break;
        }
        case 0x00: {
            if (data.length >= 12) {
                //....ftypheic ....ftypheix ....ftyphevc ....ftyphevx
                NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding];
                if ([testString isEqualToString:@"ftypheic"]
                    || [testString isEqualToString:@"ftypheix"]
                    || [testString isEqualToString:@"ftyphevc"]
                    || [testString isEqualToString:@"ftyphevx"]) {
                    return SDImageFormatHEIC;
                }
                //....ftypmif1 ....ftypmsf1
                if ([testString isEqualToString:@"ftypmif1"] || [testString isEqualToString:@"ftypmsf1"]) {
                    return SDImageFormatHEIF;
                }
            }
            break;
        }
    }
    return SDImageFormatUndefined;
}

3.SDWebImageCompat

最基础的配置文件,为了兼容苹果各个平台。

兼容类,这个类定义了很多宏还有一个伸缩图片的方法。

这个方法定义成 C 语言式的内联方法。

核心代码如下,传入 key 和图片,如果 key 中出现 @2x 就设定 scale 为2.0,出现 @3x 就设定 scale 为3.0,然后伸缩图片.

CGFloat scale = [UIScreen mainScreen].scale;
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;
    }
}
 
UIImage *scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation];
image = scaledImage;

4.SDWebImageDecoder->SDImageCoderHelper

这个是解码器类,只定义了一个解码方法,传入图片,返回的也是图片。

不解码也是可以使用的,假如说我们通过 imageNamed: 来加载 image,系统默认会在主线程立即进行图片的解码工作。这一过程就是把 image 解码成可供控件直接使用的位图。

当在主线程调用了大量的 imageNamed: 方法后,就会产生卡顿了。为了解决这个问题我们有两种比较简单的处理方法:

我们不使用 imageNamed:加载图片,使用其他的方法,比如 imageWithContentsOfFile:
从网络上下载回来的图片也不能直接在 UI 控件上显示,所以 SDWebImage 选择自己解码图片,而且 SDWebImage 将解码操作基本都放在了子线程来执行。

CGImageRef 是一个指针类型。

typedef struct CGImage * CGImageRef;

获取传入图片的 alpha 信息,然后判断是否符合苹果定义的 CGImageAlphaInfo,如果是就返回原图片。

static const size_t kBytesPerPixel = 4; // 每个像素占用四个字节
static const size_t kBitsPerComponent = 8;

static const CGFloat kDestImageSizeMB = 60.0f;   // 大图片 scaleDown 后制定的最大开销

static const CGFloat kSourceImageTileSizeMB = 20.0f; // 图片分块后每个块大小

static const CGFloat kBytesPerMB = 1024.0f * 1024.0f; // 1MB 包含多少个字节
static const CGFloat kPixelsPerMB = kBytesPerMB / kBytesPerPixel; // 1MB 大小能包含多少像素
static const CGFloat kDestTotalPixels = kDestImageSizeMB * kPixelsPerMB; // 图片 scaleDown 后的最大像素数
static const CGFloat kTileTotalPixels = kSourceImageTileSizeMB * kPixelsPerMB; // 图片 scaleDown 过程中每个块的像素数量

这个方法主要是单纯的解码图片,方便 GPU 直接渲染。

+ (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);
        size_t bytesPerRow = kBytesPerPixel * width;

        // 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.
        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
        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;
    }
}

将图片解码并进行 scaleDown 的方法:
主要处理流程:

  1. 先算出 imageScale,也就是 scaleDown 的比例;
  2. 开辟内存空间,这个内存空间一定会小于 60MB;
  3. 将原图片分成若干块,然后分别渲染到目标画布的对应块上。
+ (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 {
        CGImageRef sourceImageRef = image.CGImage;
        
        CGSize sourceResolution = CGSizeZero;
        sourceResolution.width = CGImageGetWidth(sourceImageRef);
        sourceResolution.height = CGImageGetHeight(sourceImageRef);
        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.
        float imageScale = kDestTotalPixels / sourceTotalPixels;
        CGSize destResolution = CGSizeZero;
        destResolution.width = (int)(sourceResolution.width*imageScale);
        destResolution.height = (int)(sourceResolution.height*imageScale);
        
        // current color space
        CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:sourceImageRef];
        
        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.
        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.
        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.
        CGRect destTile;
        destTile.size.width = destResolution.width;
        destTile.size.height = sourceTile.size.height * imageScale;
        destTile.origin.x = 0.0f;
        // 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;
        // 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++;
        }
        // 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;
    }
}

是否需要将图片解码:

  1. 不解码动图
  2. 不解码带有 alpha 的图片
+ (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;
}

是否需要将图片进行 scaleDown,如果原图片总的像素数的大小 > kDestTotalPixels 就需要 scaleDown

+ (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;
    float imageScale = kDestTotalPixels / sourceTotalPixels;
    if (imageScale < 1) {
        shouldScaleDown = YES;
    } else {
        shouldScaleDown = NO;
    }
    
    return shouldScaleDown;
}

获取图片的颜色空间,什么是图片的颜色空间?
获取图片的宽高和 color space(指定颜色值如何解释),判断 color space 是否支持,不支持就转换为支持的模式(RGB),再用图形上下文根据获得的信息画出来,释放掉创建的 CG 指针再返回图片。

+ (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) {
        colorspaceRef = CGColorSpaceCreateDeviceRGB();
        CFAutorelease(colorspaceRef);
    }
    return colorspaceRef;
}

5.UIView+WebCacheOperation

缓存操作的 UIView 的分类,支持三种操作,也是整个库中比较核心的操作。
但是首先我们来了解三种操作都要用到的存储数据的方法。
这两个方法用的是 OC 中 runtime 方法,原理是两个文件关联方法,和上层的存储方法差不多,传入 value 和 key 对应,取出也是根据 key 取出 value ,object 传入 self 即可。

1.设置关联方法
//  传入 object、key、value,policy
//  policy 即存储方式,和声明使用几种属性大致相同,有 copy,retain,copy,retain_nonatomic,assign 五种)
 
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
2.取出方法
// 传入 object 和 key 返回 value
id objc_getAssociatedObject(id object, const void *key)

这个方法是三种操作都要用到的,获得数据。
这个方法是使用前面两个方法,根据缓存加载数据。
有缓存则从缓存中取出数据,没有则缓存数据,返回格式是字典格式。

- (SDOperationsDictionary *)sd_operationDictionary {
    @synchronized(self) {
        SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
        if (operations) {
            return operations;
        }
        operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
        objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
        return operations;
    }
}

接下来是三种操作

1. 加载图片根据是否有缓存

从获得数据方法获得数据,传入 key,先调用第二个方法停止操作,再根据 key 缓存数据。

- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key {
    if (key) {
        [self sd_cancelImageLoadOperationWithKey:key];
        if (operation) {
            SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
            @synchronized (self) {
                [operationDictionary setObject:operation forKey:key];
            }
        }
    }
}
2. 取消加载图片如果有缓存

先获得方法一的返回字典数据,传入 key 在返回的字典中查找是否已经存在,如果存在则取消所有操作
conformsToProtocol 方法如果符合这个协议(协议中声明了取消方法),调用协议中的取消方法。

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        // Cancel in progress downloader from queue
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id<SDWebImageOperation> operation;
        
        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                [operation cancel];
            }
            @synchronized (self) {
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}
3.移除缓存

获得方法一的数据,传入 key,如果 key 对应的数据在缓存中则移除。

- (void)sd_removeImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        @synchronized (self) {
            [operationDictionary removeObjectForKey:key];
        }
    }
}

6.SDWebImageDownloader

下载器类,需要用到 SDWebImageDownloaderOperation 类,下载器操作,后面会说到。

定义了一些属性 SDWebImageDownloaderConfig
// 下载队列的最大下载数
@property (assign, nonatomic) NSInteger maxConcurrentDownloads;
// 当前下载数
@property (readonly, nonatomic) NSUInteger currentDownloadCount;
// 下载超时的时间
@property (assign, nonatomic) NSTimeInterval downloadTimeout;
// 是否解压图片,默认是
@property (assign, nonatomic) BOOL shouldDecompressImages;
// 下载器顺序,枚举类型,有两种,先进先出,还是后进先出
@property (assign, nonatomic) SDWebImageDownloaderExecutionOrder executionOrder;
 
//***************** 还有一些用户属性
// url 证书
@property (strong, nonatomic) NSURLCredential *urlCredential;
// 用户名
@property (strong, nonatomic) NSString *username;
// 密码
@property (strong, nonatomic) NSString *password;
// 头像过滤器,block 指针类型,接受 url 和字典 headers
@property (nonatomic, copy) SDWebImageDownloaderHeadersFilterBlock headersFilter;
init 方法
- (nonnull instancetype)init {
    return [self initWithConfig:SDWebImageDownloaderConfig.defaultDownloaderConfig];
}

- (instancetype)initWithConfig:(SDWebImageDownloaderConfig *)config {
    self = [super init];
    if (self) {
        if (!config) {
            config = SDWebImageDownloaderConfig.defaultDownloaderConfig;
        }
        _config = [config copy];
        [_config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxConcurrentDownloads)) options:0 context:SDWebImageDownloaderContext];
        _downloadQueue = [NSOperationQueue new];
        _downloadQueue.maxConcurrentOperationCount = _config.maxConcurrentDownloads;
        _downloadQueue.name = @"com.hackemist.SDWebImageDownloader";
        _URLOperations = [NSMutableDictionary new];
        NSMutableDictionary<NSString *, NSString *> *headerDictionary = [NSMutableDictionary dictionary];
        NSString *userAgent = nil;
#if SD_UIKIT
        // User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
        userAgent = [NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], [[UIScreen mainScreen] scale]];
#elif SD_WATCH
        // User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43
        userAgent = [NSString stringWithFormat:@"%@/%@ (%@; watchOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[WKInterfaceDevice currentDevice] model], [[WKInterfaceDevice currentDevice] systemVersion], [[WKInterfaceDevice currentDevice] screenScale]];
#elif SD_MAC
        userAgent = [NSString stringWithFormat:@"%@/%@ (Mac OS X %@)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[NSProcessInfo processInfo] operatingSystemVersionString]];
#endif
        if (userAgent) {
            if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) {
                NSMutableString *mutableUserAgent = [userAgent mutableCopy];
                if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) {
                    userAgent = mutableUserAgent;
                }
            }
            headerDictionary[@"User-Agent"] = userAgent;
        }
        headerDictionary[@"Accept"] = @"image/*,*/*;q=0.8";
        _HTTPHeaders = headerDictionary;
        _HTTPHeadersLock = dispatch_semaphore_create(1);
        _operationsLock = dispatch_semaphore_create(1);
        NSURLSessionConfiguration *sessionConfiguration = _config.sessionConfiguration;
        if (!sessionConfiguration) {
            sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration];
        }
        /**
         *  Create the session for this task
         *  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
         *  method calls and completion handler calls.
         */
        _session = [NSURLSession sessionWithConfiguration:sessionConfiguration
                                                 delegate:self
                                            delegateQueue:nil];
    }
    return self;
}
核心方法

传入 url,下载器选项(接下来会说),进度 block,完成回调 block

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                   context:(nullable SDWebImageContext *)context
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {

7. SDWebImageDownloaderOperation

下载器的操作
直接看前面下载器需要用到的初始化方法。
需要初始化了各种属性,进度,完成,取消等的回调 Block 数组。

_callbackBlocks = [NSMutableArray new];

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options
                                context:(nullable SDWebImageContext *)context {
    if ((self = [super init])) {
        _request = [request copy];
        _options = options;
        _context = [context copy];
        _callbackBlocks = [NSMutableArray new];
        _responseModifier = context[SDWebImageContextDownloadResponseModifier];
        _decryptor = context[SDWebImageContextDownloadDecryptor];
        _executing = NO;
        _finished = NO;
        _expectedSize = 0;
        _unownedSession = session;
        _coderQueue = dispatch_queue_create("com.demo.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
#if SD_UIKIT
        _backgroundTaskId = UIBackgroundTaskInvalid;
#endif
    }
    return self;
}

8. SDWebImageManager

图片管理器,负责图片的下载,转换,缓存等。
这里先说明 SDWebImageOptions

1 << X 这种是位运算符,1左移多少位,后面要用到,说明一下。

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
/**
 * 默认情况下,当一个URL下载失败时,该URL会被列入黑名单,这样库就不会继续尝试。
 * 此标志禁用此黑名单。
 */
SDWebImageRetryFailed=1<<0/**
 * 默认情况下,图像下载在 UI 交互期间启动,此标志禁用此功能,
 * 例如,导致 UIScrollView 下载延迟减速。
 */
SDWebImageLowPriority=1<<1/**
 * 此标志启用渐进式下载,图像在下载过程中会像浏览器那样渐进式显示。
 * 默认情况下,图像仅在完全下载后显示。
 */
SDWebImageProgressiveLoad=1<<2/**
 * 即使映像已缓存,也要尊重 HTTP 响应缓存控件,并在需要时从远程位置刷新映像。
 * 磁盘缓存将由 NSURLCache 而不是 SDWebImage 处理,这会导致性能略有下降。
 * 此选项有助于处理同一请求 URL 后面的图像更改,例如 Facebook graph api profile pics。
 * 如果刷新了缓存的图像,则使用缓存的图像调用一次完成块,然后使用最终图像再次调用完成块。
 * 仅当不能使用嵌入的缓存破坏参数使 UR L保持静态时,才使用此标志。
 */
SDWebImageRefreshCached=1<<3/**
 *在 iOS4+ 中,如果应用程序转到后台,请继续下载图像。这是通过请求系统额外的后台时间让请求完成。如果后台任务过期,则操作将被取消。
 */
SDWebImageContinueInBackground=1<<4/**
 * 通过设置处理存储在 NSHTTPCookieStore 中的 cookie
 * NSMutableURLRequest.HTTPShouldHandleCookies = YES;
 */
SDWebImageHandleCookies=1<<5/**
 * 启用以允许不受信任的SSL证书。
 * 用于测试目的。在生产中小心使用。
 */
SDWebImageAllowInvalidSLCertificates=1<<6/**
 * 默认情况下,按图像排队的顺序加载图像。这面旗子把他们移到排在队伍前面。
 */
SDWebImageHighPriority=1<<7/**
 * 默认情况下,在加载图像时加载占位符图像。这个标志会延迟装货
 * 直到图像加载完毕。
 */
SDWebImageDelayPlaceholder=1<<8/**
 * 我们通常不会对动画图像应用变换,因为大多数变换器无法管理动画图像。
 * 无论如何,使用此标志来转换它们。
 */
SDWebImageTransformAnimatedImage=1<<9/**
 * 默认情况下,图像在下载后添加到 imageView。但在某些情况下,我们想在设置图像之前使用(例如应用过滤器或添加交叉淡入动画)
 * 如果要在成功时手动设置完成中的图像,请使用此标志
 */
SDWebImageAvoidAutosetImage1<<10/**
 * 默认情况下,图像根据其原始大小进行解码。
 * 此标志将缩小图像的大小,使其与设备的受限内存兼容。
 * 要控制限制内存字节,请选中“SDImageCoderHelper.defaultScaleDownLimitBytes”(在iOS上默认为60MB)
 * 这实际上将转换为使用5.5.0版的上下文选项“imageThumbnailPixelSize”(在iOS上默认为(39663966))。以前没有。
 * 此标志也会影响v5.5.0中的渐进图像和动画图像。以前没有。
 * @note如果需要细节控件,最好使用上下文选项“imageThumbnailPixelSize”和“imagePreserveSpectratio”。
 */
SDWebImageScaleDownLargeImages=1<<11/**
 * 默认情况下,当图像已经缓存在内存中时,我们不会查询图像数据。此掩码可以强制同时查询图像数据。但是,此查询是异步的,除非指定'SDWebImageQueryMemoryDataSync'`
 */
SDWebImageQueryMemoryData=1<<12/**
 * 默认情况下,当您只指定“SDWebImageQueryMemoryData”时,我们将异步查询内存映像数据。同时结合这个掩码来同步查询内存图像数据。
 * @note不建议同步查询数据,除非您希望确保图像加载在同一个运行循环中,以避免在单元格重用期间闪烁。
 */
SDWebImageQueryMemoryDataSync=1<<13/**
 * 默认情况下,当内存缓存未命中时,我们异步查询磁盘缓存。此掩码可以强制同步查询磁盘缓存(当内存缓存未命中时)。
 * @注意这 3 个查询选项可以组合在一起。有关这些掩码组合的完整列表,请参见wiki页面。
 * @note不建议同步查询数据,除非您希望确保图像加载在同一个运行循环中,以避免在单元格重用期间闪烁。
 */
SDWebImageQueryDiskDataSync=1<<14/**
 * 默认情况下,当缓存丢失时,将从加载程序加载图像。此标志只能阻止从缓存加载。
 */
SDWebImageFromCacheOnly=1<<15/**
 * 默认情况下,我们在从加载程序加载图像之前查询缓存。此标志只能阻止从加载程序加载。
 */
SDWebImageFromLoaderOnly=1<<16/**
 * 默认情况下,在图像加载完成后使用“SDWebImageTransition”进行某些视图转换时,此转换仅适用于从网络下载图像。此掩码还可以强制对内存和磁盘缓存应用视图转换。
 */
SDWebImageForceTransition=1<<17/**
 * 默认情况下,我们将在缓存查询和从网络下载期间解码背景图像。这有助于提高性能,因为在屏幕上渲染图像时,需要首先对其进行解码。但这发生在主队列上的核心动画。
 * 然而,这个过程也可能增加内存使用。如果由于内存消耗过多而遇到问题,则此标志可以阻止对图像进行解码。
*/
SDWebImageAvoidDecodeImage=1<<18/**
 * 默认情况下,我们解码动画图像。此标志只能强制解码第一帧并生成静态图像。
 */
SDWebImageDecodeFirstFrameOnly=1<<19/**
 * 默认情况下,对于“SDAnimatedImage”,我们在渲染期间解码动画图像帧以减少内存使用。但是,当动画图像被许多imageview共享时,可以指定将所有帧预加载到内存中以减少CPU使用。
 * 这实际上会触发后台队列中的“preloadAllAnimatedImageFrames”(仅限磁盘缓存和下载)。
 */
SDWebImagePreloadAllFrames=1<<20/**
 * 默认情况下,当您使用“SDWebImageContextAnimatedImageClass”上下文选项(如使用“SDAnimatedImageView”(设计为使用“SDAnimatedImage”)时,当内存缓存命中或图像解码器不可用时,我们仍可以使用“UIImage”生成与您的自定义类完全匹配的类作为回退解决方案。
 * 使用此选项,可以确保我们始终使用您提供的类回调映像。如果生成失败,将使用代码为“SDWebImageErrorBadImageData”的错误。
 * 注意:此选项与“SDWebImageDecodeFirstFrameOnly”不兼容,后者总是生成UIImage/NSImage。
 */
SDWebImageMatchAnimatedImageClass=1<<21/**
 * 默认情况下,从网络加载图像时,图像将写入缓存(内存和磁盘,由“storeCacheType”上下文选项控制)
 * 这可能是一个异步操作,最终的“SDInternalCompletionBlock”回调不能保证写入的磁盘缓存已完成,并可能导致逻辑错误。(例如,在完成块中修改磁盘数据,但是磁盘缓存尚未就绪)
 * 如果需要处理完成块中的磁盘缓存,则应使用此选项确保在回调时已写入磁盘缓存。
 * 注意,如果在使用自定义缓存序列化程序或使用转换器时使用此选项,我们也将等待输出图像数据写入完成。
 */
SDWebImageWaitStoreCache=1<<22};

这里包含了各种选择

核心方法

传入 url,上面的 options,进度 block,完成回调 block。

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
​completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

9.SDWebImagePrefetcher

预抓取器,用来预抓取图片

核心方法
//  预抓取图片
- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;
//  取消预抓取图片
- (void)cancelPrefetching;

先来看预抓取图片
传入 url,进度 block,完成回调 block
首先取消抓取,然后重新开始

- (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray<NSURL *> *)urls
                                          progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
                                         completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock {
    if (!urls || urls.count == 0) {
        if (completionBlock) {
            completionBlock(0, 0);
        }
        return nil;
    }
    SDWebImagePrefetchToken *token = [SDWebImagePrefetchToken new];
    token.prefetcher = self;
    token.urls = urls;
    token->_skippedCount = 0;
    token->_finishedCount = 0;
    token->_totalCount = token.urls.count;
    atomic_flag_clear(&(token->_isAllFinished));
    token.loadOperations = [NSPointerArray weakObjectsPointerArray];
    token.prefetchOperations = [NSPointerArray weakObjectsPointerArray];
    token.progressBlock = progressBlock;
    token.completionBlock = completionBlock;
    [self addRunningToken:token];
    [self startPrefetchWithToken:token];
    
    return token;
}

最后调用 startPrefetchingAtIndex: 方法,再调用 self.manager 的核心方法,即开始下载图片

- (id )downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

10.UIImageView+WebCache

很多加载方法最终都会以缺省参数方式或者直接调用这个方法,传入一个 URL,一个用来初始化的 image,一个 options(枚举,下面详细说明),一个 progressBlock(返回图片接受进度等),一个completedBlock(完成回调 block)

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock;

首先根据 url 缓存图片,这里用到的是 OC 的 runtime中的关联方法(见4)

objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

然后判断 options(见7)是其他选择则直接给图片赋值 placehoder 图片,这里判断使用的是 & 与 位运算符,SDWebImageDelayPlacehoder 是 1 << 9,1左移9位与 options 相与。

if (!(options & SDWebImageDelayPlaceholder)) {
    dispatch_main_async_safe(^{
        self.image = placeholder;
    });
}

如果 url 存在,则定义图片操作,使用图片管理器的单例来调用核心方法(下载图片方法)

id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
    //  过程省略
 
}

11.UIImage+GIF

gif 的实现使用了 ImageIO 中的 CGImageSourceRef
用获得的 gif 数据得到 CGImageSourceRef,然后算出时间,在这个时间内把图片一帧一帧的放进一个数组,最后再把这个数组和时间转成图片,就成了 gif

+ (nullable UIImage *)sd_imageWithGIFData:(nullable NSData *)data {
    if (!data) {
        return nil;
    }
    return [[SDImageGIFCoder sharedCoder] decodedImageWithData:data options:0];
}

- (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options {
    if (!data) {
        return nil;
    }
    CGFloat scale = 1;
    NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor];
    if (scaleFactor != nil) {
        scale = MAX([scaleFactor doubleValue], 1) ;
    }
    
    CGSize thumbnailSize = CGSizeZero;
    NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize];
    if (thumbnailSizeValue != nil) {
#if SD_MAC
        thumbnailSize = thumbnailSizeValue.sizeValue;
#else
        thumbnailSize = thumbnailSizeValue.CGSizeValue;
#endif
    }
    
    BOOL preserveAspectRatio = YES;
    NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio];
    if (preserveAspectRatioValue != nil) {
        preserveAspectRatio = preserveAspectRatioValue.boolValue;
    }
    
    CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);
    if (!source) {
        return nil;
    }
    
    UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize];
    CFRelease(source);
    if (!image) {
        return nil;
    }
    
    image.sd_imageFormat = [NSData sd_imageFormatForImageData:data];
    return image;
}

你可能感兴趣的:(iOS)