SDWebImage5.0进行了一次架构上的改进,因为转Swift之后,一直没用到SDWebImage,所以也没怎么关注。最近有空刚好学习一下SDWebImage5.11的源码。
一、流程架构图
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、取消之前的任务
/*
*通过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
的代码:
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
if (key) {
// Cancel in progress downloader from queue
SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
id operation;
// 拿到对应的任务operation
@synchronized (self) {
operation = [operationDictionary objectForKey:key];
}
if (operation) {
if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
// 如果存在并遵循SDWebImageOperation代理,则取消任务
[operation cancel];
}
// 最后将任务移除
@synchronized (self) {
[operationDictionary removeObjectForKey:key];
}
}
}
}
b、占位图显示
// 是否需要延迟加载占位图
if (!(options & SDWebImageDelayPlaceholder)) {
// 主线程显示占位图
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
});
}
c、进度条,菊花器处理逻辑
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 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
的事情了。
[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
的一些判断,context
与options
的预处理,内容如下:a. 先判断url的可行性
b. 对
context
,options
进行预处理,并放到result
里面c. 调用
callCacheProcessForOperation
判断是否有缓存,如果有则进入ImageCache
拿到缓存数据,如果没有则进入callDownloadProcessForOperation
方法进一步判断如何下载先看看这些步骤的源码,看完再看
callCacheProcessForOperation
做了些什么
a、判断url的可行性
// 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、对context
,options
进行预处理,并放到result
里面
// 将当前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
进行说明处理的,然后将context
跟options
放入result里面。context的处理源代码就不贴出来了,大概就是对SDWebImageContextImageTransformer
、SDWebImageContextCacheKeyFilter
、SDWebImageContextCacheSerializer
这3个进行一个判断,看是否有自定义的传过来,没有就用默认的。
c、callCacheProcessForOperation
的调用
这里主要是判断要到哪里去取数据,ImageCache,还是去下载,接下来就进入这个方法看一下。
这里主要是判断任务是否该走缓存查询,或者直接下载。如果是缓存查询,就进入
SDImageCache
里面进行缓存查询,且在此处理缓存结果的回调。否则就调用callDownloadProcessForOperation
进入下一步判断。①. 拿到
imageCache
,拿到缓存类型queryCacheType
②. 通过
options
判断,走缓存还是下载。如果走缓存,则调用SDImageCache
里面的queryImageForKey
(开始进入SDImageCache
的逻辑);如果走下载,则调用callDownloadProcessForOperation
开始下载前的一些处理。
①、拿到imageCache
,拿到缓存类型queryCacheType
// Grab the image cache to use
// 查看是否有传进来的自定义缓存对象,没有就用默认的imageCache
id 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
,判断缓存查找,还是下载
// 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、
SDWebImage
的context
里面有个SDWebImageContextCacheKeyFilter
,里面存储的是用来存放自定义key
逻辑的协议
,通过重写cacheKeyForURL
自定义key,如果没有传SDWebImageContextCacheKeyFilter
进来则使用url的string
值。
b、然后通过context
里面的SDWebImageContextImageThumbnailPixelSize
、SDWebImageContextImagePreserveAspectRatio
和SDWebImageContextImageTransformer
这3个里面是否有值,如果有值就加上上面的key
进行拼接,没值就直接用上面的key
。
查到缓存后就是回调了,回调看代码注释,问题应该不大,要注意的是它也走了callDownloadProcessForOperation
这个方法,因为options
为SDWebImageRefreshCached
的情况下,也是要走下载的,所以索性将找到的缓存,放到callDownloadProcessForOperation
处理,而不是直接回调。
接下来看一下SDImageCache
模块,看看SDWebImage
是如何查询缓存的。