源码分析之SDWebImage

前言


在iOS开发中,很多都使用到SDWebImage库,该库使用了外观模式,提供简洁的API,UIButtonUIImageView等类可以直接调用API进行网络图片的下载,接下来,就来分析一下SDWebImage的整个工作流程。

整体结构


查看SDWebImage的整个项目结构,除去一些辅助的类,其结构大致如下图所示,UIView+WebCacheOperation类作为UIView的类别,提供基本的方法,如设置、取消、移除Operation操作,该Operation满足<SDWebImageOperation>协议,并通过关联引用来进行存储。
UIImageView+WebCacheSDWebImageManager等类提供对外的接口。
SDWebImageManager类将作为Manager来进行下载和缓存的管理,而真正负责下载的类为SDWebImageDownloader类,SDWebImageDownloaderOperation负责封装下载对象;管理缓存的类为SDImageCache
好了,大致结构就是这样,接下来将通过一次调用来分析其内部的具体流程。

具体流程


接下来,以设置UIImageView类的图片为例,我们将使用UIImageView+WebCache提供的API,其接口方法非常简便,你可以什么都不用管,直接调用- (void)sd_setImageWithURL:(NSURL *)url方法就能进行图片的赋值,SDWebImageCache库将自动根据默认配置进行下载和缓存。
UIImageView+WebCache类提供的下载接口如下,每个接口方法只是参宿的个数不一样,不管是使用其中的哪个方法,最终都会等同于调用最后一个方法,其中缺省的参数会使用默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)sd_setImageWithURL:(NSURL *)url;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
- (void)sd_setImageWithURL:(NSURL *)url completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletionBlock)completedBlock;
//最终都会通过我来调用
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

接下来,我们就来看一下方法的处理流程,代码如下所示:

  1. 代码1:用来取消之前的图片加载。
  2. 代码2:当选项不包含SDWebImageDelayPlaceholder时,设置占位图片。
  3. 代码3:调用SDWebImageManager单例类的方法来进行图片的下载和缓存管理,该方法返回满足<SDWebImageOperation>协议的对象,在该方法的completed参数中,我们进行一些基本的操作,如将图片赋给ImageView等。
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
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
//1
[self sd_cancelCurrentImageLoad];
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
//2
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
if (url) {
// check if activityView is enabled or not
if ([self showActivityIndicatorView]) {
[self addActivityIndicator];
}
__weak __typeof(self)wself = self;
//3
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);
}
});
}];
//将operation通过关联引用存储到字典对象中进行管理
[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);
}
});
}
}

接下来,我们再接着往下走,分析一下上面代码中调用的- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock方法,代码如下,由于原代码代码量较多,所以裁剪了一些简单的处理代码。

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
//1
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
dispatch_main_sync_safe(^{
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
});
return operation;
}
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
NSString *key = [self cacheKeyForURL:url];
//2
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
//3
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
completedBlock(image, nil, cacheType, YES, url);
});
}
//配置项的赋值
// download if no image or requested to refresh anyway, and download allowed by delegate
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
//4
if (image && options & SDWebImageRefreshCached) {
// force progressive off if image already cached but forced refreshing
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// ignore image read from NSURLCache if image if cached but force refreshing
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
//5
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {
}
else {
//6
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
}
//7
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
//调用委托方法来获得transform后的图片
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
//进行图片的缓存
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
});
}
else {
if (downloadedImage && finished) {
//进行图片缓存
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
}
}
if (finished) {
@synchronized (self.runningOperations) {
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
}
}];
//取消的Block,当取消操作执行时调用
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
};
}
//找到缓存图片时调用
else if (image) {
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
//移除操作对象
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
}];
return operation;
}
  1. 代码1用来判断请求的URL是否包含在失败的URL列表中,如果包含,且请求没有设置重试选项,则直接返回,不再进行接下来的流程。
  2. 接下来,着重看一下代码2部分,该操作是给operationcacheOperation属性赋值,调用SDImageCache类的- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock方法,该方法将查询是否在硬盘或者内存中已经存在该图片,查询完之后调用done完成块,接下来看一下done完成块中的操作。
  3. 代码3的if判断满足条件为:没有找到缓存的image,或者有需要刷新缓存
    的选项且调用的委托方法返回True
  4. 代码4:当image存在,且选项包含SDWebImageRefreshCached时,去掉下载选项中的SDWebImageDownloaderProgressiveDownload选项,既移除progresive功能,该功能可以在图片下载的过程中渐进式的展示图片;除此之外,再把SDWebImageDownloaderIgnoreCachedResponse选项加到下载选项中,该选项忽略NSURLCache的缓存(使用SDWebImageCache库作请求图片时默认是不使用NSURLCache缓存的),关于NSURLCache,可参考NSURLCache
  5. 代码5:创建一个下载图片的操作,调用SDWebImageDownloader类的- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock方法,先来看一下completed完成块的操作,先假设下载操作完成。
  6. 代码6:如果下载选项包括错误重试时,将URL从错误URL列表中移除。
  7. 代码7:如果下载图片时有用到transform,将进入该if块,接着会调用委托来让我们进行相应的transform,之后将调用SDImageCache类来进行图片的缓存。

缓存

接下来,分析一下SDImageCache类的工作流程,SDImageCache类负责图片的缓存管理,缓存包括内存、硬盘两种缓存方式:

  • 内存缓存:使用NSCache类,通过监听内存警告的通知,在得到通知后移除NSCache类中保存的数据。
  • 硬盘缓存:既将图片作为文件保存到硬盘上,关于图片过期的问题,SDImageCache类的处理方式是:监听App的状态,当进入后台运行时,将开启一个后台任务,对硬盘上保存的图片进行清理,这个过程包含两个操作(具体代码可参见SDImageCache类的- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock方法):
    • 移除过期的文件。
    • 当缓存的文件大小已经操作最大缓存大小时,将移除文件更新时间最老的图片文件,直到小于最大缓存为止。

下载

图片的下载使用的NSOperationQueue来实现,它的好处就是可以cacel,且可以添加依赖,SDWebImage除了默认的FIFO先进先出策略外,还提供LIFO的策略,既后进先出,先执行后加入的下载操作,这个策略就是通过依赖来完成的。
下载操作被封装在SDWebImageDownloaderOperation对象中,SDWebImageDownloaderOperation继承自NSOperation类。
图片的下载使用的NSURLSession,通过实现NSURLSession相关的委托协议,来实现Progress的调用、协议认证等操作。

总结

SDWebImage库整体结构还是很清晰的。对网络部分比较感兴趣的可以读一下源码。

热评文章