SD_webImage调用链路探索

前言

  关于sd_webImage是这样介绍的。Asynchronous image downloader with cache support with an UIImageView category. (一个异步下载且支持缓存的uiimageview分类)。

很多时候图片资源都是作为一个远程资源。为了加载图片需要根据url(统一资源定位符)去获取资源。我以前曾亲身体会到使用

正文

1
2
3
[UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:url]]];///1

[self.imageView sd_setImageWithURL:[NSURL URLWithString:self.imageUrl]];///2

  两者带来极大不同效果。方法1是在主线程中货源图片资源并且加载,如果把这个事情放到列表中去做,滑动列表时会出现明显的卡顿效果,这是因为主线程因为加载资源被阻塞了。

  使用方法2就是把资源获取通过异步的方式放在子线程中处理,取得之后通过回调的方式在加载图片。列表滑动会变得相当流程。

  在对SD_webImage常用方法进行探索前,可以先明确一下实现这样一个异步加载器需要些什么。首先是”异步“,可以猜测其中关于根据图片url获取图片的操作不是在主线程进行的。然后是“缓存”,一个常见的缓存包含了内存缓存和磁盘缓存。所以在第一次获取到资源后应该会有一个类似cache保存的操作,减少下次重新下载耗费的时间。也许还会有一个磁盘缓存,app下次启动时如果发现已经存在这个资源也需要再次下载了。要做到这些需要一些缓存策略之类的东西。然后就是”分类”,这是一个对于uiimageView的分类,提供了一些新的分类方法。所以整个流程大概如下,uiimageview在加载图片时首先判断这个图片是否在内存中有无,有的话直接返回,没有的话再去本地路径中查找,有的话就返回,没有再去发起get请求,获取图片资源数据。获取之后再把数据存到内存中,同时存到本地。然后把数据通过回调返回。下面根据一次调用过程去看整个链路是如何工作的。

1
2
3
4
5
6
7
8
9
10
11
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
这是我们常用的加载图片的方法。这个方法暴露在 <UIImageView+WebCache.h>中,到实现文件查看,发现,几个同类型的方法最后都走向同一个方法

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;

  这是uiview分类中的方法,因为作者也为buButton和NSButton提供了异步加载图片的能力,所以在他们共同的父类中扩展方法可以使其都可以调用。

  在这个方法里面首先是一些上下文的操作,以UIImageview为例,首先尝试获取operationkey,对这个operationkey的解释是(pass through the operation key to downstream, which can used for tracing operation or image view class),意思就是方便追踪正在操作的视图类。获取到之后会

1
2
self.sd_latestOperationKey = validOperationKey;
[self sd_cancelImageLoadOperationWithKey:validOperationKey];

  意思就是如果validOperationKey对应的视图对象正在下载图片,会取消正在下载的操作。感觉这部分理解起来是要表达这么一个意思,因为对于不同的UIImageview对象会生成相同的operationkey,所以每次只会对一个uiimageview对象进行下载操作。(刚才想了下,我的想法有问题,虽然不通的UIImageview对象生成了相同的key,但是不同的对象应该持有的是不同的操作下载队列,不同的uiimageview对象是否同时在下载应该是NSURLSession关心的事情接下来就是把站位图放到视图上。接下来到调用下载方法之间还有一堆操作,大致理解一下就是在下载的过程中,增加一个指示器,根据下载进度更新指示器,完成之后移除指示器。

  最主要的工作就是下面这个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nonnull SDInternalCompletionBlock)completedBlock;
///completedBlock

typedef void(^SDInternalCompletionBlock)(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL);

/// Image Cache Type
typedef NS_ENUM(NSInteger, SDImageCacheType) {

SDImageCacheTypeNone,

SDImageCacheTypeDisk,

SDImageCacheTypeMemory,

SDImageCacheTypeAll
};

  这个方法由SDWebImageManager进行管理,completedBlock中有很多的参数,特别关注下其中的cacheType,查看枚举定义,很明显的看出后面很根据cachetype的值决定是否下载。下载完成的会调用也是处理一些是否下载成功,是否出现错误的保护操作。继续进到下载的方法中查看,首先是对url的一些校验,最关键的还是其中的这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    // Start the entry to load image from cache
[self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock]; ///call


// Query normal cache process
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock; /// configeration

///
// Check whether we should query cache
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);

  这就是前面说的查询缓存的方法,根据shouldQueryCache 决定是否查询缓存,假定命中了需要查询缓存,然后就是根据url和context查询key值,在根据key值去查询。

///查询缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
- (id<SDWebImageOperation>)queryImageForKey:(NSString *)key
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
cacheType:(SDImageCacheType)cacheType
completion:(SDImageCacheQueryCompletionBlock)completionBlock

/// Policy for cache operation
typedef NS_ENUM(NSUInteger, SDImageCachesManagerOperationPolicy) {
SDImageCachesManagerOperationPolicySerial, // process all caches serially (from the highest priority to the lowest priority cache by order) 串行
SDImageCachesManagerOperationPolicyConcurrent, // process all caches concurrently。并行
SDImageCachesManagerOperationPolicyHighestOnly, // process the highest priority cache only 高优先级
SDImageCachesManagerOperationPolicyLowestOnly // process the lowest priority cache only 低优先级
};

  到这里,发现东西越来越杂,看的有点头疼了。但是主要过程大致如下:

  如果caches为空,直接返回,如果cache中一个值,直接查询,否则根据策略决定使用何种方式查询。选择默认的 SDImageCachesManagerOperationPolicySerial策略看下内部,内部主要调用的方法是:

1
2
3
4
5
6
7
- (nullable id<SDWebImageOperation>)queryImageForKey:(nullable NSString *)key
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
cacheType:(SDImageCacheType)cacheType
completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock;
///最终查询方法
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock;

  在最终内存查询的方法中,就是找到回调中带有image数据,没找到,回调中数据为nil,在末尾终于被我找到了,shouldQueryMemoryOnly,如果要执行disk查找的话, shouldQueryMemoryOnly返回no。最终都会返回一个doneBlock给外部。好,现在回到外层,如果一开始就认为不用查询缓存,就会执行下载操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Download process
- (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(SDWebImageContext *)context
cachedImage:(nullable UIImage *)cachedImage
cachedData:(nullable NSData *)cachedData
cacheType:(SDImageCacheType)cacheType
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;

/// 接口请求图片
- (nullable id<SDWebImageOperation>)requestImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDImageLoaderCompletedBlock)completedBlock;

  这一步就会去调用接口请求数据了,这个方法里面还有一个for循环,所以应该还存在并行调用的场景,最终的下载方法是:

1
2
3
4
5
6
7
8
9
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

///存储
// Continue store cache process
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];

  通过这个方法会去下载图片,如果下载成功,就会调用存储方法:存储之后在讲对应的回调返回出去。

  以上就是一次图片下载经过的整个过程,大致流程和一开始预测的过程类似。但是其中包含了设计者相当多的设计思路,尽量保证每一种场景case都考虑在内,所以其中if,esle场景判断相当多。

  其次是其中代码的管理也相当规范,可以看到暴露给用户的就是一个简单的sd_setImageWithURL方法,但是内部完成的动作可以说是相当复杂。而且其中还有很多我当前没理解到的地方,比如说context的管理, 各个manager对于多种operation的管理。还有内部代码多是使用block回调处理的,很容易一下就看不明白了。

  只能说这次缕了一下简单的过程,其中代码管理,各个模块协调的部分还需要继续往下看才行。

  参考
https://github.com/SJ110/analyze/blob/master/contents/SDWebImage/iOS%20%E6%BA%90%E4%BB%A3%E7%A0%81%E5%88%86%E6%9E%90%20—%20SDWebImage.md