SDWebImage解析

SDWebImage是开发中常用的网络加载图片的库,使用频率高到令人发指。本身优秀的接口封装和底层的内存管理都让开发者免去了很多麻烦,那么作为一个开发者,有必要对这么一个每天都要打交道的库有一些更深入的了解。这里我记录下自己阅读源码的心得,算是做个笔记。

1
2
3
4
5
sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock

而这个方法里面是直接调用

1
2
3
4
5
6
7
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

这个方法首先第一步就是检测是否用了相同的请求,在UIView的webCacheOperation category中,实现方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
// Cancel in progress downloader from queue
SDOperationsDictionary *operationDictionary = [self operationDictionary];
id operations = operationDictionary[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];
}
}

来看看这个方法,SDOperationDictionary是一个普通的dictionary,当一个UIView或者其子类调用webCacheOperation方法的时候会为其延迟加载一个dictionary,由于这个property只会在运行时声明和调用,所以这里采取objc_getAssociatedObject 关联对象的方法,对于每个UIView,如果要使用SDOperationDictionary这个property,那么会在需要的时候调用。实际上整个框架里的操作都是通过一个operationDictionary来管理,添加到VIView上可以使任何UIView的子类可以直接绑定对应的operationDictionary。

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

这个方法为UIView动态添加一个property。
回到sd_cancelImageLoadOperationWithKey方法,从这个dictionary中取出对应key的元素,如果是数组则每一个都取消,否则直接取消(前提是实现SDWebImageOperation这个协议)这样的话当开启一个新的操作的时候不会被前面的操作所影响。

取消之后,首先如果需要的话(SDWebImageOption 这个flag中没有选择 SDWebImageDelayPlaceholder)在主线程中使用placeholder图片来暂时占据发起图片加载请求的UIView(通常为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
37
38
39
40
41
42
if (url) {
// check if activityView is enabled or not
/* indicator 操作*/
__weak __typeof(self)wself = self;
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
__strong __typeof (wself) sself = wself;
[sself sd_removeActivityIndicator];
if (!sself) {
return;
}
dispatch_main_async_safe(^{
if (!sself) {
return;
}
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
completedBlock(image, error, cacheType, url);
return; // 通常情况下当图片加载完成之后就直接将其设置为UIImageView的图片,只要sdWebImageAvoidAutoSetImage这个flag设定被选择
} else if (image) {
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout]; //这个方法其实直接调用setImageBlock,将image和data作为参数赋予block
} else {
if ((options & SDWebImageDelayPlaceholder)) {
[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];// 特别情况,placeholder的赋予被手动延迟,在图片加载完成之后没有成功的情况下才实现
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}// 只有需要手动完成图片显示的情况下才需要在这里最终完成complete的调用
});
}];
[self sd_setImageLoadOperation:operation forKey:validOperationKey]; // 将这个请求加入到operation的dictionary中
} else {
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);
}
});
}

上面关键的部分我加入了一些注释,这是在取得了image & data的情况下怎么样调用数据的处理。总的来说这个部分其实是完成三个功能:(1)创建一个新的operation指令对象 (2)赋予这个对象complete block (3)将请求加入UIView的opereation dictionary中

接下来先看如何创建一个operation对象,至于dictionary中的指令队列处理放到后面再说。
首先看看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背后,管理异步下载(SDWebImageDownloader)和图片缓存(SDImageCache)的类。实际上和它的名字本身是一样,是一个统合管理的类,构建起几个模块中间的桥梁。

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
// SDWebImageManager
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock{
/* 字符串处理 */
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation; // 创建一个新的operation对象
/* url合法性判断,长度不为0,不在记录的已失败队列中 */
/* 为operation对象的NSOperation类property---cacheOperation赋值
operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
//* 如果需要重新更新图片cache */
重新从服务端获取新的数据,并根据SDWebImageDownloaderOptions来调整缓存策略
// 创建operationToken对象
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
/* 如果operation被取消了则什么都不干 */
/* 如果error了则将url加入到failUrl数组中 */
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost
&& error.code != NSURLErrorNetworkConnectionLost) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
// 正常情况
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
} 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), ^{
// 当图片数据从网络端下载好之后, 如果delegate完成了图片转化的功能,那么会先将图片交由delegate进行一定的转换(编解码等处理)然后将得到的transformedImage进行缓存
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
//缓存图片,如果是被改变过的,则将imageData置为nil,这样imageCache能够根据处理过的transformedImage来重新计算data
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
}
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
});
} else {
// 如果不需要下载好的图像进行处理则直接缓存
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
}
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
if (finished) {
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
operation.cancelBlock = ^{
// 对operation的cancelBlock进行赋值
[self.imageDownloader cancel:subOperationToken];
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self safelyRemoveOperationFromRunning:strongOperation];
};
}else if (cachedImage) {
//已经缓存了图片的情况
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
} else {
// 图片没有被缓存并且也不被允许下载
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
[self safelyRemoveOperationFromRunning:operation];
}
}
return operation; }

如上面的代码所示,SDWebImageManager中调用方法返回一个operation对象的过程大致是(1)创建一个SDWebImageCombinedOperation对象 (2)调用[self.imageCache queryCacheOperationForKey:key done:^(UIImage cachedImage, NSData cachedData, SDImageCacheType cacheType)方法赋值operation的cacheOperation,里面包含从网络获取图片的block以及对image数据预处理和缓存的指令(3)给operation的cancelBlock赋值
到这里我们就已经是在成功获取图片的前提下进行了一系列操作,大家其实都可以明白,由于是将回调block的形式进行调用,所以这里在底层执行的顺序其实和我们现在一步步查找代码的调用顺序是反过来的,下面才是真正拿到图片的重头戏:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#SDWebImageDownloader
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
__weak SDWebImageDownloader *wself = self;
// 直接调用了另一个方法,为下载操作添加回调的块
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
/* 很多操作,后面再谈 */
}];
}

可以看到是直接调用另一个方法的,那我们先看这个被调用的方法:

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
- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
forURL:(nullable NSURL *)url
createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
// URL被用来作为字典的Key,所以一定不能为空,否则全部清空
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return nil;
}
// 创建一个新的token
__block SDWebImageDownloadToken *token = nil;
dispatch_barrier_sync(self.barrierQueue, ^{
// 从字典中取出对应url的operation
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (!operation) {
// 如果不存在的话就初始化一个,并且设定comletionBlock:执行完成之后就把自己从字典中删掉(注意block中变量的使用方法防止retain loop,)需要注意的是,如果已经创建过operation就没有必要再进行一次这个操作了
operation = createCallback();
self.URLOperations[url] = operation;
__weak SDWebImageDownloaderOperation *woperation = operation;
operation.completionBlock = ^{
SDWebImageDownloaderOperation *soperation = woperation;
if (!soperation) return;
if (self.URLOperations[url] == soperation) {
[self.URLOperations removeObjectForKey:url];
};
};
}
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
// 将url和对应的operation生成的cancelToken作为参数,生成token并return
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
return token;
}

而上面可以看到,系统自己调用的createCallback是这样的:

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
__weak SDWebImageDownloader *wself = self;
// 真的直接调用addprogressCallBack方法
return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
__strong __typeof (wself) sself = wself;
NSTimeInterval timeoutInterval = sself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}
// 为了禁止有可能出现的重复缓存(NSURLCache + SDImageCache),这里默认禁用图片请求时加入的cache
// NSURLRequestCachePolicy 当URL加载系统处理一个请求时在缓存系统中采取的策略,这里的选项NSURLRequestReloadIgnoringLocalCacheData代表着从URL加载来的数据必须来自数据源头,不管是不是缓存了最新的数据,都不能直接将本地的缓存的图片数据当做请求的结果。
// 默认情况
NSURLRequestCachePolicy cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
// 根据传入的option来改变缓存策略
if (options & SDWebImageDownloaderUseNSURLCache) {
if (options & SDWebImageDownloaderIgnoreCachedResponse) {
cachePolicy = NSURLRequestReturnCacheDataDontLoad;
} else {
cachePolicy = NSURLRequestUseProtocolCachePolicy;
}
}
// 建立请求request
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval];
// request的参数设置
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;
// headersFilter这个block负责从一个大的httpheader字典中筛选出作为http request的header的字典,如果block为空就直接把loader类的httpheaders赋值给request
if (sself.headersFilter) {
request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
}
else {
request.allHTTPHeaderFields = sself.HTTPHeaders;
}
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
if (sself.urlCredential) {
operation.credential = sself.urlCredential;
} else if (sself.username && sself.password) {
operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
}
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 最关键的,将请求加到downloadQueue里
[sself.downloadQueue addOperation:operation];
// 如果是LIFO策略就将这个请求调到最前面
if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// Emulate LIFO execution order by systematically adding new operations as last operation's dependency
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];

注意实际上调用的时候completion blocks执行的顺序其实和上面分析的顺序正好是反过来的,毕竟只有底层调用完成之后才能执行上一层的completion操作,而且也需要说明的是由于最底层的operation请求不是直接启用,而是加入到operationQueue中等待执行,所以所有的处理方法全部只能用block的形式封装起来进行传递。
整个框架在阅读起来比较麻烦,因为采用了太多的回调的形式,并且manager类联系所有其他功能类的这个结构,一开始没发现的话其实在不同类中跳来跳去十分繁琐。但是一但了解这种设计模式,是十分有好处的,并且框架中大量使用的block语法和多线程编程,十分精彩。阅读这个框架的源码确实能学到很多东西。

总的来说SDWebImage的加载策略非常直观,首先请求内存是否有URL对应的图片,如果没有就查找磁盘中的缓存;
如果缓存中都没有的话发起请求异步加载图片(当然加载的过程,设置还有对应的回调是最精髓的地方),将请求加入到待执行的请求队列中,一个个执行。直到异步加载成功之后,执行completion回调,缓存新下载的图片并更新UI(如果需要的话)。

补充说明:NSURLCache的执行策略

上面有些地方忽略说明,因为很难一两句交代清楚,这里补充说明一下NSURLCache的缓存策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
NSURLRequestUseProtocolCachePolicy = 0,
// 默认的缓存策略,由协议制定了最好的实现方式
NSURLRequestReloadIgnoringLocalCacheData = 1,
// 完全从服务器端加载数据,忽略本地缓存
NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // Unimplemented
NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,
NSURLRequestReturnCacheDataElseLoad = 2,
// 使用缓存数据,忽略过期时间,只有在没有缓存版本的时候才从远端加载数据
NSURLRequestReturnCacheDataDontLoad = 3,
// 只使用cache数据
NSURLRequestReloadRevalidatingCacheData = 5, // Unimplemented
};

根据官网的说明,应该遵循的流程如下:
(1) 如果请求的缓存响应不存在,直接从源加载数据
(2) 如果存在,查看是否每次需要重新验证,如果不是响应的缓存的过期则直接加载缓存数据
(3) 如果缓存的响应过期或者需要重新验证,URL加载系统发送HEAD请求到源,查看是否需要抓取新的响应
(4) 如果需要就重新请求,如果不需要就直接返回缓存的数据

前面提到的源码中调用了NSURLRequestCachePolicy的大致逻辑就是如此。

总结SDWebImage的流程

SDWebImage中一般调用sd_setImageWithURL:placeholderImage:这个方法,最终它会调用一个需要progressBlock,completionBlock的方法。

然后获取SDWebImageManager中的单例调用一个downloadImageWithURL:…方法来获取图片,先查找魂村中的记录,以URL作为索引,顺序是先查找缓存然后查找磁盘中的数据,如果有就直接使用。如果都没有那么manager就会调用加载图片的方法downloadImageWithURL:来从网络获取图片,它会调用另一个addProgressCallback:andCompletedBlock:URL:createCallback:来存储progress和completed的回调block,第一次添加时会实例化NSMutableURLRequest和SDWebImageDownloaderOperation,然后将operation加入下载队列开始异步加载,下载完成后使用图片。

本身倒是挺通俗易懂的。

当前网速较慢或者你使用的浏览器不支持博客特定功能,请尝试刷新或换用Chrome、Firefox等现代浏览器