学习源码时可能太过枯燥,一个不错背景音乐能让心情平静提升专注力(๑•̀ㅂ•́)و✧
推荐歌单:http://music.163.com/#/m/playlist?id=6683129
接着上回,我们还留着downloadImageWithURL
没说,现在就到SDWebImageManager.h
里来看看:
/**
* 如果图片的url不在缓存中则下载,否则使用缓存中的图片.
*
* @param url 图片的url
* @param options 选项
* @param progressBlock 当图片正在下载中调用该block
* @param completedBlock 当任务完成后调用该block.
*
* completedBlock必须提供,不允许为nil.
* typedef void(^SDWebImageCompletionWithFinishedBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL);
* 这个block没有返回值 并且 将请求的UIImage作为第一个参数.
* 如果发生错误,image参数将会为nil,第二个参数会包含NSError
*
* 第三个参数是`SDImageCacheType`枚举用于表示图片是从本地缓存(磁盘)还是内存或是网络中获取的
*
* 当开启了SDWebImageProgressiveDownload选项,并且正在下载图片时,finished会被设置成NO。当图片被完整的下载好后会执行block传入完整的图片以及将之设置为YES
*
* @return 返回一个遵循SDWebImageOperation协议的NSObject. 应该是一个SDWebImageDownloaderOperation的实例
*
*/
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
从描述中可以了解到该方法会帮我们下载图片以及从缓存中取出以前下载过的图片。
同时我们注意到返回参数是id <SDWebImageOperation>
,跟到SDWebImageOperation.h
中看到里面仅仅只有一个方法:
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end
这个是面向协议的做法,想使用该接口的话要遵循接口中提供的方法来用。同时继承了该接口的子类需要对接口的方法实现。
到SDWebImageManager.m
中,我们将downloadImageWithURL
代码拆成几个片段来分析:
// completedBlock 必须要有
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
//有个常见的错误,那就是将NSString传进url中,然而因为奇怪的原因xcode又不会发出警告。所以这里遇到这种情况会做一次转换
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
// 防止传入错误的非NSURL类型造成闪退,这里处理了一下
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}
这里主要是针对常见的错误进行处理,曾经我也将NSString误传进行调用,Xcode还没报错代码也能用,直到我翻开代码一看原来SDWebImage帮我们做了一层转换啊。
继续往下看发现代码中使用到了__block
和__weak
修饰符:
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation; //weak
__block
的作用是在block内想要修改外部一个变量就需要在外头这个变量前加上该修饰符
__weak
的作用是避免在block内出现循环引用
BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
//如果url长度为0或者是未设置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;
}
这段代码大意是:如果未设置SDWebImageRetryFailed
进行下载失败重试的话,遇到进了黑名单的url就直接返回错误,因为不需要重试了嘛。
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation]; //<往runningOperations加入当前的operation
}
这段代码中使用到了@synchronized
用来保证线程安全,即仅只允许一个线程对runningOperations
进行操作,其他线程阻塞。
NSString *key = [self cacheKeyForURL:url]; //将url换算成缓存的key
从名称上可以直接猜到[self cacheKeyForURL:url]
方法的用意是为url创建一个缓存的key,跟进查看我们的猜测对不对:
- (NSString *)cacheKeyForURL:(NSURL *)url {
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
}
else {
return [url absoluteString];
}
}
这个方法简单明了将url转换成key,在用户未设置自定义规则(self.cacheKeyFilter)的时候直接返回url的absoluteString。
接下来轮到queryDiskCacheForKey
这个方法了:
// 从缓存中查询image,先从内存找,找不到再到磁盘中找
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
...
我们跟到SDImageCache.m里查看:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) { //< 没有doneBlock直接返回nil
return nil;
}
if (!key) { //< 没有key则调用doneBlock
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// 先检查内存中是否有缓存
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
NSOperation *operation = [NSOperation new];
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
@autoreleasepool {
//从磁盘中查找是否命中缓存
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) { //< 找到图片以及允许将图片缓存在内存中
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;
}
大致可以从代码中看出些端倪,先使用key从内存中查找图片,找不到的话再去磁盘中查找,然后放到内存中并返回图片以及SDImageCacheType:
typedef NS_ENUM(NSInteger, SDImageCacheType) {
/**
* 该图片还未存入SDWebImage caches(SDWebImage 缓存)中,来自web上下载的。
* The image wasn't available the SDWebImage caches, but was downloaded from the web.
*/
SDImageCacheTypeNone,
/**
* 该图片是从磁盘中获取的
*/
SDImageCacheTypeDisk,
/**
* 该图片是从缓存中获取的
*/
SDImageCacheTypeMemory
};
目前我们只需要了解该方法的作用是用来从缓存中取出图片用的就够了,具体的分析等到后面的章节再说。
我们的重点是接下来的代码,按照惯例将它们拆开来分析:
if (operation.isCancelled) { //< 当前的任务被取消
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation]; //< 从runningOperations中删除掉
}
return;
}
这段代码意图明显,运行至此发现当前的operation已经被取消,那也不用继续执行了,将operation从运行任务列表中删除即可。
接下来进入分支条件,一条是需要从网络下载图片,一条是直接使用缓存,最后一条是无法找到图片且又设置了不允许下载。
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) { //< 如果缓存中找不到图片或者开启了SDWebImageRefreshCached(2选1) 并且 没实现shouldDownloadImageForURL
我们从第一条分支看起:
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
//如果图片在缓存中被找到,但启用了SDWebImageRefreshCached就需要重新从服务器上下载图片使NSURLCache刷新缓存
completedBlock(image, nil, cacheType, YES, url);
});
}
// download if no image or requested to refresh anyway, and download allowed by delegate
SDWebImageDownloaderOptions downloaderOptions = 0;
if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
if (image && options & SDWebImageRefreshCached) { //< 开启了SDWebImageRefreshCached
//强制关闭ProgressiveDownload
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
//忽略从NSURLCache缓存中读取
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
// 将下载的事情交给SDWebImageDownloader来搞定,SDWebImageManager负责管理和调度,实现职责单一
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
这里主要完成了SDWebImageManager
的options到SDWebImageDownloader
的options转换,将下载的事情交给SDWebImageDownloader来搞定。
接下来就是对异常的处理:
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) { //< strongOperation被释放或者已经被取消了
// 什么都不做
// See #699 for more details
// if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
}
else if (error) { //< 有错误
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
}
});
//如果不是以下这些错误,将url加入黑名单
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
这里将url加入黑名单,如未设置下载重试的话,下次请求该图片地址将直接略过
最后是当strongOperation存在且没有错误的情况:
//如果开启了SDWebImageRetryFailed将url从failedURLs中删除
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
//如果开启SDWebImageCacheMemoryOnly,则cacheOnDisk为NO,即不缓存在磁盘上
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
//图片命中NSURLCache的缓存,则不调用completion block
}
// 如果图片(downloadedImage)下载成功并且想transform图片的情况
// 默认情况下,animated image是不允许transform的,得先使用downloadedImage.images判断这个图片是不是animated image,为nil即为静态图
// 但如果打开了SDWebImageTransformAnimatedImage,允许强制transform
// 要transform还需实现transformDownloadedImage这个delegate
else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage]; //< 对比图片是否被转换过
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
}
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
});
}
else {
if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; //< 缓存图片
}
dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
}
这段代码大意是图片下载完成,用户可以对图片进行处理,然后SDWebImage将它存入缓存中。
第二个分支是从缓存中找到了图片,非常简洁:
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(image, nil, cacheType, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
直接调用completedBlock返回,然后再从runningOperations删除
第三条分支为图片不在缓存中并且在delegate中又不允许下载,所以image为nil,error也为nil:
dispatch_main_sync_safe(^{
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation && !weakOperation.isCancelled) {
completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
}
});
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
到此为止SDWebImageManager
中关键的downloadImageWithURL
代码的主流程我们已经大致理解了。