- Asynchronous image downloader with cache support as a UIImageView category
*一个异步的图片下载与缓存的UIImageViewCategory
对于它究竟是如何工作的,相信大家应该或多或少都已经有所了解。但是它内部是怎么实现,又有那些细节,我却一直犯懒没有真正的好好去研究过,最近不是很忙,于是我就仔细的研究了一下它的实现细节,这里我源码的版本为4.0.0,也就是当前最新的版本。
**在这里我推荐大家去github
下载对应的源码,一边看blog一边看对应的源码,最好再做上自己的注释,这样会看的更快,且做做笔记会加深自己的映像,SDWebImage:github
地址:https://github.com/rs/SDWebImage **
好了,废话不多说,直接来看代码吧。
下面的代码是我们经常使用的SDWebImage
的方法之一,给imageView
传入对应的图片url和占位图片,它就帮我们实现了图片的所有操作。
点进它的具体实现,可以看到它是一个UIImageView
的分类,分类的调用方法如下,我已经给对应的参数做出了对应的翻译:
/**
* 使用一个url,占位图片和自定义选项来设置imageView
* 下载是异步且缓存的
* url 图像的url
* placeholder 占位图片,初始化时被设置,在请求结束时消失
* options 在下载图片的时候使用的选项,看SDWebImageOptions有哪些可能的值
* progressBlock 当图像下载时候调用的block,这个block在一个后台队列执行
* completedBlock 当操作结束时调用的block,这个block没有返回值,把请求到的图像作为第一个参数,如果发生错误的话,第一个参数为空,第二个参数会包含一个NSError对象,第三个参数是一个bool值,指是从本地缓存还是从网络来重新获取图像,第四个参数是图片原始的url
*/
- (void)sd_setImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDExternalCompletionBlock)completedBlock {
[self sd_internalSetImageWithURL:url
placeholderImage:placeholder
options:options
operationKey:nil
setImageBlock:nil
progress:progressBlock
completed:completedBlock];
}
相信大家对上面的参数,并不陌生,即使曾经没研究过,看到对应的名称和注释也能大概猜出它们的作用,这里唯一不太了解的应该是options
的含义了。
options
是一个枚举,下面是options
对应的值,作用已经添加在注释中了
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
/**
* 默认情况下,如果一个url在下载的时候失败了,那么这个url会被加入黑名单并且library不会尝试再次下载,这个flag会阻止library把失败的url加入黑名单(简单来说如果选择了这个flag,那么即使某个url下载失败了,sdwebimage还是会尝试再次下载他.)
*/
SDWebImageRetryFailed = 1 << 0,
/**
* UI交互期间下载
* 导致延迟下载在UIScrollView减速的时候,(也就是你滑动的时候scrollview不下载,你手从屏幕上移走,scrollview开始减速的时候才会开始下载图片)
*/
SDWebImageLowPriority = 1 << 1,
/**
* 只进行内存缓存,不进行磁盘缓存
*/
SDWebImageCacheMemoryOnly = 1 << 2,
/**
* 这个标志可以渐进式下载,显示的图像是逐步在下载(就像你用浏览器浏览网页的时候那种图片下载,一截一截的显示
*/
SDWebImageProgressiveDownload = 1 << 3,
/**
* 即使图像缓存,也要遵守HTTP响应缓存控制,如果需要,可以从远程位置刷新图像
* 磁盘缓存将由NSURLCache而不是SDWebImage处理,导致轻微的性能降低。
* 这个选项帮助处理在同样的网络请求地址下图片的改变
* 如果刷新缓存的图像,完成的block会在使用缓存图像的时候调用,还会在最后的图像被调用
* 当你不能使你的URL静态与嵌入式缓存
*/
SDWebImageRefreshCached = 1 << 4,
/**
* 在iOS4以上,如果app进入后台,也保持下载图像,这个需要取得用户权限
* 如果后台任务过期,操作将被取消
*/
SDWebImageContinueInBackground = 1 << 5,
/**
* 操作cookies存储在NSHTTPCookieStore通过设置NSMutableURLRequest.HTTPShouldHandleCookies = YES
*/
SDWebImageHandleCookies = 1 << 6,
/**
* 允许使用无效的SSL证书
* 用户测试,生成情况下小心使用
*/
SDWebImageAllowInvalidSSLCertificates = 1 << 7,
/**
* 优先下载
*/
SDWebImageHighPriority = 1 << 8,
/**
* 在加载图片时加载占位图。 此标志将延迟加载占位符图像,直到图像完成加载。
*/
SDWebImageDelayPlaceholder = 1 << 9,
/**
* 我们通常不调用transformDownloadedImage代理方法在动画图像上,大多数情况下会对图像进行耗损
* 无论什么情况下都使用
*/
SDWebImageTransformAnimatedImage = 1 << 10,
/**
* 图片在下载后被加载到imageView。但是在一些情况下,我们想要设置一下图片(引用一个滤镜或者加入透入动画)
* 使用这个来手动的设置图片在下载图片成功后
*/
SDWebImageAvoidAutoSetImage = 1 << 11,
/**
* 图像将根据其原始大小进行解码。 在iOS上,此标记会将图片缩小到与设备的受限内存兼容的大小。
*/
SDWebImageScaleDownLargeImages = 1 << 12
};
看完上面的枚举值,大家应该还是不知道有什么作用,没关系,接着往下看。
继续往后可以看到,最终它真正调用的是UIView+WebCache.h
的方法,这里就是要详细讲解的第一个方法,在下面的代码中我已经贴了一些注释来方便讲解:
- (void)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 {
// 获取可用的operationKey
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
// 取消该key对应的任务
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
// 给该视图的实例对象设置一个属性
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 如果options不为SDWebImageDelayPlaceholder,那么先把placeholder设置到该视图上
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
if (url) {
// check if activityView is enabled or not
// 如果有url,且设置显示ActivityIndicator,那么显示
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
__weak __typeof(self)wself = self;
// ⚠️这里的operation不是继承自NSOperation的,我们可以把它看做一个关联视图操作的对象,我们称它为op对象
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;
// 图像下载成功后,移除ActivityIndicator
[sself sd_removeActivityIndicator];
if (!sself) {
return;
}
dispatch_main_async_safe(^{
if (!sself) {
return;
}
// 如果有image且options为SDWebImageAvoidAutoSetImage且有completedBlock
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
// 在这里获取到图片,且做一些加工的操作
completedBlock(image, error, cacheType, url);
return;
} else if (image) {
// 如果有image,设置视图的图像
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
// 标记设为需要布局
[sself sd_setNeedsLayout];
} else {
// image已经尝试获取过了,但是没有从网络端获取到
// 如果options为SDWebImageDelayPlaceholder,当前视图设置为占位图片
// 标记设为需要布局
if ((options & SDWebImageDelayPlaceholder)) {
[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}
}
// 有completedBlock且下载finished为yes,将需要的参数传出去
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
// 将现在的op对象加到对应的视图实例中
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
// 如果url为空,抛出错误
dispatch_main_async_safe(^{
[self sd_removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
下面我们将一步步来解释这些代码的含义:
1.首先先获取validOperationKey
,如果为空,那么就获取到当前类的名称,查看UIImageView+WebCache.h
对应的传入参数,可以发现UIImageView
传入的对应validOperationKey
为nil
,也就是说默认情况下,如果我们不直接给validOperationKey
赋值,它就为nil
,那么这里获得的validOperationKey
一般也就是对应类的class
,也就是说如果是UIImageView
调用这个方法,那么对应的validOperationKey
也就是UIImageView
。
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
2.取消该key
对应的任务
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
什么情况,怎么还没开始做事情就开始取消了?
在这里我们做一个标记,一会来解释
⚠️标记1:- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key
是什么意思,为什么一来就取消,有什么作用?
3.给该视图的实例对象设置一个属性,这里的知识是使用了runtime
,如果对runtime
不够了解的,可以参看资料:让你快速上手Runtime。
通俗点讲:这里的作用就是给UIView
的实例添加了@property (nonatomic, strong) NSString *url;
,只是这个属性的获取方式是通过key/value
的方式来获得的,url
这个value
对应的key
为&imageURLKey
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
4.接下来就是设置placeholder
,如果不想让SDWebImage
来帮你设置占位图片,就给它传入setImageBlock
来自定义设置占位图片。
// 如果options不为SDWebImageDelayPlaceholder,那么先把placeholder设置到该视图上
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
[self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
});
}
这里有两个需要讲解的
-
options & SDWebImageDelayPlaceholder
: &是按位与
举个例子:a & b a=1 b=2 a== 0000 0001(二进制) b== 0000 0010(二进制) a & b = 0000 0000(二进制)
放在这里就是,如果options
中包含SDWebImageDelayPlaceholder
,那么就不设置占位图。 -
dispatch_main_async_safe
:这是一个定义的宏
如果当前是主进程,就直接执行block,否则把block放到主进程运行。为什么要判断是否是主进程?因为iOS上任何UI的操作都在主线程上执行,所以主进程还有一个名字,叫做“UI进程”。
ifndef dispatch_main_async_safe
define dispatch_main_async_safe(block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
endif
5.下面的操作是根据url来加载网络图片,分为有`url`有值和`url`无值的情况
```obj
if (url) {
// check if activityView is enabled or not
if ([self sd_showActivityIndicatorView]) {
[self sd_addActivityIndicator];
}
__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;
} else if (image) {
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
} else {
if ((options & SDWebImageDelayPlaceholder)) {
[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
dispatch_main_async_safe(^{
[self sd_removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
先来分析
url
无值的情况,也就是上面代码中的else
,可以很清晰的看到先会调用[self sd_removeActivityIndicator];
,根据名字我们大概能猜到是移除一个ActivityIndicator
,然后会使用完成的block
在主线程抛出一个NSError
对象。-
现在来看
url
有值的情况,首先// 如果有url,且设置显示ActivityIndicator,那么显示 if ([self sd_showActivityIndicatorView]) { [self sd_addActivityIndicator]; }
然后通过
SDWebImageManager
的单例对象调用下面的方法,返回了一个名为operation
的id
类型的对象- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock;
先不看这个方法的实现,先猜一猜这个方法是做什么的?
我想你肯定已经猜到了,这个方法就是下载图片且给UIImageView
设置图片的方法
现在先来看看这个方法完成的block中的代码:
__strong __typeof (wself) sself = wself;
// 图像下载成功后,移除ActivityIndicator
[sself sd_removeActivityIndicator];
// 如果self为nil,直接返回
if (!sself) {
return;
}
然后如果获取到图片,options
中包含SDWebImageAvoidAutoSetImage
,且完成的block
不为空的情况下,直接调用完成block
返回
// 如果有image且options为SDWebImageAvoidAutoSetImage且有completedBlock
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
// 在这里获取到图片,且做一些加工的操作
completedBlock(image, error, cacheType, url);
return;
}
如果没有获取到options
为SDWebImageAvoidAutoSetImage
,但是获取到了image
,直接设置对应视图的image
else if (image) {
// 如果有image,设置视图的图像
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
// 标记设为需要布局
[sself sd_setNeedsLayout];
}
然后就是当image
没有获取到的时候的操作,如果之前设置的options
有SDWebImageDelayPlaceholder
(也就是延迟加载占位图),那么现在也应该把占位图设置上了
else {
// image已经尝试获取过了,但是没有从网络端获取到
// 如果options为SDWebImageDelayPlaceholder,当前视图设置为占位图片
if ((options & SDWebImageDelayPlaceholder)) {
[sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
}
最后,在所有的判断结束以后,通过completedBlock
将对应的参数传递出去
// 有completedBlock且下载finished为yes,将需要的参数传出去
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
在url不为nil的逻辑代码的最后,将前面生成的operation
和最开始获取到的validOperationKey
设置到对应的视图,也就是下面的代码!!!
在这里我们再做一个标记,下面来解释
⚠️标记2:- (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key
又是什么意思,和上面的标记1有什么关系?
// 将现在的op对象加到对应的视图实例中
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
上面对对应的逻辑进行大概的梳理,大家应该学习到了一些,但是有些地方肯定还是不清楚,所以看下面吧
下面是解决问题的时间
第一个问题
- 首先看到⚠️标记1和上面的⚠️标记2
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
在所有操作刚开始执行的时候,视图就执行了个取消的操作,最后又给视图增加了一个operation
,这到底是怎么回事?
1.根据经验,如果要给一个UIImageView
设置image
,那么肯定要获取到对应的image
,如果这是一个网络图片,那么肯定是要将这个图片下载,然后下载好了,再将图片设置到对应的UIImageView
,相信大家对这个逻辑是没有异议的。
2.现在下载图片对应的操作就是id <SDWebImageOperation> operation
来执行,一开始的取消操作就是取消了这样一个任务
注意:这里的operation
可不是继承自NSOperation
的对象,而是一个继承自NSObject的对象,你可以将它看做一个操作图片更新的对象
3.看一下- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key
对应的实现,首先通过[self operationDictionary]
获取到存有operation
的字典(这里的字典也是通过runtime
动态来添加的),然后通过对应的key
取出对应的operation
,调用cancel
来取消对应的操作,然后通过key
移除对应的operation
。
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
// Cancel in progress downloader from queue
// operation的字典
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];
}
}
4.接着看一下对应的设置方法,设置方法中先调用了sd_cancelImageLoadOperationWithKey
,然后再将对应的operation
添加到了字典中
- (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key {
if (key) {
[self sd_cancelImageLoadOperationWithKey:key];
if (operation) {
SDOperationsDictionary *operationDictionary = [self operationDictionary];
operationDictionary[key] = operation;
}
}
}
下面我举个例子来讲一下这么做的作用:
在常用的tableView
中cell
上有图片是再常见不过的了,如下所示的这种cell
- 在我们使用
SDWebImage
给上面的cell
中的imageview
设置网络图片的时候,图片的下载是异步的,那么如果现在给当前cell设置的为cell.imageview
为a.png
,随着tableView
的滑动,这个cell
会被复用,复用后现在cell.imageview
为b.png
,这里的a.png
和b.png
都是从网络上异步下载的,不是本地的资源图片 - 一开始
cell
的index
为1,image
为a
,复用以后cell
的index
为6,image
为b
,按道理来说图片应该先为a
,然后为b
,但是a
很大,b
很小,b
都已经下载好了,a
还没有下载好,当滑动到显示index
为6的cell
的时候,cell
的图片先显示的b
,因为b
已经下载好了,过了一会,a
也下载好了
那么神奇的事情发生了,index
为6的cell
中的图片a
把b
覆盖了,应该显示b
的变成显示a
了 - 整个数据都乱了,这实在太可怕了
如果上面我举的例子没看懂,请反复多看几遍!!
好,我现在认为你已经看懂了~
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
上面👆的两个方法就是为了防止这种情况的发生,因此先取消对应的图片操作,再重新添加,刚开始先通过key
获取operation
,如果有operation
对象---->取消。当重新产生一个operation
对象以后,还是看对应的字典中有没有,有----> 取消(因为现在还没将新产生的operation
添加到字典中),没有--->operationDictionary[key] = operation;
,将这个operation
放到字典中,这样就可以保证一个视图对象只有一个operation
在操作图像
在这里也就是说如果设置了cell
的网络图片为b
,那么就取消掉之前的a
的相关操作,这样就不会出现显示错乱的问题了。
作者的想法真的是很聪明呀!
第二个问题
在SDWebImage中常常可以看到options & SDWebImageRefreshCached
这种写法,查看SDWebImageRefreshCached
的定义可以看到SDWebImageRefreshCached = 1 << 4
。
例如:a=1 b=2 a== 0000 0001(二进制) b== 0000 0010(二进制) a & b = 0000 0000 (二进制) 十进制为0
也就是说SDWebImageRefreshCached
是将1左移4位的一个值,二进制表示为00010000,十进制为16
在接下来的代码中还会看到downloaderOptions | SDWebImageDownloaderLowPriority
,这种写法是按位或,也是位运算的一种:
例如:a=5,b=11; 5 ==0000 0101 (二进制) 10==0000 1011(二进制) a | b== 0000 1111(二进制) 十进制为15
如果想了解更多的相关知识,可以参考这篇博客:按位与,按位或
总结
我用了一张流程图来表示这篇文章的内容,方便大家查看
以上是一些我的个人理解,如果有什么不对的地方也希望大家能够指出,互相学习!
这是SDWebImage源码解析的第一篇,下一篇将会对下面产生
operation
的方法进行分析,欢迎大家关注!
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock