近期公司项目用到SDWebImage(v3.8.2),来实现聊天图片的缓存,正好有时间,想好好研究下SDWebImage。本文是阅读网上许多关于SDWebImage文章加上个人理解写下的,也方便日后复习。

目录

  1. 前言
  2. 思维导图
  3. 源码结构
  4. 目录结构
  5. 运行流程
  6. 核心方法的解析
  7. 完整核心方法注释

前言

首先老套路来介绍一下这个 SDWebImage 这个著名开源框架吧, 这个开源框架的主要作用就是:

Asynchronous image downloader with cache support with an UIImageView category.

一个异步下载图片并且支持缓存的 UIImageView 分类.就这么直译过来相信各位也能理解, 框架中最最常用的方法其实就是这个:

基本方法
1
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

在看源码之前,先对思维导图、源码结构、运行流程先有个总体把握可以加深我们对后续代码的理解。

思维导图

思维导图思维导图

源码结构

源码结构源码结构

源码结构图已经将这个框架是如何组织的基本展现出来, UIImageView+WebCacheUIButton+WebCache 直接为表层的 UIKit框架提供接口, 而 SDWebImageManger 负责处理和协调 SDWebImageDownloaderSDWebImageCache. 并与 UIKit 层进行交互, 而底层的一些类为更高层级的抽象提供支持.

目录结构

  • Downloader
    • SDWebImageDownloader
    • SDWebImageDownloaderOperation
  • Cache
    • SDImageCache
  • Utils
    • SDWebImageManager
    • SDWebImageDecoder
    • SDWebImagePrefetcher
  • Categories
    • UIView+WebCacheOperation
    • UIImageView+WebCache
    • UIImageView+HighlightedWebCache
    • UIButton+WebCache
    • MKAnnotationView+WebCache
    • NSData+ImageContentType
    • UIImage+GIF
    • UIImage+MultiFormat
    • UIImage+WebP
  • Other
    • SDWebImageOperation(协议)
    • SDWebImageCompat(宏定义、常量、通用函数)
类名 功能
SDWebImageDownloader 是专门用来下载图片和优化图片加载的,跟缓存没有关系
SDWebImageDownloaderOperation 继承于 NSOperation,用来处理下载任务的
SDImageCache 用来处理内存缓存和磁盘缓存(可选)的,其中磁盘缓存是异步进行的,因此不会阻塞主线程
SDWebImageManager 作为 UIImageView+WebCache 背后的默默付出者,主要功能是将图片下载(SDWebImageDownloader)和图片缓存(SDImageCache)两个独立的功能组合起来
SDWebImageDecoder 图片解码器,用于图片下载完成后进行解码
SDWebImagePrefetcher 预下载图片,方便后续使用,图片下载的优先级低,其内部由 SDWebImageManager 来处理图片下载和缓存
UIView+WebCacheOperation 用来记录图片加载的 operation,方便需要时取消和移除图片加载的 operation
UIImageView+WebCache 集成 SDWebImageManager 的图片下载和缓存功能到 UIImageView 的方法中,方便调用方的简单使用
UIImageView+HighlightedWebCache UIImageView+WebCache 类似,也是包装了 SDWebImageManager,只不过是用于加载 highlighted 状态的图片
UIButton+WebCache UIImageView+WebCache 类似,集成 SDWebImageManager 的图片下载和缓存功能到 UIButton 的方法中,方便调用方的简单使用
MKAnnotationView+WebCache 跟 UIImageView+WebCache 类似
NSData+ImageContentType 用于获取图片数据的格式(JPEG、PNG等)
UIImage+GIF 用于加载 GIF 动图
UIImage+WebP 用于解码并加载 WebP 图片

运行流程

了解了工程目录结构之后,我们就可以看之前跳过的执行流程图了。

运行流程运行流程

核心方法的解析

SDWebImage 的接口简单易用,开发者一句代码就能使用,代码: - (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; ,复杂的逻辑和实现都被隐藏在这句代码的后面。各模块独立,Cache缓存模块提供库的内存缓存和可选的磁盘缓存支持,Downloader下载模块提供基于 NSURLSessionNSOperation 的下载器。SDWebImageManager 则将 CacheDownloader 两个模块很好的整合在一起,提供基于缓存的图片加载功能。最后是使用 Categories 封装了常用 UI 控件的接口。

我们看UIImageView+WebCache.h文件,我们可以发现为开发者提供了很多类似于下面的这个方法

1
(void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder

这些方法最终都会调用- sd_setImageWithURL: placeholderImage: options: progress: completed:,这个方法可以算是核心方法,下面我们详细看一下内部实现 。

代码的第一句是

1
[self sd_cancelCurrentImageLoad];

看方法名就知道它的作用,就是取消这个视图 ImageView 正在加载图片的操作,如果这个 ImageView 正在加载图片,保障在开始新的加载图片任务之前,取消掉正在进行的加载操作。

看下具体的实现代码 UIImageView+WebCache.m

1
2
3
4
5
6
- (void)sd_cancelCurrentImageLoad {
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}
- (void)sd_cancelCurrentAnimationImagesLoad {
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewAnimationImages"];
}

两个 key 说明有两个不一样的加载方式,一个是单张图片的,另一个是连续下载多张,放到 NSArray<UIImage *> *animationImages 中。
看下取消操作的代码实现,UIView+WebCacheOperation.m:

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
- (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;
}
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
[self sd_cancelImageLoadOperationWithKey:key];
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary setObject:operation forKey:key];
}
- (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];
}
}

代码中,通过 objc_setAssociatedObject 关联对象的方法,给 UIImageView 动态添加了一个 NSMutableDictionary 的属性。通过 key-value 维护这个 ImageView 已经有了哪些下载操作,如果是数组就是 UIImageViewAnimationImages 否则就是 UIImageViewImageLoad 。最后获得的都是遵从了 <SDWebImageOperation> 协议的对象,可以统一调用定义好的方法 cancel,达到取消下载操作的目的,如果 operation 都被取消了,则删除对应 key 的值。
继续看 - (void)sd_setImageWithURL: placeholderImage: options: ; 里的代码

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

如果加载图片的选项不是 SDWebImageDelayPlaceholder 则会在主线程中先设置 placeholder 的占位图,
SDWebImageDelayPlaceholder 的情况后面说。dispatch_main_async_safe 是一个宏定义,点进去一看发现宏是这样定义的

1
2
3
4
5
6
#define dispatch_main_sync_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_sync(dispatch_get_main_queue(), block);\
}

相信这个宏的名字已经讲他的作用解释的很清楚了: 因为图像的绘制只能在主线程完成, 所以, dispatch_main_sync_safe 就是为了保证 block 能在主线程中执行.

下面这段就是这个类中比较关键的代码了

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
if (url) {
// check if activityView is enabled or not
if ([self showActivityIndicatorView]) {
[self addActivityIndicator];
}
__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) {
[wself removeActivityIndicator];
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(^{
[self removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}

先检查 activityView 是否可用,可用的话给 ImageView 正中间添加一个活动指示器,并旋转,加载图片完成或失败都会清除掉。__weak __typeof(self)wself = self; 避免循环引用,接下来就是调用 SDWebImageManager 的方法 downloadImageWithURL: options: progress: completed: ,在该方法的 completed block 回调中,如果 option 是 SDWebImageAvoidAutoSetImage ,就是要求不要给 ImageView 自动设置图片,则只回调 completedBlock 然后 return,否则有 image 就设置给 ImageView 。没有 image 通常就是错误情况,如果 option 是 SDWebImageDelayPlaceholder 则设置占位图(可以设置成提示用户图片没加载出来的图片),最后回调 completedBlock。上面代码最后一句是把这个 operation 存到 ImageView 的 NSMutableDictionary 中,为了之前提到的 [self sd_cancelCurrentImageLoad]; 操作准备的。

完整核心方法注释

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
82
83
84
85
- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_cancelCurrentImageLoad];
//将 url作为属性绑定到ImageView上,用static char imageURLKey作key
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
/*options & SDWebImageDelayPlaceholder这是一个位运算的与操作,!(options & SDWebImageDelayPlaceholder)的意思就是options参数不是SDWebImageDelayPlaceholder,就执行以下操作
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
*/
这是一个宏定义,因为图像的绘制只能在主线程完成,所以dispatch_main_sync_safe就是为了保证block在主线程中执行
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
//设置imageView的placeHolder
self.image = placeholder;
});
}
if (url) {
// 检查是否通过`setShowActivityIndicatorView:`方法设置了显示正在加载指示器。如果设置了,使用`addActivityIndicator`方法向self添加指示器
if ([self showActivityIndicatorView]) {
[self addActivityIndicator];
}
__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) {
//移除加载指示器
[wself removeActivityIndicator];
//如果imageView不存在了就return停止操作
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
/*
SDWebImageAvoidAutoSetImage,默认情况下图片会在下载完毕后自动添加给imageView,但是有些时候我们想在设置图片之前加一些图片的处理,就要下载成功后去手动设置图片了,不会执行`wself.image = image;`,而是直接执行完成回调,有用户自己决定如何处理。
*/
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
completedBlock(image, error, cacheType, url);
return;
}
/*
如果后两个条件中至少有一个不满足,那么就直接将image赋给当前的imageView
,并调用setNeedsLayout
*/
else if (image) {
wself.image = image;
[wself setNeedsLayout];
} else {
/*
image为空,并且设置了延迟设置占位图,会将占位图设置为最终的image,,并将其标记为需要重新布局。
*/
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
// 为UIImageView绑定新的操作,以为之前把ImageView的操作cancel了
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else {
// 判断url不存在,移除加载指示器,执行完成回调,传递错误信息。
dispatch_main_async_safe(^{
[self removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}

UIImageView+WebCache 类中的代码还是很容易的,逻辑很清晰,没有难懂的地方。请接着下篇 SDWebImageManager 源码阅读记录(二)SDWebImageManager解析核心方法:

相关阅读:
SDWebImage 源码阅读记录(一)
SDWebImageManager 源码阅读记录(二)
SDWebImageDownloader 源码阅读记录(三)
SDWebImageDownloaderOperation 源码阅读记录(四)
SDWebImageCache 源码阅读记录(五)
SDWebImage相关类 源码阅读记录(六)