SDWebImage源码分析


一、流程架构图

img
  • SDWebImage对UIButton,UIImageView,NSButton,UIView进行了拓展,并对外提供了接口。无论对UIButton,UIImageView还是NSButton调用sd_setImageWithURL的时候,最终都会调用到UIView拓展类的sd_internalSetImageWithURL方法。
  • 前面的拓展都只是对外的接口,主要逻辑处理放在SDWebImageManager里面。他相当于一个调度中心,如果需要缓存(读跟取),他就会调用SDImageCache,如果需要下载,就会调用SDWebImageDownloader。类似我们MVP模式下的Presenter,收到View拓展接口相关的参数后,根据不同业务传递给cache跟downloader处理,最后将处理完的数据通过block回调给接口。

最后还有一些工具,没有在流程图中画出来,这里说明一下:

  • Decoder:做一些编解码操作,针对不同类型的图片进行不同的操作。
  • Transform:从缓存或下载转换图像加载的转换器协议。
  • AnimatedImage:可以替代UIImageView,支持gif
  • Utils:存放一些枚举,Define,还有菊花器
  • Categories:对需要的类进行拓展,大部分是UIImage
  • Private:一些私人方法

二、代码部分

1. UIView+WebCache

直接找到sd_internalSetImageWithURL方法,这是入口进来后第一个处理的方法,处理内容如下:

a. 拿到旧的operation(任务),取消其操作,并从SDOperationsDictionary移除。然后创建新的加载任务,并加入到SDOperationsDictionary中。

b. 处理进度条,重置进度条

c. 处理菊花器

d. 创建SDWebImageManager,并调用loadImageWithURL加载图片

我们先看sd_internalSetImageWithURL方法里面的代码

a、取消之前的任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
*通过SDWebImageContextSetImageOperationKey拿到SDOperationsDictionary的key:validOperationKey(说白了这里就是二维字典)
*在通过validOperationKey拿到对应的Operation(任务),对任务进行取消之类的相关操作
*/
NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
if (!validOperationKey) {
// pass through the operation key to downstream, which can used for tracing operation or image view class
validOperationKey = NSStringFromClass([self class]);
// 对context进行深拷贝,转为可变字典
SDWebImageMutableContext *mutableContext = [context mutableCopy];
// 将当前类对象名称装载进mutableContext
mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
// 将mutableContext转回不可变字典context
context = [mutableContext copy];
}
// 将validOperationKey存储起来
self.sd_latestOperationKey = validOperationKey;
// 如果这个key存在任务,则取消任务,且从SDOperationsDictionary移除
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
// 将url存储起来
self.sd_imageURL = url;

这段代码主要是拿到validOperationKey,并传给sd_cancelImage方法,sd_cancelImage的逻辑也很简单,通过validOperationKey,在SDOperationsDictionary里面拿到对应的任务,并取消。下面是sd_cancelImage的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
// Cancel in progress downloader from queue
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id<SDWebImageOperation> operation;
// 拿到对应的任务operation
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
if (operation) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
// 如果存在并遵循SDWebImageOperation代理,则取消任务
[operation cancel];
}
// 最后将任务移除
@synchronized (self) {
[operationDictionary removeObjectForKey:key];
}
}
}
}
b、占位图显示
1
2
3
4
5
6
7
// 是否需要延迟加载占位图
if (!(options & SDWebImageDelayPlaceholder)) {
// 主线程显示占位图
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
c、进度条,菊花器处理逻辑
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
if (url) {
// reset the progress
// 重置进度条
NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
if (imageProgress) {
imageProgress.totalUnitCount = 0;
imageProgress.completedUnitCount = 0;
}

#if SD_UIKIT || SD_MAC
// check and start image indicator
// 有菊花器就转菊花
[self sd_startImageIndicator];
id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
// 拿到当前manager,没有就创建,有的话就将context的移除,防止循环引用
SDWebImageManager *manager = context[SDWebImageContextCustomManager];
if (!manager) {
manager = [SDWebImageManager sharedManager];
} else {
// remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
SDWebImageMutableContext *mutableContext = [context mutableCopy];
mutableContext[SDWebImageContextCustomManager] = nil;
context = [mutableContext copy];
}

// 对进度条进行处理,如果菊花器是进度条类型的,那就让进度条跑起来,回调进度block
SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
if (imageProgress) {
imageProgress.totalUnitCount = expectedSize;
imageProgress.completedUnitCount = receivedSize;
}
#if SD_UIKIT || SD_MAC
if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
double progress = 0;
if (expectedSize != 0) {
progress = (double)receivedSize / expectedSize;
}
progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
dispatch_async(dispatch_get_main_queue(), ^{
[imageIndicator updateIndicatorProgress:progress];
});
}
#endif
if (progressBlock) {
progressBlock(receivedSize, expectedSize, targetURL);
}
};

注意这里并没有直接调用combinedProgressBlock处理进度条,而是在下面加载图片的时候将combinedProgressBlock扔过去处理。

d、通过SDWebImageManager调用加载图片的方法

这里调用了SDWebImageManager的图片加载方法,将一些必要参数传递过去,接下来就是SDWebImageManager的事情了。

1
[manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)

2. SDWebImageManager

直接找到loadImageWithURL方法,这个方法主要是对url的一些判断,contextoptions的预处理,内容如下:

a. 先判断url的可行性

b. 对contextoptions进行预处理,并放到result里面

c. 调用callCacheProcessForOperation 判断是否有缓存,如果有则进入ImageCache 拿到缓存数据,如果没有则进入callDownloadProcessForOperation 方法进一步判断如何下载

先看看这些步骤的源码,看完再看callCacheProcessForOperation做了些什么

a、判断url的可行性
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
// Invoking this method without a completedBlock is pointless
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

// Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
// throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}

// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}

// 可以把operation当做是一个任务,一个执行着读取图片(缓存跟加载器的组合)操作的任务
SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self;

BOOL isFailedUrl = NO;
// 检测当前url是否在failedURLs列表中
if (url) {
//os_unfair_lock的宏定义
// 加锁,防止多个线程对failedURLs操作,引起的数据问题
SD_LOCK(_failedURLsLock);
isFailedUrl = [self.failedURLs containsObject:url];
SD_UNLOCK(_failedURLsLock);
}

// 如果url为nil,且未设置SDWebImageRetryFailed,url在failedURLs列表中,执行失败回调
// SDWebImageRetryFailed为失败链接重试,默认是不会重试
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
return operation;
}

这里注释应该很清楚了,就是判断url的可行性,跟url是否在失败列表里面,如果在的,且options没有SDWebImageRetryFailed的话,就直接失败回调。值得注意的是SD的锁在iOS10以上用的是os_unfair_lock,iOS10以下用的是OSSpinLockLock(这个锁存在任务优先级问题,已经被淘汰了)

b、对contextoptions进行预处理,并放到result里面
1
2
3
4
5
6
7
8
9
10
11
// 将当前operation加入到runningOperations(正在运行的operation)
// 加锁,防止多个线程对runningOperations进行操作
SD_LOCK(_runningOperationsLock);
[self.runningOperations addObject:operation];
SD_UNLOCK(_runningOperationsLock);

// Preprocess the options and context arg to decide the final the result for manager
// 对context进行预处理,然后将处理的context跟options包装到result里面。
/* 里面对context的处理包括,SDWebImageContextImageTransformer、SDWebImageContextCacheKeyFilter、SDWebImageContextCacheSerializer。分别查看外面是否自定义这3个key的context,如果有就使用,没有就使用SD默认的。除了SDWebImageContextCacheKeyFilter(缓存url的key)默认是本身的url,其他2个都是nil
*/
SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];

这里先将任务加入到正在执行的列表里面,然后再对context进行预处理,源代码是没有对options进行说明处理的,然后将contextoptions放入result里面。context的处理源代码就不贴出来了,大概就是对SDWebImageContextImageTransformerSDWebImageContextCacheKeyFilterSDWebImageContextCacheSerializer这3个进行一个判断,看是否有自定义的传过来,没有就用默认的。

c、callCacheProcessForOperation的调用

这里主要是判断要到哪里去取数据,ImageCache,还是去下载,接下来就进入这个方法看一下。

这里主要是判断任务是否该走缓存查询,或者直接下载。如果是缓存查询,就进入SDImageCache里面进行缓存查询,且在此处理缓存结果的回调。否则就调用callDownloadProcessForOperation进入下一步判断。

①. 拿到imageCache,拿到缓存类型queryCacheType

②. 通过 options判断,走缓存还是下载。如果走缓存,则调用SDImageCache里面的queryImageForKey(开始进入SDImageCache的逻辑);如果走下载,则调用callDownloadProcessForOperation开始下载前的一些处理。

①、拿到imageCache,拿到缓存类型queryCacheType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Grab the image cache to use
// 查看是否有传进来的自定义缓存对象,没有就用默认的imageCache
id<SDImageCache> imageCache;
if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
imageCache = context[SDWebImageContextImageCache];
} else {
imageCache = self.imageCache;
}
// Get the query cache type
// 查看缓存类型,默认是all,如果有传进来的就用传进来的
SDImageCacheType queryCacheType = SDImageCacheTypeAll;
if (context[SDWebImageContextQueryCacheType]) {
queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];
}

②、通过options,判断缓存查找,还是下载

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
// Check whether we should query cache
// SD_OPTIONS_CONTAINS为与运算,当options为SDWebImageFromLoaderOnly时为true(或者全是1也可以)
// 注意这里是取反,也就是设置了SDWebImageFromLoaderOnly后是不走缓存,直接下载
BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
if (shouldQueryCache) {
// 拿到缓存的key
NSString *key = [self cacheKeyForURL:url context:context];
@weakify(operation);
// 缓存查询,并返回缓存任务
operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation);
// 如果没有执行的任务,或者任务被取消了
if (!operation || operation.isCancelled) {
// Image combined operation cancelled by user
// 抛出错误
[self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
// 安全的移除任务
[self safelyRemoveOperationFromRunning:operation];
return;
} else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
// 没拿到缓存图片,且图片有经过Transformer转化,那就去查询原始图片缓存
//有机会去查询原始缓存
// Have a chance to query original cache instead of downloading
[self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
return;
}

// Continue download process
// 走下载流程
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
} else {// 走下载流程
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}

这里解释一下key是怎么拿(SDWebImage的缓存key是怎么样的),逻辑在这个方法里面cacheKeyForURL,代码就不贴出来了,说一下大概逻辑。

a、SDWebImagecontext里面有个SDWebImageContextCacheKeyFilter,里面存储的是用来存放自定义key逻辑的协议,通过重写cacheKeyForURL自定义key,如果没有传SDWebImageContextCacheKeyFilter进来则使用url的string值。
b、然后通过context里面的SDWebImageContextImageThumbnailPixelSizeSDWebImageContextImagePreserveAspectRatioSDWebImageContextImageTransformer这3个里面是否有值,如果有值就加上上面的key进行拼接,没值就直接用上面的key

查到缓存后就是回调了,回调看代码注释,问题应该不大,要注意的是它也走了callDownloadProcessForOperation这个方法,因为optionsSDWebImageRefreshCached的情况下,也是要走下载的,所以索性将找到的缓存,放到callDownloadProcessForOperation处理,而不是直接回调。

3.SDImageCache

缓存获取数据,主要是通过key缓存,cacheType判断缓存方式,options进行缓存拓展。主要内容如下:

a. 对cacheOptions类型进行筛选

b. 进入queryCacheOperationForKey方法,对具体缓存方式进行划分,其中包括内存缓存,磁盘缓存。然后又在各自缓存下面进行了详细划分

a、内存查找

为啥说缓存的查找是先内存呢,看下面这段代码:

1
2
3
4
5
6
// 先检查内存里面的缓存
UIImage *image;
if (queryCacheType != SDImageCacheTypeDisk) {
// 通过key去内存表拿到value(image),默认实现表为weak(NSMapTable<KeyType, ObjectType> *weakCache)
image = [self imageFromMemoryCacheForKey:key];
}

一般的queryCacheType默认为SDImageCacheTypeAll,在没有自定义queryCacheTypeSDImageCacheTypeDisk的情况下都是先走的memoryCache

imageFromMemoryCacheForKey这个方法里面的查找方式也很简单,通过封装SDMemoryCache协议,并用NSMapTable<KeyType, ObjectType>类型存储的值去取到对应的image

1
2
3
4
5
6
7
8
9
// 是否只能在内存缓存查询,是的话直接将找到的image回调,注意这里是不查询data,所以data为nil
// 默认情况下,在内存缓存找到image是不查询data了,但是设置了SDImageCacheQueryMemoryData,就会强制去查询
BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}

正常情况下,如果照片找到了,就直接回调block。但是在queryCacheType不指定为SDImageCacheTypeMemory,且optionsSDImageCacheQueryMemoryData的时候那就得继续往下,去磁盘查找。

b、磁盘查找

磁盘查找分为同步跟异步,默认情况是异步查找,以下情况是同步查找

1
2
3
4
5
6
7
/*
*两种情况,需要同步查询磁盘
*1、在内存缓存中存在图片,但是指定标签为SDImageCacheQueryMemoryDataSync
*2、在内存缓存找不到图片,且指定标签为SDImageCacheQueryDiskDataSync
*/
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));

磁盘的查找有2种方式

一种是通过SDDiskCache协议内部封装的方法,通过key获取path,然后拿到data
一种是通过additionalCachePathBlock拿到保存的path,然后拿到data
如果是通过磁盘拿到的image,还会将image保存到内存,以便下次查询。

这里说个小细节,磁盘查询的过程是用了@autoreleasepool包了起来,为了防止多张照片查询,引起的内存飙升。