前言

说来惭愧,我之前从事 Android 开发,用过许多著名的第三方库,可却始终停留在简单地调用 API 阶段,从未涉及到源码的阅读以了解其背后的实现原理,所以一直处于「知其然,而不知所以然」的状态。

如今转投 iOS 怀抱的我,决定将阅读优秀项目的源码作为学习的一部分,而且是带着疑问去阅读。由于在项目中使用及考虑到 SDWebImage 的普及程度,其顺理成章的成为了我第一个阅读的项目。

话不多说,让我们开始吧!


简介

Asynchronous image downloader with cache support as a UIImageView category.

言简意赅:SDWebImage 以 UIImageView category(分类)的形式,来支持图片的异步下载与缓存。

其提供了以下功能:

  1. 以 UIImageView 的分类,来支持网络图片的加载与缓存管理
  2. 一个异步的图片加载器
  3. 一个异步的内存 + 磁盘图片缓存
  4. 支持 GIF
  5. 支持 WebP
  6. 后台图片解压缩处理
  7. 确保同一个 URL 的图片不被多次下载
  8. 确保虚假的 URL 不会被反复加载
  9. 确保下载及缓存时,主线程不被阻塞
  10. 使用 GCD 与 ARC
  11. 支持 Arm64

UIImageView+WebCache

首先,SDWebImage 最常见的使用场景想必如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#import <SDWebImage/UIImageView+WebCache.h>

...

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *MyIdentifier = @"MyIdentifier";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:MyIdentifier] autorelease];
}

// 在这里,我们使用 UIImageView 分类提供的 sd_setImageWithURL: 方法来加载网络图片
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

cell.textLabel.text = @"My Text";
return cell;
}

我们在使用 UITableView 时,往往需要在 Cell 上显示来自网络的图片,这里最关键的一行代码便是:

1
2
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

于是我们「CMD + 左键」来到了 UIImageView+WebCache 查看具体的实现:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
/**
* 根据 url、placeholder 与 custom options 为 imageview 设置 image
*
* 下载是异步的,并且被缓存的
*
* @param url 网络图片的 url 地址
* @param placeholder 用于预显示的图片
* @param options 一些定制化选项
* @param progressBlock 下载时的 Block,其定义为:typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
* @param completedBlock 下载完成时的 Block,其定义为:typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
*/

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_cancelCurrentImageLoad];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

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

if (url) {
__weak __typeof(self)wself = self;
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
completedBlock(image, error, cacheType, url);
return;
}
else if (image) {
wself.image = image;
[wself setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else {
dispatch_main_async_safe(^{
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
if (completedBlock) {
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}

虽然代码只有几十行,但其中涉及到的知识点却可不少哦,不要急,让我们将迷雾一层层剥开:


UIView+WebCacheOperation

首先来看:

1
[self sd_cancelCurrentImageLoad];

「CMD + 左键」后带我们来到了 UIView+WebCacheOperation

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// 取消正在进行的下载队列
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];
}
}

框架中的所有操作实际上都是通过一个 operationDictionary(具体查看 UIView+WebCacheOperation)来管理的,而这个 Dictionary 实际上是通过动态的方式(详情可参见:Objective-C Associated Objects 的实现原理)添加到 UIView 上的一个属性,至于为什么添加到 UIView 上, 主要是因为这个 operationDictionary 需要在 UIButton 和 UIImageView 上重用,所以需要添加到它们的根类上。

当执行 sd_setImageWithURL: 函数时,首先会 cancel 掉 operationDictionary 中已经存在的 operation,并重新创建一个新的 SDWebImageCombinedOperation 对象来获取 image,该 operation 会被存入 operationDictionary 中。

这样来保证每个 UIImageView 对象中永远只存在一个 operation,当前只允许一个图片网络请求,该 operation 负责从缓存中获取 image 或者是重新下载 image。

SDWebImageCombinedOperation 的 cancel 操作同时会 cacel 掉缓存查询的 operation 以及 downloader 的 operation


dispatch_main_sync_safe & dispatch_main_async_safe 宏定义

再来看:

1
2
3
dispatch_main_async_safe(^{
self.image = placeholder;
});

上述代码中的 dispatch_main_sync_safedispatch_main_async_safe 均为宏定义, 点进去一看发现宏是这样定义的:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define dispatch_main_sync_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_sync(dispatch_get_main_queue(), block);\
}

#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}

相信你通过这两个宏的名字就能猜到它们的作用了: 因为图像的绘制只能在主线程完成,所以dispatch_main_sync_safedispatch_main_async_safe 就是为了保证 block 能在主线程中执行。


SDWebImageManager

SDWebImageManager.h 中你可以看到关于 SDWebImageManager 的描述:

The SDWebImageManager is the class behind the UIImageView+WebCache category and likes. It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache). You can use this class directly to benefit from web image downloading with caching in another context than a UIView.

这个类就是隐藏在 UIImageView+WebCache 背后,用于处理异步下载和图片缓存的类,当然你也可以直接使用 SDWebImageManager 的上述方法 downloadImageWithURL:options:progress:completed: 来直接下载图片:

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
/**
* 如果在缓存中则直接返回,否则根据所给的 URL 下载图片
*
* @param url 网络图片的 url 地址
* @param options 一些定制化选项
* @param progressBlock 下载时的 Block,其定义为:typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
* @param completedBlock 下载完成时的 Block,其定义为:typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
* @return 返回 SDWebImageOperation 的实例
*/

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
/**
* 前面省略 n 行,主要作了如下处理:
* 1. 判断 url 的合法性
* 2. 创建 SDWebImageCombinedOperation 对象
* 3. 查看 url 是否是之前下载失败过的
* 4. 如果 url 为 nil,或者在不可重试的情况下是一个下载失败过的 url,则直接返回操作对象并调用完成回调
*/

// 根据 URL 生成对应的 key,没有特殊处理为 [url absoluteString];
NSString *key = [self cacheKeyForURL:url];
// 去缓存中查找图片(参见 SDImageCache)
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType)
{
/* ... */
// 如果在缓存中没有找到图片,或者采用的 SDWebImageRefreshCached 选项,则从网络下载
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
dispatch_main_sync_safe(^{
// 如果图片找到了,但是采用的 SDWebImageRefreshCached 选项,通知获取到了图片,并再次从网络下载,使 NSURLCache 重新刷新
completedBlock(image, nil, cacheType, YES, url);
});
}
/* 下载选项设置 */
// 使用 imageDownloader 开启网络下载
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
/* ... */
if (downloadedImage && finished) {
// 下载完成后,先将图片保存到缓存中,然后主线程返回
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
}
}
/* ... */
}
else if (image) {
// 在缓存中找到图片了,直接返回
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
}
}];
return operation;
}

更详细的注解可参见:SDWebImage源码解析之SDWebImageManager的注解

要点

  1. 在 SDWebImageManager 中管理了一个 failedURLs 的 NSMutableSet,里面下载失败的 url 会被存储下来。同时,可以通过 SDWebImageRetryFailed 来强制继续重试下载

  2. 查找缓存,若缓存中没有 image 则通过 SDWebImageDownloader 来进行下载,下载完成后通过 SDImageCache 进行缓存,会同时缓存到 memCache 和 diskCache 中


可以看到 SDWebImageManager 这个类的主要作用就是为 UIImageView+WebCache 和 SDWebImageDownloader,SDImageCache 之间构建一个桥梁,使它们能够更好的协同工作,在接下来的系列文章中,就让我们一探究竟:它是如何协调异步下载和图片缓存的?