SDWebImage源码解析<一>
之前就想阅读SDWebImage,但由于工作太忙没时间,最近终于能抽出时间好好看看SDWebImage源码了,受益匪浅。现在就把自己的理解分享给大家。
首先,我对框架内的几个类简单解释一下:
-
UIImageView+WebCache
通常在为一个UIImageView
设置一张网络图片并让SD自动缓存起来就会使用这个分类下的- (void)sd_setImageWithURL:(NSURL *)url;
方法 -
UIView+WebCacheOperation
为方便找到和管理视图的正在进行的一些操作,SD将每一个视图的实例和它正在进行的操作(下载和缓存的组合操作)绑定起来,实现操作和视图的一一对应关系,以便可以随时拿到视图正在进行的操作,控制其取消等。 -
SDWebImageManager
在实际的运用中,我们并不直接使用SDWebImageDownloader
类及SDImageCache
类来执行图片的下载及缓存。为了方便用户的使用,SDWebImage
提供了SDWebImageManager
对象来管理图片的下载与缓存。而且我们经常用到的诸如UIImageView+WebCache
等控件的分类都是基于SDWebImageManager
对象的。该对象将一个下载器和一个图片缓存绑定在一起,并对外提供两个只读属性来获取它们 -
SDWebImageDownloaderOptions
它是一个NSOperation
的子类,同时遵守了<SDWebImageOperation>
协议(其实这个协议只声明了一个方法cancel用于取消操作)。这个操作负责管理下载的选项,进行网络访问时的request,设置网络处理质询的凭据,进行网络连接接收数据,管理网络访问的response和是否解压的选项等。总之,它的任务就是网络访问配置、进行网络访问以及处理数据。 -
SDWebImageDownloader
如果说SDWebImageDownloaderOperation
实现了下载图片的细节,那么SDWebImageDownloader
就负责控制operation来触发下载任务,并管理所有的下载任务,包括改变他们的状态,SDWebImageDownloader
是进行下载控制的接口,在实际应用中,我们几乎很少直接使用SDWebImageDownloaderOperation
,而几乎都是使用SDWebImageDownloader
来进行下载任务 -
SDImageCache SDImageCache
类是一个功能无比强大的缓存管理器。它可以实现内存和磁盘缓存功能的实现和管理,主要包括以下几个方面:
1.对内存或磁盘缓存进行单个图片增、删、查等操作
2.还提供使用命名空间的方式对图片分类管理,管理应用启动前的放入app中的预缓存图
3.同时还可以对所有的缓存整体操作,如查询总缓存文件个数,查询总缓存大小,一次性清理内存缓存,一次性清理磁盘缓存。
SDWebImage
最常见的使用场景不必我多说了,直接进入源码:
来到UIImageView+WebCache
查看具体的实现:
/**
* 根据 url、placeholder 与 custom options 为 imageview 设置 image
*
* 下载是异步的,并且被缓存的
*
* @param url 网络图片的 url 地址
* @param placeholder 用于预显示的图片
* @param options 一些定制化选项
* @param progressBlock 下载时的 Block,其定义为:typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
* @param completedBlock 下载完成时的 Block,其定义为:typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
*/
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
// 移除UIImageView当前绑定的操作。这一句非常关键,当在TableView的cell包含了的UIImageView被重用时,首先调用这一行代码,保证这个ImageView的下载和缓存组合操作都被取消。如果上次赋值的图片正在下载,则下载不再进行;下载完成了,但还没有执行到调用回调(回调包含wself.image = image) ,由于操作被取消,因而不会显示和重用的cell相同的图片;以上两种情况只有在网速极慢和手机处理速度极慢的情况下才会发生,实际上发生的概率非常小,大多数是这种情况:操作已经进行到下载完成了,这次使用的cell是一个重用的cell,而且保留着imageView的image,对于这种情况SD会用下面的设置占位图的语句,将image暂时设置为占位图,如果占位图为空,就意味着先暂时清空image。这行代码的内部实现下面会详细讲到。
[self sd_cancelCurrentImageLoad];
// 将传入的url与self绑定,用的是runtime中的Objective-C Associated Objects,后面文章会专门写,如果现在不懂,请自行百度。
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 如果没有设置延迟加载占位图,设置image为占位图
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
if (url) {
// 检查是否通过`setShowActivityIndicatorView:`方法设置了显示正在加载指示器。如果设置了,使用`addActivityIndicator`方法向self添加指示器
if ([self showActivityIndicatorView]) {
[self addActivityIndicator];
}
__weak __typeof(self) wself = self;
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)
{ // 如果设置了禁止自动设置image选项,则不会执行`wself.image = image;`,而是直接执行完成回调,有用户自己决定如何处理。
completedBlock(image, error, cacheType, url);
return;
}
else if (image) {
// 设置image
wself.image = image;
[wself setNeedsLayout];
} else { // image为空,并且设置了延迟设置占位图,会将占位图设置为最终的image
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
// 为UIImageView绑定新的操作
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else { // 判断url不存在,移除加载指示器,执行完成回调,传递错误信息。
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);
}
});
}
}
UIView+WebCacheOperation
现在我们来看看第一行代码[self sd_cancelCurrentImageLoad]
里面的乾坤:
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// 取消正在进行的下载队列
NSMutableDictionary *operationDictionary = [self operationDictionary];
id operations = [operationDictionary objectForKey:key];
if (operations) {
//这个数组是请求gif图时传进来的
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];
}
}
框架中的所有操作实际上都是通过一个 operationDictionary(具体查看 UIView+WebCacheOperation)来管理的,而这个 Dictionary 实际上是通过动态的方式添加到 UIView 上的一个属性
写到这里,不得不提- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key
方法
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
//先取消索引为key的operation的操作
[self sd_cancelImageLoadOperationWithKey:key];
NSMutableDictionary *operationDictionary = [self operationDictionary];
//重新设置key对应的operation
[operationDictionary setObject:operation forKey:key];
}
当执行 sd_setImageWithURL:
函数时,首先会 cancel 掉 operationDictionary 中已经存在的 operation,并重新创建一个新的 SDWebImageCombinedOperation 对象来获取 image,该 operation 会被存入 operationDictionary 中。
这样来保证每个 UIImageView 对象中永远只存在一个 operation,当前只允许一个图片网络请求,该 operation 负责从缓存中获取 image 或者是重新下载 image。
SDWebImageCombinedOperation 的 cancel 操作同时会 cacel 掉缓存查询的 operation 以及 downloader 的 operation
SDWebImageManager
这个类中还有两个代理方法不得不说
// 控制当图片在缓存中没有找到时,应该下载哪个图片
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
// 允许在图片已经被下载完成且被缓存到磁盘或内存前立即转换
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;
为了能够更好地理解这个方法的实现,再次必须强调一个SDWebImageOptions
选项值SDWebImageRefreshCached
,如果设置了这个值:
即使SD对图片缓存了,也期望HTTP响应cache control,并在需要的情况下从远程刷新图片。也就是说如果在磁盘中找到了这张图片,但设置了这个选项,仍然需要进行网络请求,查看服务器端的这张图片有没有被改变,并决定进行下载,然后使用新的图片,同时完成新的缓存。
但是这个下载并不是自己决定要不要进行的,还需要如果代理通过方法[self.delegate imageManager:self shouldDownloadImageForURL:url]
返回NO,那就是代理要求这个url对应的图片不需要下载。这种情况下就不再下载,而是使用在缓存中查找到的图片
/**
* 如果在缓存中则直接返回,否则根据所给的 URL 下载图片
*
* @param url 网络图片的 url 地址
* @param options 一些定制化选项
* @param progressBlock 下载时的 Block,其定义为:typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
* @param completedBlock 下载完成时的 Block,其定义为:typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
* @return 返回 SDWebImageOperation 的实例
*/
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
/**
* 前面省略 n 行,主要作了如下处理:
* 1. 判断 url 的合法性
* 2. 创建 SDWebImageCombinedOperation 对象
* 3. 查看 url 是否是之前下载失败过的
* 4. 如果 url 为 nil,或者在不可重试的情况下是一个下载失败过的 url,则直接返回操作对象并调用完成回调
*/
// 创建一个组合操作,主要用于将查询缓存、下载操作、进行缓存等工作联系在一起
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
// 检查这个url是否在失败列表中,也就是是否曾经下载失败过。
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
// 如果没有设置失败重试选项(SDWebImageRetryFailed),并且是一个失败过的url,则直接执行完成回调。
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;
}
// self.runningOperations是一个数组,元素为正在进行的组合操作
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
// 根据 URL 生成对应的 key,没有特殊处理为 [url absoluteString];
NSString *key = [self cacheKeyForURL:url];
// 去缓存中查找图片(参见 SDImageCache)
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType)
{
// 如果操作被取消了,从正在进行的操作列表中将它移出.
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
// 条件A:在缓存中没有找到图片 或者 options选项包含SDWebImageRefreshCached (这两种情况都需要进行请求网络图片的) , 条件B:代理允许下载
/*
条件B的实现为:代理不能响应imageManager:shouldDownloadImageForURL:方法 或者 能响应且方法返回值为YES。也就是说没有实现这个方法就是允许的,而如果实现了的话,返回为YES才是允许的。
*/
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
// 分支一:缓存中找到了图片 且 options选项包含SDWebImageRefreshCached, 先在主线程完成一次回调,使用的是缓存中找到的图片
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
// 如果在缓存中找到了image但是设置了SDWebImageRefreshCached选项,传递缓存的image,同时尝试重新下载它来让NSURLCache有机会接收服务器端的更新
completedBlock(image, nil, cacheType, YES, url);
});
}
// 如果没在缓存中找到image 或者 设置了需要请求服务器刷新的选项,则仍需要下载.
SDWebImageDownloaderOptions downloaderOptions = 0;
/.../
if (image && options & SDWebImageRefreshCached) {
// 如果image已经被缓存但是设置了需要请求服务器刷新的选项,强制关闭渐进式选项
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// 如果image已经被缓存但是设置了需要请求服务器刷新的选项,忽略从NSURLCache读取的image
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
/* 下载选项设置 */
// 使用 imageDownloader 开启网络下载
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) {
// 如果操作取消了,不做任何事情
//如果调用completedBlock, 这个block会和另一个completedBlock争夺同一个对象。因此,如果这个block后被调用,会覆盖新的数据。
}
else if (error) {
// 进行完成回调
// 将url添加到失败列表中
// ...
}
else {
// 如果设置了失败重试,将url从失败列表中去掉
// ...
// 设置了SDWebImageRefreshCached选项 且 缓存中找到了image 且 没有下载成功
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
// 这个分支的进入的条件:既没有error、downloadedImage又是nil,这种回调在SDWebImageDownloaderOperation进行下载的时候只有读取了URL的缓存才会发生,即下载正常完成,但是没有数据。
// 图片刷新遇到了NSSURLCache中有缓存的状况,不调用完成回调。
// Image refresh hit the NSURLCache cache, do not call the completion block
}
// 下载成功 且 设置了需要变形Image的选项 且变形的代理方法已经实现
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
/*
全局队列异步执行:
1.调用代理方法完成形变
2.进行缓存
3.主线程执行完成回调
*/
// ...
}
else {
/*
1.进行缓存
2.主线程执行完成回调
*/
// ...
}
}
if (finished) {
// 从正在进行的操作列表中移除这个组合操作
// ...
}
}];
// 设置组合操作的取消回调
// ...
}
// 处理其他情况
// 情况一:在缓存中找到图片(代理不允许下载 或者 没有设置SDWebImageRefreshCached选项 满足至少一项)
else if (image) {
// 使用image执行完成回调
// 从正在进行的操作列表中移除组合操作
// ...
}
// 情况二:在缓存中没找到图片 且 代理不允许下载
else {
// 执行完成回调
// 从正在进行的操作列表中移除组合操作
// ...
}
}];
return operation;
}
这个方法主要完成了这些工作:
1.创建一个组合Operation,是一个SDWebImageCombinedOperation对象,这个对象负责对下载operation创建和管理,同时有缓存功能,是对下载和缓存两个过程的组合。
2.先去寻找这张图片 内存缓存和磁盘缓存,这两个功能在self.imageCache的queryDiskCacheForKey: done:方法中完成,这个方法的返回值既是一个缓存operation,最终被赋给上面的Operation的cacheOperation属性。
在查找缓存的完成回调中的代码是重点:它会根据是否设置了SDWebImageRefreshCached选项和代理是否支持下载决定是否要进行下载,并对下载过程中遇到NSURLCache的情况做处理,还有下载失败的处理以及下载之后进行缓存,然后查看是否设置了形变选项并调用代理的形变方法进行对图片形变处理。
3.将上面的下载方法返回的操作命名为subOperation,并在组合操作operation的cancelBlock代码块中添加对subOperation的cancel方法的调用。
4.处理请他的情况:代理不允许下载但是找到缓存的情况,没有找到缓存且代理不允许下载的情况
5.这个方法最终返回的是operation也就是一个SDWebImageCombinedOperation对象,而不是下载操作。
还需要注意区分:
本方法,也就是SDWebImageManager对象的- (id <SDWebImageOperation>)downloadImageWithURL:options:progress:completed:返回的是SDWebImageCombinedOperation对象
SDImageCache对象的- (NSOperation *)queryDiskCacheForKey: done:返回的是一个空的NSOperation对象(用于取消磁盘缓存查询和内存缓存备份)
SDWebImageDownloader对象的- (id <SDWebImageOperation>)downloadImageWithURL:options: progress:completed:返回的是一个已经放到队列中执行的下载操作,默认是SDWebImageDownloaderOperation对象
更详细的注解可参见:SDWebImage源码解析之SDWebImageManager的注解
要点
-
在 SDWebImageManager 中管理了一个 failedURLs 的 NSMutableSet,里面下载失败的 url 会被存储下来。同时,可以通过 SDWebImageRetryFailed 来强制继续重试下载
-
查找缓存,若缓存中没有 image 则通过 SDWebImageDownloader 来进行下载,下载完成后通过 SDImageCache 进行缓存,会同时缓存到 memCache 和 diskCache 中
下一篇我会详细讲解SD的下载和缓存 SDWebImage源码解析<二>
参考:
通读SDWebImage①--总体梳理、下载和缓存
SDWebImage 源码阅读笔记(一)