读 SDWebImage 笔记

介绍

GitHub 地址:

https://github.com/rs/SDWebImage

简介

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

特性:

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

原理

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

类的作用

  • SDImageCache
    设置缓存的类型、方式、路径等。

  • SDWebImageCompat
    兼容类,定义了很多宏和一个转换图片的方法。

  • SDWebImageDecoder
    解码器,让图片色彩转换(涉及到 color space)。

  • SDWebImageDownloader
    下载器,设置下载相关,要用到 SDWebImageDownloaderOperation。

  • SDWebImageDownloaderOperation
    下载器的操作

  • SDWebImageManager
    管理图片下载,取消操作,判断url是否已缓存等

  • SDWebImageOperation
    图片操作,后面很多类都要用到。

  • SDWebImagePrefetcher
    预抓取器,预先下载 urls 中的图片。

  • UIButton+WebCache
    按钮图片的缓存

  • UIImage+GIF
    缓存 GIF。

  • NSData+ImageContentType
    判断图片的类型,png/jpeg/gif/webp。

  • UIImage+MultiFormat
    缓存多种格式的图片,要用到 NSData+ImageContentType 的判断图片类型方法和 UIImage+GIF 的判断是否为 GIF 图片方法,以及 ImageIO 里面的方法。

  • UIImageView+HighlightedWebCache
    缓存高亮图片

  • UIImageView+WebCache
    主要用到这个,加载及缓存 UIImageView 的图片。

  • UIView+WebCacheOperation
    缓存的操作,有缓存、取消操作、移除缓存。

源码解析

先讲一些比较边缘的方法

0.SDWebImageOperation

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

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

1.NSData+ImageContentType

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

图片文件 头标识 十六进制头字节
jpeg/jpg FFD8 0xFF
png 8950 0x89
gif 4749 0x47
tiff 4D4D / 4949 0x49/0x4D
WebP 格式开头是 0x52,但是还有可能是其他类型文件,所以要识别前缀为
52 49 46 46 对应 RIFF
后缀 57 45 42 50 对应 WEBP,符合这些条件的才是 WebP 图片文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
+ (NSString *)sd_contentTypeForImageData:(NSData *)data {
uint8_t c;
[data getBytes:&c length:1];
switch (c) {
case 0xFF:
return @"image/jpeg";
case 0x89:
return @"image/png";
case 0x47:
return @"image/gif";
case 0x49:
case 0x4D:
return @"image/tiff";
case 0x52:
// R as RIFF for WEBP
if ([data length] < 12) {
return nil;
}

NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) {
return @"image/webp";
}

return nil;
}
return nil;
}

2.SDWebImageCompat

兼容类,这个类定义了很多宏,还有一个伸缩图片的方法,宏就不说了。
这个方法定义成 C 语言式的内联方法。
核心代码如下,传入 key 和图片,如果 key 中出现 @2x 就设定 scale 为 2.0,出现 @3x 就设定 scale 为 3.0,然后伸缩图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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;

3.SDWebImageDecoder

这个是解码器类,只定义了一个解码方法,传入图片,返回的也是图片。
CGImageRef 是一个指针类型。typedef struct CGImage *CGImageRef;
获取传入图片的 alpha 信息,然后判断是否符合苹果定义的 CGImageAlphaInfo,如果是就返回原图片。

1
2
3
4
5
6
7
8
9
10
11
CGImageRef imageRef = image.CGImage;

CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);

if (anyAlpha) {
return image;
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);

// current
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);

bool unsupportedColorSpace = (imageColorSpaceModel == 0 || imageColorSpaceModel == -1 || imageColorSpaceModel == kCGColorSpaceModelCMYK || imageColorSpaceModel == kCGColorSpaceModelIndexed);
if (unsupportedColorSpace)
colorspaceRef = CGColorSpaceCreateDeviceRGB();

CGContextRef context = CGBitmapContextCreate(NULL, width,
height,
CGImageGetBitsPerComponent(imageRef),
0,
colorspaceRef,
kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithAlpha = [UIImage imageWithCGImage:imageRefWithAlpha scale:image.scale orientation:image.imageOrientation];
if (unsupportedColorSpace)
CGColorSpaceRelease(colorspaceRef);
CGContextRelease(context);
CGImageRelease(imageRefWithAlpha);

return imageWithAlpha;

这个算是核心部分。

4.UIView+WebCacheOperation

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

1.设置关联方法

1
2
3
4
//传入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)
这个方法是三种操作都要用到的,负责获得数据。
这个方法是使用前面两个方法,根据缓存加载数据。
有缓存则从缓存中取出数据,没有则缓存数据,返回格式是字典格式。

1
2
3
4
5
6
7
8
9
- (NSMutableDictionary *)operationDictionary {
NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
if (operations) {
return operations;
}
operations = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}

接下来是三种操作

一. 加载图片根据是否有缓存。
从获得数据方法获得数据,传入 key,先调用第二个方法停止操作,再根据 key 缓存数据。

1
2
3
4
5
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
[self sd_cancelImageLoadOperationWithKey:key];
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary setObject:operation forKey:key];
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// Cancel in progress downloader from queue
NSMutableDictionary *operationDictionary = [self operationDictionary];
id operations = [operationDictionary objectForKey:key];
if (operations) {
if ([operations isKindOfClass:[NSArray class]]) {
for (id <SDWebImageOperation> operation in operations) {
if (operation) {
[operation cancel];
}
}
} else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
[(id<SDWebImageOperation>) operations cancel];
}
[operationDictionary removeObjectForKey:key];
}
}

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

1
2
3
4
- (void)sd_removeImageLoadOperationWithKey:(NSString *)key {
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary removeObjectForKey:key];
}

5.SDWebImageDownloader

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

定义了一些属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//下载队列的最大下载数
@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方法
初始化了一些属性和 HTTP 请求头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (id)init {
if ((self = [super init])) {
_operationClass = [SDWebImageDownloaderOperation class];
_shouldDecompressImages = YES;
_executionOrder = SDWebImageDownloaderFIFOExecutionOrder;
_downloadQueue = [NSOperationQueue new];
_downloadQueue.maxConcurrentOperationCount = 6;
_URLCallbacks = [NSMutableDictionary new];
#ifdef SD_WEBP
_HTTPHeaders = [@{@"Accept": @"image/webp,image/*;q=0.8"} mutableCopy];
#else
_HTTPHeaders = [@{@"Accept": @"image/*;q=0.8"} mutableCopy];
#endif
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
_downloadTimeout = 15.0;
}
return self;
}

核心方法
传入 URL、下载器选项(接下来会说)、进度 block、完成回调 block。

1
2
3
4
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

这个方法非常复杂,定义了 HTTP 请求,定义了 SDWebImageDownloaderOperation 实例,即下载器操作。初始化过程非常复杂,用到了 HTTP 请求,也用到了前面定义的那些属性,最后返回这个操作,这个过程建议去看源码。

6.SDWebImageDownloaderOperation

下载器的操作。
直接看前面下载器需要用到的初始化方法。
需要初始化各种属性,主要是几个 block:进度 block、完成回调 block、取消回调 block。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (id)initWithRequest:(NSURLRequest *)request
options:(SDWebImageDownloaderOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageDownloaderCompletedBlock)completedBlock
cancelled:(SDWebImageNoParamsBlock)cancelBlock {
if ((self = [super init])) {
_request = request;
_shouldDecompressImages = YES;
_shouldUseCredentialStorage = YES;
_options = options;
_progressBlock = [progressBlock copy];
_completedBlock = [completedBlock copy];
_cancelBlock = [cancelBlock copy];
_executing = NO;
_finished = NO;
_expectedSize = 0;
responseFromCached = YES;
}
return self;
}

7.SDWebImageManager

图片管理器,负责图片的下载、转换、缓存等。
这里先说明 SDWebImageOptions。
1 << X 这种是位运算符,1 左移多少位,后面要用到,说明一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
SDWebImageRetryFailed = 1 << 0,//无效url会加入黑名单,这个标志是禁用黑名单
SDWebImageLowPriority = 1 << 1, //低优先级,会后下载
SDWebImageCacheMemoryOnly = 1 << 2, //禁用磁盘缓存
SDWebImageProgressiveDownload = 1 << 3, //显示下载进度,下载完才显示
SDWebImageRefreshCached = 1 << 4, //重新从远程缓存
SDWebImageContinueInBackground = 1 << 5, //在后台继续下载图片
SDWebImageHandleCookies = 1 << 6, //把cookie存储到NSHTTPCookieStorey
SDWebImageAllowInvalidSSLCertificates = 1 << 7, //允许非信任ssl证书
SDWebImageHighPriority = 1 << 8, //高优先级,插队下载队列
SDWebImageDelayPlaceholder = 1 << 9, //显示的是替代图片(初始化图片)
SDWebImageTransformAnimatedImage = 1 << 10, //转换图片大小
SDWebImageAvoidAutoSetImage = 1 << 11 //避免自动设置图片(想手动的时候设置)
};

这里包含了各种选择。

核心方法
传入 URL、上面的 options、进度 block、完成回调 block。

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

实例化过程请去看源码。

说下其他方法。
一个传入 key 判断图片是否存在存储空间的方法,使用的是 NSFileManager 的方法。

1
2
3
4
5
6
7
8
9
- (BOOL)diskImageExistsWithKey:(NSString *)key {
BOOL exists = NO;
exists = [[NSFileManager defaultManager] fileExistsAtPath:[self defaultCachePathForKey:key]];

if (!exists) {
exists = [[NSFileManager defaultManager] fileExistsAtPath:[[self defaultCachePathForKey:key] stringByDeletingPathExtension]];
}
return exists;
}

还有从存储空间或者缓存取出图片的方法。
self.memCache 是 AutoPurgeCache(单纯继承自 NSCache)的实例。
从存储空间取图片要先判断内存中是否存在,然后才从存储空间中查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
- (UIImage *)imageFromMemoryCacheForKey:(NSString *)key {
return [self.memCache objectForKey:key];
}

- (UIImage *)imageFromDiskCacheForKey:(NSString *)key {

UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
return image;
}
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
return diskImage;
}

- (UIImage *)diskImageForKey:(NSString *)key {
NSData *data = [self diskImageDataBySearchingAllPathsForKey:key];
if (data) {
UIImage *image = [UIImage sd_imageWithData:data];
image = [self scaledImageForKey:key image:image];
if (self.shouldDecompressImages) {
image = [UIImage decodedImageWithImage:image];
}
return image;
}
else {
return nil;
}
}

还有几个清除方法:

1
2
3
4
5
- (void)clearDisk;
- (void)clearMemory;

- (void)cleanDisk;
- (void)cleanDiskWithCompletionBlock;

8.SDWebImagePrefetcher

预抓取器,用来预抓取图片。
核心方法

1
2
3
4
//预抓取图片
- (void)prefetchURLs:(NSArray *)urls progress:(SDWebImagePrefetcherProgressBlock)progressBlock completed:(SDWebImagePrefetcherCompletionBlock)completionBlock;
//取消预抓取图片
- (void)cancelPrefetching;

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)prefetchURLs:(NSArray *)urls progress:(SDWebImagePrefetcherProgressBlock)progressBlock completed:(SDWebImagePrefetcherCompletionBlock)completionBlock {
[self cancelPrefetching]; // Prevent duplicate prefetch request
self.startedTime = CFAbsoluteTimeGetCurrent();
self.prefetchURLs = urls;
self.completionBlock = completionBlock;
self.progressBlock = progressBlock;

if (urls.count == 0) {
if (completionBlock) {
completionBlock(0,0);
}
} else {
NSUInteger listCount = self.prefetchURLs.count;
for (NSUInteger i = 0; i < self.maxConcurrentDownloads && self.requestedCount < listCount; i++) {
[self startPrefetchingAtIndex:i];
}
}
}

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

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

最后是各种分类,即直接给初始化的控件设置图片,支持 UIButton、UIImage、UIImageView,大同小异,我直接说 UIImageView+WebCache。

9.UIImageView+WebCache

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

1
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

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

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

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

1
2
3
4
5
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}

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

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

}

10.UIImage+GIF

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

+ (UIImage *)sd_animatedGIFWithData:(NSData *)data {
if (!data) {
return nil;
}

CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL);

size_t count = CGImageSourceGetCount(source);

UIImage *animatedImage;

if (count <= 1) {
animatedImage = [[UIImage alloc] initWithData:data];
}
else {
NSMutableArray *images = [NSMutableArray array];

NSTimeInterval duration = 0.0f;

for (size_t i = 0; i < count; i++) {
CGImageRef image = CGImageSourceCreateImageAtIndex(source, i, NULL);

duration += [self sd_frameDurationAtIndex:i source:source];

[images addObject:[UIImage imageWithCGImage:image scale:[UIScreen mainScreen].scale orientation:UIImageOrientationUp]];

CGImageRelease(image);
}

if (!duration) {
duration = (1.0f / 10.0f) * count;
}

animatedImage = [UIImage animatedImageWithImages:images duration:duration];
}

CFRelease(source);

return animatedImage;
}

总结

看完 SDWebImage 的源码后感觉学到了很多东西,特别是缓存那一块写得特别好。ImageIO 和 objc/runtime 很值得学习一下。也感觉到设计出这样一个库需要很强大的知识面,非常严谨的思想,真的不容易。