1. 解决上一篇遗留的坑
上一篇中对sd_setImageWithURL函数简单分析了一下,还留了一些坑。不过因为我们现在对这个函数有一个大概框架了,我们就按顺序一个个来解决。
首先是这一句代码:
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
就是给UIImageView当前这个对象添加一个NSString的关联对象url。相当于现在这个图片的url属性绑定到了UIImageView对象上。如果对这个函数有疑问,请移步我的这篇博客。
下面简单的部分我就不说了,直接跳到
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
completedBlock(image, error, cacheType, url);
return;
}
首先是SDWebImageAvoidAutoSetImage,我们看看它的注释
/**
* By default, image is added to the imageView after download. But in some cases, we want to
* have the hand before setting the image (apply a filter or add it with cross-fade animation for instance)
* Use this flag if you want to manually set the image in the completion when success
*/
翻译过来就是说,默认情况下是等image完全从网络端下载完后,就会直接将结果设置到UIImageView。但是有些人想在获取到图片后,对图片做一些处理,比如使用filter去渲染图片或者给图片加个cross-fade animation(淡出动画)显示出来。那你就设置这个选项。然后得手动去处理图片下载完成后的事情。
上面说了要手动处理了,很自然你就会想到,这个手动处理就是compeletedBlock啊!当然,除了有这个枚举选项时需要手动处理,其实只要你自定义了compeletedBlock,都会调用你自定义处理的函数。你说我怎么知道的?你看下面的代码,如果你自定义了下载完成后的处理方式,并且也确实下载完成了(finished为YES),就执行自定义方式:
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
最后还剩下一个情况,就是url不存在的情况(注意上面讲的是if(url){…},下面讲的是在else{…}):
dispatch_main_async_safe(^{
[self removeActivityIndicator];
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
if (completedBlock) {
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
首先自定义一个NSError的对象,表示url为空的错误。然后传给compeletedBlock。
知识点:NSError构造方法errorWithDomain
+ (instancetype)errorWithDomain:(NSString *)domain code:(NSInteger)code userInfo:(nullable NSDictionary *)dict;
其实目前来说,我心中还有两个最大的疑惑,一个就是operation怎么执行的,一个就是如何自定义compeletedBlock。
2. operation执行过程
我们可以看到这里downloadImageWithURL其实是SDWebImageManager的方法
- (id )downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
我们进去该函数实现,快两百行的代码了。好吧,先歇着,我们看看注释(我直接贴出我翻译过后的注释)。
/**
* 如果图片不在缓存中,根据指定的URL下载图片,否则使用缓存中的图片。.
*
* @param url 图片的URL
* @param options 该请求所要遵循的选项。(前面已经介绍了两个)
* @param progressBlock 当图片正在下载时调用该block。
* @param completedBlock 当操作完成后调用该block。
*
* 该参数是必须的。(指的是completedBlock)
*
* 该block没有返回值并且用请求的UIImage作为第一个参数。
* 如果请求出错,那么image参数为nil,而第二参数将包含一个NSError对象。
*
* 第三个参数是'SDImageCacheType'枚举,表明该图片重新获取方式是从本地缓存(硬盘)或者
* 从内存缓存,还是从网络端重新获取image一遍。.
*
* 当options设为SDWebImageProgressiveDownload并且此时图片正在下载,finished将设为NO
* 因此这个block会不停地调用直到图片下载完成,此时才会设置finished为YES.
*
* @return 返回一个遵循SDWebImageOperation协议的NSObject. 应该是一个SDWebImageDownloaderOperation的实例
*/
上面的注释有几个不认识的概念。一个是SDImageCacheType,另一个是options中的SDWebImageProgressiveDownload,还有一个SDWebImageDownloaderOperation。
2.1 SDImageCacheType
typedef NS_ENUM(NSInteger, SDImageCacheType) {
/**
* 该图片无法从SDWebImage的缓存中获取,必须从web端下载。
*/
SDImageCacheTypeNone,
/**
* 图片从硬盘缓存(disk cache)中获取
*/
SDImageCacheTypeDisk,
/**
* 图片从硬盘缓存(disk cache)中获取
*/
SDImageCacheTypeMemory
};
具体缓存实现方式我放在SDWebImage源码阅读(五)了。
2.2 SDWebImageProgressiveDownload
如果在加载图片中设定了该选项,那么图片会随着下载的进度一点点地显示出来。缺省情况下,图片是下载完成后一次显示出来的。
2.3 SDWebImageDownloaderOperation
看到这个类,我内心是愉快的。之前我不是说这个opertion应该和NSOperation有些关系吗?这个类就是NSOperation的子类啊,并且遵循SDWebImageOperation协议。这下SDWebImageDownloaderOperation将NSOperation和SDWebImageOperation联系在了一起,我们可以看下它的声明:
@interface SDWebImageDownloaderOperation : NSOperation
所以不用说,这个类一定是个重头戏。
但是我们搜索SDWebImageDownloaderOperation,发现SDWebImageManager中的downloadImageWithURL函数并没有返回SDWebImageDownloaderOperation。这一点很让人疑惑。不过我们发现SDWebImageDownloaderOperation遵循SDWebImageOperation协议,会不会和downloadImageWithURL的返回值id
我先在所有工程中搜索SDWebImageDownloaderOperation,发现在SDWebImageDownloader中也有一个downloadImageWithURL函数。而且里面就定义了一个opertion,这个opertion就是SDWebImageDownloaderOperation 类型,并且这个函数也是返回operation的。
__block SDWebImageDownloaderOperation *operation;
回到我们的SDWebImageManager中的downloadImageWithURL函数中,搜索downloadImageWithURL,找找看是不是有蛛丝马迹。果然,下面这段代码:
id subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished)
此处的downloadImageWithURL是SDWebImageDownloader的一个方法。
好,但此为止,我们也只是觉得上面那些东西有联系,但是联系并不是很清晰。而且已经有了一个downloadImageWithURL,还要弄一个干什么?
太多问题了,我现在也只能大概猜测到底怎么回事了,在继续探索之前,我们先整理这里面的关系:
上面文字部分,我用标注了两个问题,我们先解决第一个
问题一:函数返回值为id,这是什么返回值?
其实返回的是一个id类型,只是这个id类型一定要遵循里面的protocol,比如id
问题二:已经有了一个downloadImageWithURL,还要弄一个干什么?
这个说实话,我也不是很清楚,只能找这两个函数之间的关联了。其实更准确地说是找SDWebImageManager中downloadImageWithURL中的subOperation(SDWebImageDownloaderOperation)和operation的关系。根据这个思路,我发现了subOperation只在这个函数里面出现了两次。第一次是定义的地方,第二次就是:
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:weakOperation];
}
};
无语,你辛辛苦苦弄了个subOperation,结果就亮了个像,还是cancel,就没了。太没人性了。不过是细想其实是有原因的,我在SDWebImageDownloaderOperation的downloadImageWithURL函数注释中找到了答案:
@return A cancellable SDWebImageOperation
一个cancellable的SDWebImageOperation,是不是和这里只用了cancel对应上了。虽然找到了点联系,不过还是流于表面,这与为什么这么做,这么做的理由还不是很清楚。
我们还是细细分析cancelBlock那段代码。
首先是一个block,operation.cancelBlock有一个对应的setCancelBlock函数:
- (void)setCancelBlock:(SDWebImageNoParamsBlock)cancelBlock {
// 检测self(是一个SDWebImageCombinedOperation类型的operation)是否取消了,如果取消了,就执行对应的cancelBlock函数。
if (self.isCancelled) {
if (cancelBlock) {
cancelBlock();
}
_cancelBlock = nil; //不要忘了置cancelBlock为nil,否则会crash
} else {
_cancelBlock = [cancelBlock copy];
}
}
也就是说operation如果取消了,那么就会执行subOperation的cancel函数。并且从runningOperations中移除该operation,因为是block,为了避免循环引用,所以使用了weakOperation。runningOperations大概从名字也能猜到,用来存储正在运行的operation。既然当前operation被取消了,肯定要从runningOperations移除的嘛!
注意此处的operation的类型是SDWebImageCombinedOperation,具体定义如下:
// 遵循SDWebImageOperation
@interface SDWebImageCombinedOperation : NSObject
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
// 注意SDWebImageCombinedOperation遵循SDWebImageOperation,所以实现了cancel方法
// 在cancel方法中,主要是调用了cancelBlock,这个设计很值得琢磨
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
// 根据cacheType获取到image,这里虽然名字用的是cache,但是如果cache没有获取到图片
// 还是要把image下载下来的。此处只是把通过cache获取image和通过download获取image封装起来
@property (strong, nonatomic) NSOperation *cacheOperation;
@end
还有一个问题就是@synchronized是什么?
知识点:@synchronized
避免多个线程执行同一段代码,主要防止当前operation会被多次remove,从而造成crash。这里括号内的self.runningOperations是用作互斥信号量。 即此时其他线程不能修改self.runningOperations中的属性。
虽然看懂了这段代码,可是后面不知道该看什么了。
所以我还是从头看这段代码(SDWebImageManager中downloadImageWithURL),看能不能找到点什么头绪:
// 如果调用此方法,而没有传completedBlock,那将是无意义的
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
不要将completedBlock参数设为nil,因为这样做是毫无意义的。如果你是想使用downloadImageWithURL来预先获取image,那就应该使用[SDWebImagePrefetcher prefetchURLs],而不是直接调用SDWebImageManager中的downloadImageWithURL函数。
// 使用NSString对象而非NSURL作为url是常见的错误. 因为某些奇怪的原因,Xcode不会报任何类型不匹配的警告,这里允许传NSString对象给URL。
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 防止传了一个NSNull值给NSURL
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
我觉得此处对于细节地处理很值得学习。
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
这个没啥好说的,避免循环引用,使用了weak。
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject: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;
}
这个failedURLs从字面上理解就是一组下载失败的图片URL。所以这段代码也很好理解,就是如果这个图片url无法下载,那就使用completedBlock进行错误处理。那什么情况下算这个图片url无法下载呢?第一种情况是该url为空,另一种情况就是如果是failedUrl也无法下载,但是要避免无法下载就放入failedUrl的情况,就要设置options为SDWebImageRetryFailed。一般默认image无法下载,这个url就会加入黑名单,但是设置了SDWebImageRetryFailed会禁止添加到黑名单,不停重新下载。
如果该url可以下载,那么就添加一个新的operation到runningOperations中。
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
剩下的100多行就是为了生成一个cacheOperation。那它到底是何方神圣?它是一个NSOperation,所以加入NSOperationQueue会自动执行。不过我还是全局搜索cacheOperation,发现它在SDWebImageCombinedOperation中的cancel方法中调用了:
- (void)cancel {
self.cancelled = YES;
if (self.cacheOperation) {
[self.cacheOperation cancel];
self.cacheOperation = nil;
}
if (self.cancelBlock) {
self.cancelBlock();
// TODO: this is a temporary fix to #809.
// Until we can figure the exact cause of the crash, going with the ivar instead of the setter
// self.cancelBlock = nil;
_cancelBlock = nil;
}
}
还记得SDWebImageCombinedOperation遵循SDWebImageOperation协议吗?这就是SDWebImage实现的cancel。而cacheOperation是NSOperation,所以调用自身的cancel。注意是在这才会设置cancelled设为YES。
好,现在回来看这个queryDiskCacheForKey函数。在此之前,先看上面有段代码,用图片的url来获取cache对应的key,也就是说cache中如果已经有了该图片,那就返回该图片在cache中对应的key,你可以根据这个key去cache中获取图片。
NSString *key = [self cacheKeyForURL:url];
获取到key后,你就可以使用queryDiskCacheForKey函数去查找了:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
// 如果doneBlock不存在。那么就return nil。这个处理和downloadImageWithURL的completedBlock很类似
if (!doneBlock) {
return nil;
}
// 如果key为nil,说明cache中没有该image。所以doneBlock中传入SDImageCacheTypeNone,表示cache中没有图片,要从网络重新获取。
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// 如果key不为nil
// 首先在内存cache中查找
UIImage *image = [self imageFromMemoryCacheForKey:key];
// 找到了,就传入SDImageCacheTypeMemory,说是在内存cache中获取的
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
// 否则,说明图片就在磁盘cache中。
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
UIImage *diskImage = [self diskImageForKey:key];
// 如果磁盘中得到了该image,并且还需要缓存到内存中,为了同步最新数据
if (diskImage && self.shouldCacheImagesInMemory) {
// 后面细讲
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
// 传入SDImageCacheTypeDisk,说明是从磁盘中获取的
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
其实这段代码如果不深究的话,也很容易理解的。我直接把说明写成注释在上面了。不过这里我还有个疑问,就是为啥operation要在查找硬盘缓存时,才创建了一个新的operation?这里我谈谈我的想法:因为“图片可以用”这个状态意味着图片必须在内存中了。图片在网络还是在硬盘,其实相对来说并没有本质区别,最后都是要加进内存的。所以这里就有一个加载到内存的过程,需要产生一个NSOperation,也就理所当然会发生cancel。我这也不是胡乱猜的,函数中有一个self.ioQueue。表明这是一个io序列(dispatch_queue_t)。
这下再回到downloadImageWithURL里面剩下的代码,就会很轻松了。因为它无非就是要处理上面那几种cache情况嘛。
我们还是一点点来看done^{}中的代码:
这段代码简单,不解释了,随时判断该operation是否已经cancel了。
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
下面又是一段巨长的代码,我们先看看if中表示什么:
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
// ...
}
我们先看后面那个delegate方法:
/**
* 当image无法在缓存中找到,调用该函数控制该image的下载
*
* @param imageManager 当前的`SDWebImageManager`
* @param imageURL 需要下载的image的URL
*
* @return 返回NO表示当图片缓存未命中,反而阻止图片下载。如果该函数没实现,相当于返回YES。
*/
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;
这里要着重说明下此处return的含义。
注意在if里面最后一组||表达式,使用了短路判断(A||B,只要A为真,就不用判断B了),也就是说,如果delegate没有实现上面那个函数,整个表达式就为真,相当于该函数返回了YES。如果delegate实现了该函数,那就执行该函数,并且判断该函数执行结果。如果函数返回NO,那么整个if表达式都为NO,那么当图片缓存未命中时,图片下载反而被阻止。
目前我看的源码中并没有地方实现了该函数,所以就当if后半段恒为YES。我们主要还是看前面那个||表达式:
(!image || options & SDWebImageRefreshCached)
如果没有缓存到image,或者options中有SDWebImageRefreshCached选项,就执行if语句。现在我们深入看看if判断下的代码到底执行了什么,首先又是一个if语句:
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
// 如果图片在缓存中找到,但是options中有SDWebImageRefreshCached
// 那么就尝试重新下载该图片,这样是NSURLCache有机会从服务器端刷新自身缓存。
completedBlock(image, nil, cacheType, YES, url);
});
}
下面的代码就表示开始要下载图片了。
首先定义了一个SDWebImageDownloaderOptions枚举值downloaderOptions,并根据options来设置downloaderOptions。基本上SDWebImageOptions和SDWebImageDownloaderOptions是一一对应的。只需要注意最后一个选项SDWebImageRefreshCached,这个得先强制关闭ProgressiveDownload方式。那后面的SDWebImageDownloaderIgnoreCachedResponse是什么意思呢?可能会有这样的疑惑,不是已经从imageCache中获取到了image了吗?还要Ignore干啥?这里简单提下,后面会详解:因为SDWebImage有两种缓存方式,一个是SDImageCache,一个就是NSURLCache,所以知道为什么这个选项是Ignore了吧,因为已经从SDImageCache获取了image,就忽略NSURLCache了。
if (image && options & SDWebImageRefreshCached) {
// 相当于downloaderOptions = downloaderOption & ~SDWebImageDownloaderProgressiveDownload);
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// 相当于 downloaderOptions = (downloaderOptions | SDWebImageDownloaderIgnoreCachedResponse);
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
然后生成了一个subOperation,这段代码也很长,我大致看了下SDWebImageDownloader中的downloadImageWithURL函数,感觉终于到了 “真正”下载的代码了。为什么这么说了?因为里面代码大部分都是iOS自带框架底层的代码了。总算到头了。不过这段代码我准备下一篇再看。
直接跳出这个subOperation的赋值语句,来到对应的else if语句:
else if (image) {
// 从缓存中获取到了图片,而且不需要刷新缓存的
// 直接执行completedBlock,其中error置为nil即可。
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
// 执行完后,说明图片获取成功,可以把当前这个operation溢移除了。
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
else {
// 又没有从缓存中获取到图片,shouldDownloadImageForURL又返回NO,不允许下载,悲催!
// 所以completedBlock中image和error均传入nil。
dispatch_main_sync_safe(^{
if (!weakOperation.isCancelled) {
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
}
恩,SDWebImageManager中的downloadImageWithURL函数我们还剩下那个最精彩的SDWebImageDownloader中的downloadImageWithURL函数,留着下一篇阅读。
本文转载polobymulberry-博客园