SDWebImage源码学习笔记 ☞ 结构及基本流程

SDWebImage-源码学习笔记.png

前言

这是本系列的第 2 篇,本篇的目的如题所示,就是了解 SDWebImage 这个常用框架的文件结构,然后通过案例梳理流程,作为后边章节的主线。

目录结构

为了查看 SDWebImage 的完整目录结构,首先需要导入 SDWebImage,由 上一篇 我们了解到,新版 SDWebImage 总共分了 4 个子 pod,默认只导入了 Core,为了将它们全部导入 demo 中,Podfile 文件需要这么来写:

1
2
3
4
5
6
7
8
9
10
11
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'

target 'HHSDWebImageStudy' do

pod 'SDWebImage', '4.4.2' // 为了讨论方便,这里选定了一个比较新的版本
pod 'SDWebImage/WebP'
pod 'SDWebImage/GIF'
pod 'SDWebImage/MapKit'

end

执行 pod install 之后得到的目录结构如下图所示,点此查看完整目录结构

SDWebImage-目录结构.png

示例:用 UIImageView 展示一张静态网络图片

下面是使用时的代码,加载图片时调用的是 UIImageView+WebCache 中的方法 sd_setImageWithURL:

1
2
3
4
5
6
// 1.创建 UIImageView
UIImageView *imgV = [[UIImageView alloc] initWithFrame:CGRectMake(125, 70, 160, 160)];
[self.view addSubview:imgV];

// 2.加载网络图片
[imgV sd_setImageWithURL:[NSURL URLWithString:@"https://img.zcool.cn/community/01c81558a2723ca801219c77a1e34e.jpg"]];

查看 sd_setImageWithURL: 的实现,内部调用了一个参数很全的方法 sd_setImageWithURL: placeholderImage: options: progress: completed:,只不过其他参数已经给了默认值。

1
2
3
- (void)sd_setImageWithURL:(nullable NSURL *)url {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}

其实,还有很多类似方法,都是对这个方法不同程度的封装,即给一些参数提供了默认值,或者增加了一些额外操作,比如下边这几个方法:

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
- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock];
}

- (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock];
}

- (void)sd_setImageWithPreviousCachedImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
// 1.取出本地缓存
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:url];
UIImage *lastPreviousCachedImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:key];

// 2.调用本类中的 参数很多的方法,并将缓存数据 (没有时用 placeholder) 传给 placeholder
[self sd_setImageWithURL:url
placeholderImage:lastPreviousCachedImage ?: placeholder
options:options
progress:progressBlock
completed:completedBlock];
}

再来看看这个参数巨多的方法 sd_setImageWithURL: placeholderImage: options: progress: completed: 究竟是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
// 跳转 UIView+WebCache 中的 sd_internalSetImageWithURL: 方法执行,若 <= 3.x.x 时,sd_internalSetImageWithURL: 在本类内部
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:nil
setImageBlock:nil
progress:progressBlock
completed:completedBlock];
}

还是继续调用别的方法,不过这次是调用父类分类 (UIView+WebCache) 中的方法,之所以要调用直接或间接父类的方法,是为了让其他控件 (如 UIButton 等) 可以复用加载网络图片的方法,他们间的关系如下:

UIView和直接或间接子类间的关系.png

现在去父类的分类 UIView+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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
return [self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:operationKey
setImageBlock:setImageBlock
progress:progressBlock
completed:completedBlock
context:nil];
}

// *** 核心方法
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
operationKey:(nullable NSString *)operationKey
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock
context:(nullable NSDictionary<NSString *, id> *)context {

// 1.取消当前 validOperationKey 对应的 loadOperation
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];

// 创建 manager(两种:1.用户自定义 2.此库自带的单例)
SDWebImageManager *manager;
if ([context valueForKey:SDWebImageExternalCustomManagerKey]) {
manager = (SDWebImageManager *)[context valueForKey:SDWebImageExternalCustomManagerKey];
} else {
manager = [SDWebImageManager sharedManager];
}

// 2.下载
id <SDWebImageOperation> operation = [manager loadImageWithURL:url
options:options
progress:combinedProgressBlock
completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)
{
__strong __typeof (wself) sself = wself;
// ...
SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
if (!sself) { return; }
if (!shouldNotSetImage) {
[sself sd_setNeedsLayout];
}
if (completedBlock && shouldCallCompletedBlock) {
completedBlock(image, error, cacheType, url);
}
};
// ...
dispatch_main_async_safe(^{
#if SD_UIKIT || SD_MAC
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
[sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock];
#endif
callCompletedBlockClojure();
});
}];

// 3.为 UIImageView 绑定新的 operation,即上边这个 operation
[self sd_setImageLoadOperation:operation forKey:validOperationKey];

} else { // 如果图片 URL 不存在,执行 completedBlock,返回错误提示

dispatch_main_async_safe(^{

if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}

可以看到,父类最终调用了 - (void)sd_internalSetImageWithURL: ... 这个核心方法,虽然实现代码较多,其实主要就做了这么 3 件事:

① 取消 validOperationKey 对应的已经存在的 loadOperation。因为传进来的参数 validOperationKey 是 nil,所以 validOperationKey 实际取的是当前类名对应的字符串 NSStringFromClass([self class])。

② 执行 SDWebImageManager 的方法 - (id <SDWebImageOperation>)loadImageWithURL: ... 下载图片,并返回一个 operation 对象,其实是这个类 SDWebImageCombinedOperation 的实例,与上一步 cancel 的对象是同一类型,下面就会用到。

③ 保存 validOperationKey 与刚刚生成的 operation 之间的映射关系,以备取消时使用(如 ①)。

对于 ② 用到的方法,其实现代码太长,此处代码也做了精简:

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
// SDWebImageManager
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock
{
// ...

// 1.创建 operation
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self; // 肯定是 weak 属性

__weak typeof(strongOperation) weakSubOperation = strongOperation;

// 2.使用 imageCache 的方法查询缓存
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key
options:cacheOptions
done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType)
{
// 以下都是查询结束后 (查到/没查到) 的操作

if (shouldDownload) {

// 3. 需要下载

__weak typeof(strongOperation) weakSubOperation = strongOperation;
strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url
options:downloaderOptions
progress:progressBlock
completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished)
{
// 缩放图片(如果可以的话)
// 缓存图片
// 执行完成的回调
}
} else if (cachedImage) {

// 4. 如果取到了缓存
// 执行完成的回调
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
// 从正在运行的 operation 数组中移除当前 operation
[self safelyRemoveOperationFromRunning:strongOperation];

} else {

// 5. 没取到缓存 && 不允许下载
// 执行完成的回调
// Image not in cache and download disallowed by delegate
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
// 从正在运行的 operation 数组中移除当前 operation
[self safelyRemoveOperationFromRunning:strongOperation];
}
}

return operation;
}

该方法主要是在创建 SDWebImageCombinedOperation 对象,大概的操作见上边的代码注释。只对其中 2 个自己认为更重要点的方法做一个简要说明,详细的讨论可以查看后边的相关篇章。

  • SDImageCache 中查询缓存的方法 - (nullable NSOperation *)queryCacheOperationForKey: ...。这个方法的作用是查询二级缓存,也就依次从内存、磁盘两处缓存查询我们需要的图片数据,前者查不到时,才在后者中查找。
1
2
3
4
// SDImageCache
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key
options:(SDImageCacheOptions)options
done:(nullable SDCacheQueryCompletedBlock)doneBlock;
  • SDWebImageDownloader 中下载图片的方法 - (nullable SDWebImageDownloadToken *)downloadImageWithURL: ...。此方法的实现通过 operation 和 operationQueue 配合使用来执行下载操作的,即 先分别创建 operation 和 operationQueue,然后将 operation 添加到 operationQueue 中,就自动启动任务了。
1
2
3
4
5
// SDWebImageDownloader
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
  • 无论是查询缓存还是请求网络数据,最终都会给 imageView 赋值,也就是我们通常希望达到的效果。

小结

至此,我们大概理了一下 使用 SDWebImage 加载网络图片的主要流程,最后借用 SDWebImage 作者提供的一张时序图做个简单总结吧。

SDWebImageSequenceDiagram.png

源码注释及 demo

HHSDWebImageStudy

0%