一行行看iOS SDWebImage源碼(一)
typedef NS_ENUM(NSInteger, SDImageCacheType) {
/**
* The image wasn't available the SDWebImage caches, but was downloaded from the web.
該圖像是不可用的SDWebImage緩存,但是從網絡下載的.
*/
SDImageCacheTypeNone,
/**
* The image was obtained from the disk cache.
圖像從磁盤高速緩存獲得.
*/
SDImageCacheTypeDisk,
/**
* The image was obtained from the memory cache.
圖像從存儲器高速緩存獲得
*/
SDImageCacheTypeMemory
};</code></pre>
SDWebImage是iOS開發者經常使用的一個開源框架,這個框架的主要作用是:
一個異步下載圖片并且支持緩存的UIImageView分類.
UIImageView+WebCache
我們最常用的方法就是這個:
[_fineImageView sd_setImageWithURL:picURL placeholderImage:nil];
現在開始我們一步步地看這個方法的內部實現:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder {
[self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil];
}
這里會調用下面這個方法:
- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock
我們看UIImageView+WebCache.h文件,我們可以發現為開發者提供了很多類似于
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder
的方法.
這些方法最終都會調用- sd_setImageWithURL: placeholderImage: options: progress: completed:
,這個方法可以算是核心方法,下面我們看一下- sd_setImageWithURL: placeholderImage: options: progress: completed:
方法的實現:
首先執行
//移除UIImageView當前綁定的操作.當TableView的cell包含的UIImageView被重用的時候首先執行這一行代碼,保證這個ImageView的下載和緩存組合操作都被取消
[self sd_cancelCurrentImageLoad];
接下來我們來看看[self sd_cancelCurrentImageLoad]
內部是怎么執行的:
- (void)sd_cancelCurrentImageLoad {
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}
//然后會調用UIView+WebCacheOperation的
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key</code></pre>
UIView+WebCacheOperation
下面我們先來看看UIView+WebCacheOperation
里面都寫了些什么:
UIView+WebCacheOperation
這個分類提供了三個方法,用于操作綁定關系
#import <UIKit/UIKit.h>
#import "SDWebImageManager.h"
@interface UIView (WebCacheOperation)
/**
- Set the image load operation (storage in a UIView based dictionary)
設置圖像加載操作(存儲在和UIView做綁定的字典里面)
*
- @param operation the operation
- @param key key for storing the operation
*/
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key;
/**
- Cancel all operations for the current UIView and key
用這個key找到當前UIView上面的所有操作并取消
*
- @param key key for identifying the operations
*/
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key;
/**
Just remove the operations corresponding to the current UIView and key without cancelling them
*
- @param key key for identifying the operations
*/
- (void)sd_removeImageLoadOperationWithKey:(NSString )key;</code></pre>
為了方便管理和找到視圖正在進行的一些操作,WebCacheOperation
將每一個視圖的實例和它正在進行的操作(下載和緩存的組合操作)綁定起來,實現操作和視圖一一對應關系,以便可以隨時拿到視圖正在進行的操作,控制其取消等,如何進行綁定我們在下面分析:
UIView+WebCacheOperation.m
文件內
- (NSMutableDictionary
)operationDictionary</code>用到了<objc/runtime.h>
中定義的兩個函數:</p>
- objc_setAssociatedObject
- objc_getAssociatedObject
NSObject+AssociatedObject.h
@interface NSObject (AssociatedObject)
@property (nonatomic, strong) id associatedObject;
@end
NSObject+AssociatedObject.m
@implementation NSObject (AssociatedObject)
@dynamic associatedObject;
-(void)setAssociatedObject:(id)object{
objc_setAssociatedObject(self, @selector(associatedObject), object, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(id)associatedObject {
return objc_getAssociatedObject(self, @selector(associatedObject));
}
objc_setAssociatedObject
作用是對已存在的類在擴展中添加自定義的屬性 ,通常推薦的做法是添加屬性的key最好是static char
類型的,通常來說該屬性的key應該是常量唯一的.
objc_getAssociatedObject
根據key獲得與對象綁定的屬性.
- (NSMutableDictionary *)operationDictionary {
/*
這個loadOperationKey 的定義是:static char loadOperationKey;
它對應的綁定在UIView的屬性是operationDictionary(NSMutableDictionary類型)
operationDictionary的value是操作,key是針對不同類型視圖和不同類型的操作設定的字符串
注意:&是一元運算符結果是右操作對象的地址(&loadOperationKey返回static char loadOperationKey的地址)
*/
NSMutableDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
//如果可以查到operations,就rerun,反正給視圖綁定一個新的,空的operations字典
if (operations) {
return operations;
}
operations = [NSMutableDictionary dictionary];
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
return operations;
}
- (void)sd_cancelImageLoadOperationWithKey:(NSString )key {
// 取消正在下載的隊列
NSMutableDictionary operationDictionary = [self operationDictionary];
//如果 operationDictionary可以取到,根據key可以得到與視圖相關的操作,取消他們,并根據key值,從operationDictionary里面刪除這些操作
id operations = [operationDictionary objectForKey: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];
}
}</code></pre>
接下來我們繼續探索
- sd_setImageWithURL: placeholderImage: options: progress: completed:
- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionBlock)completedBlock {
[self sd_cancelCurrentImageLoad];
//將 url作為屬性綁定到ImageView上,用static char imageURLKey作key
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
/*options & SDWebImageDelayPlaceholder這是一個位運算的與操作,!(options & SDWebImageDelayPlaceholder)的意思就是options參數不是SDWebImageDelayPlaceholder,就執行以下操作
define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
*/
這是一個宏定義,因為圖像的繪制只能在主線程完成,所以dispatch_main_sync_safe就是為了保證block在主線程中執行
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
//設置imageView的placeHolder
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];
//如果imageView不存在了就return停止操作
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
/
SDWebImageAvoidAutoSetImage,默認情況下圖片會在下載完畢后自動添加給imageView,但是有些時候我們想在設置圖片之前加一些圖片的處理,就要下載成功后去手動設置圖片了,不會執行wself.image = image;
,而是直接執行完成回調,有用戶自己決定如何處理。
/
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
completedBlock(image, error, cacheType, url);
return;
}
/
如果后兩個條件中至少有一個不滿足,那么就直接將image賦給當前的imageView
,并調用setNeedsLayout
/
else if (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綁定新的操作,以為之前把ImageView的操作cancel了
[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);
}
});
}
}</code></pre>
SDWebImageManager
-sd_setImageWithURL:forState:placeholderImage:options:completed:
中,下載圖片方法是位于SDWebImageManager
類中- downloadImageWithURL: options:progress:completed:
函數
我們看下文檔是對SDWebImageManager
怎么描述的
/**
- The SDWebImageManager is the class behind the UIImageView+WebCache category and likes.
- It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache).
- You can use this class directly to benefit from web image downloading with caching in another context than
- a UIView.
*
- Here is a simple example of how to use SDWebImageManager:
*
- @code
/
概述了SDWenImageManager的作用,其實UIImageVIew+WebCache這個Category背后執行操作的就是這個SDWebImageManager.它會綁定一個下載器也就是SDwebImageDownloader和一個緩存SDImageCache/
/**
- Downloads the image at the given URL if not present in cache or return the cached version otherwise.
若圖片不在cache中,就根據給定的URL下載圖片,否則返回cache中的圖片
*
- @param url The URL to the image
- @param options A mask to specify options to use for this request
- @param progressBlock A block called while image is downloading
- @param completedBlock A block called when operation has been completed.
*
- This parameter is required.
- This block has no return value and takes the requested UIImage as first parameter.
- In case of error the image parameter is nil and the second parameter may contain an NSError.
*
- The third parameter is an
SDImageCacheType
enum indicating if the image was retrieved from the local cache
- or from the memory cache or from the network.
*
- The last parameter is set to NO when the SDWebImageProgressiveDownload option is used and the image is
- downloading. This block is thus called repeatedly with a partial image. When image is fully downloaded, the
- block is called a last time with the full image and the last parameter set to YES.
*
- @return Returns an NSObject conforming to SDWebImageOperation. Should be an instance of SDWebImageDownloaderOperation
*/
/第一個參數是必須的,就是image的url
第二個參數options你可以定制化各種各樣的操作第三個參數是一個回調block,用于圖片下載過程中的回調
第四個參數是一個下載完成的回調,會在圖片下載完成后回調返回值是一個NSObject類,并且這個NSObject類是遵循一個協議這個協議叫SDWebImageOperation,這個協議里面只寫了一個協議,就是一個cancel一個operation的協議
*/
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;</code></pre>
我們繼續看SDWebImageManager .m
初始化方法
/初始化方法
1.獲得一個SDImageCache的單例2.獲得一個SDWebImageDownloader的單例
3.新建一個MutableSet來存儲下載失敗的url4.新建一個用來存儲下載operation的可遍數組
*/
- (id)init {
if ((self = [super init])) {
_imageCache = [self createCache];
_imageDownloader = [SDWebImageDownloader sharedDownloader];
_failedURLs = [NSMutableSet new];
_runningOperations = [NSMutableArray new];
}
return self;
}</code></pre>
利用image的url生成一個緩存時需要的key,cacheKeyFilter
的定義如下:
@property (nonatomic, copy) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
typedef NSString *(^SDWebImageCacheKeyFilterBlock)(NSURL *url);
是一個可以返回一個字符串的block
//如果檢測到cacheKeyFilter不為空的時候,利用cacheKeyFilter來生成一個key
//如果為空,那么直接返回URL的string內容,當做key.
- (NSString )cacheKeyForURL:(NSURL )url {
if (self.cacheKeyFilter) {
return self.cacheKeyFilter(url);
}
else {
return [url absoluteString];
}
}</code></pre>
檢查一個圖片是否被緩存的方法
- (BOOL)cachedImageExistsForURL:(NSURL )url {
//調用上面的方法取到image的url對應的key
NSString key = [self cacheKeyForURL:url];
//首先檢測內存緩存中時候存在這張圖片,如果已有直接返回yes
if ([self.imageCache imageFromMemoryCacheForKey:key] != nil) return YES;
//如果內存緩存里面沒有這張圖片,那么就調用diskImageExistsWithKey這個方法去硬盤找
return [self.imageCache diskImageExistsWithKey:key];
}
// 檢測硬盤里是否緩存了圖片
(BOOL)diskImageExistsForURL:(NSURL )url {
NSString key = [self cacheKeyForURL:url];
return [self.imageCache diskImageExistsWithKey:key];
}</code></pre>
下面兩個方法比較類似,都是先根據圖片的url創建對應的key
第一個方法先用
BOOL isInMemoryCache = ([self.imageCache imageFromMemoryCacheForKey:key] != nil);
判斷圖片有沒有在內存緩存中,如果圖片在內存緩存中存在,就在主線程里面回調block,如果圖片沒有在內存緩存中就去查找是不是在磁盤緩存里面,然后在主線程里面回到block
第二個方法只查詢圖片是否在磁盤緩存里面,然后在主線程里面回調block
- (void)cachedImageExistsForURL:(NSURL *)url
completion:(SDWebImageCheckCacheCompletionBlock)completionBlock {
NSString *key = [self cacheKeyForURL:url];
BOOL isInMemoryCache = ([self.imageCache imageFromMemoryCacheForKey:key] != nil);
if (isInMemoryCache) {
// making sure we call the completion block on the main queue
dispatch_async(dispatch_get_main_queue(), ^{
if (completionBlock) {
completionBlock(YES);
}
});
return;
}
[self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
// the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
if (completionBlock) {
completionBlock(isInDiskCache);
}
}];
}
(void)diskImageExistsForURL:(NSURL *)url
completion:(SDWebImageCheckCacheCompletionBlock)completionBlock {
NSString *key = [self cacheKeyForURL:url];
[self.imageCache diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) {
// the completion block of checkDiskCacheForImageWithKey:completion: is always called on the main queue, no need to further dispatch
if (completionBlock) {
completionBlock(isInDiskCache);
}
}];
}</code></pre>
上面的方法都是用于查詢圖片是否在內存緩存或磁盤的緩存下面的方法可以算是SDWebImageManager
核心方法:
//通過url建立一個operation用來下載圖片.
(id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock</code></pre>
//防止開發者把傳入NSString類型的url,如果url的類型是NSString就給轉換成NSURL類型
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
//如果轉換NSURL失敗,就把傳入的url置為nil下載停止
if (![url isKindOfClass:NSURL.class]) {
url = nil;
}</code></pre>
首先我們先來看看__block
和__weak
的區別
__block
用于指明當前聲明的變量在被block捕獲之后,可以在block中改變變量的值.因為在block聲明的同時會截獲該block所使用的全部自動變量的值,這些值只在block中只有"使用權"而不具有"修改權".而block說明符就為block提供了變量的修改權,**block不能避免循環引用**,這就需要我們在 block 內部將要退出的時候手動釋放掉 blockObj,blockObj = nil
__weak
是所有權修飾符,__weak
本身是可以避免循環引用的問題的,但是其會導致外部對象釋放之后,block內部也訪問不到對象的問題,我們可以通過在block內部聲明一個__strong
的變量來指向weakObj,使外部既能在block內部保持住又能避免循環引用
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;
我們再來看看SDWebImageCombinedOperation
到底有一些什么內容
SDWebImageCombinedOperation
它什么也不做,保存了兩個東西(一個block,可以取消下載operation,一個operation,cacheOperation用來下載圖片并且緩存的operation)
并且SDWebImageCombineOperation
遵循<SDWebImageOperation>
協議,所以operation
可以作為返回值返回
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic) NSOperation *cacheOperation;
@end
@implementation SDWebImageCombinedOperation
(void)setCancelBlock:(SDWebImageNoParamsBlock)cancelBlock {
// check if the operation is already cancelled, then we just call the cancelBlock
if (self.isCancelled) {
if (cancelBlock) {
cancelBlock();
}
_cancelBlock = nil; // don't forget to nil the cancelBlock, otherwise we will get crashes
} else {
_cancelBlock = [cancelBlock copy];
}
}
(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;
}
}
@end</code></pre>
@synchronized
是OC中一種方便地創建互斥鎖的方式--它可以防止不同線程在同一時間執行區塊的代碼
self.failedURLs
是一個NSSet
類型的集合,里面存放的都是下載失敗的圖片的url,failedURLs不是NSArray類型的原因是:
在搜索一個個元素的時候NSSet比NSArray效率高,主要是它用到了一個算法hash(散列,哈希) ,比如你要存儲A,一個hash算法直接就能找到A應該存儲的位置;同樣當你要訪問A的時候,一個hash過程就能找到A存儲的位置,對于NSArray,若想知道A到底在不在數組中,則需要遍歷整個數據,顯然效率較低了
并且NSSet里面不含有重復的元素,同一個下載失敗的url只會存在一個
- (BOOL)containsObject:(ObjectType)anObject;
,判斷集合里面是否含有這個obj
BOOL isFailedUrl = NO;
//創建一個互斥鎖防止現有的別的線程修改failedURLs
//判斷這個url是否是fail過的,如果url failed過的那么isFailedUrl就是true.
@synchronized (self.failedURLs) {
isFailedUrl = [self.failedURLs containsObject:url];
}
//如果url不存在那么直接返回一個block,如果url存在那么繼續
//!(options & SDWebImageRetryFailed) 之前就提過一個類似的了,它的意思看這個options是不是和SDWebImageRetryFailed不相同
//如果不相同并且isFailedUrl是true.那么就回調一個error的block
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;
}</code></pre>
把operation加入到self.runningOperations的數組里面,并創建一個互斥線程鎖來保護這個操作
獲取image的url對應的key
@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
NSString
key = [self cacheKeyForURL:url];</code></pre>
其實看到這里,下面牽扯的代碼就會越來越越多了,會牽扯到的類也越來越多,我們先一步步地順著往下看,然后再看涉及到的類里面寫了些什么,最后再做總結,在這之前如果你對NSOperation
還不夠了解建議你先暫時停下看看下這篇文章NSOperation. 然后再繼續往下閱讀.
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock;
是SDImageCache的一個方法,根據圖片的key,異步查詢磁盤緩存的方法
我們先來看下這個方法里面都有什么:
_ioQueue
的定義是:
@property (SDDispatchQueueSetterSementics, nonatomic) dispatch_queue_t ioQueue;
_ioQueue
的初始化是:
_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);
DISPATCH_QUEUE_SERIAL代表的是創建一個串行的隊列,所以_ioQueue
是一個串行隊列(任務一個執行完畢才執行下一個)
PS:如果你對GCD隊列不太了解可以先看下GCD使用經驗與技巧淺談,然后再繼續閱讀額
- (NSOperation )queryDiskCacheForKey:(NSString )key
done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}
if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}
// 1.首先查看內存緩存,如果查找到,則直接調用doneBlock并返回
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}
//2.如果內存中沒有,則在磁盤中查找,如果找到,則將其放到內存緩存中,并調用doneBlock回調
NSOperation operation = [NSOperation new];
//在ioQueue中串行處理所有磁盤緩存
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}
//創建自動釋放池,內存及時釋放
@autoreleasepool {
//根據圖片的url對應的key去磁盤緩存中查找圖片
UIImage diskImage = [self diskImageForKey:key];
//如果可以在磁盤中查找到image,并且self.shouldCacheImagesInMemory = YES(默認是YES,if memory cache is enabled)就將image儲存到內存緩存中
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
//self.memCache是NSCache創建的一個對象,下面的方法是NSCache儲存對象的方法,如果你對cost的作用不太了解可以看我另外一篇文章NSCache
[self.memCache setObject:diskImage forKey:key cost:cost];
}
//最后在主線程里面調用doneBlock返回
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});
return operation;</code></pre>
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
if (operation.isCancelled) {
@synchronized (self.runningOperations) {
[self.runningOperations removeObject:operation];
}
return;
}
//條件1:在緩存中沒有找到圖片或者options選項里面包含了SDWebImageRefreshCached(這兩項都需要進行請求網絡圖片的)
//條件2:代理允許下載,SDWebImageManagerDelegate的delegate不能響應imageManager:shouldDownloadImageForURL:方法或者能響應方法且方法返回值為YES.也就是沒有實現這個方法就是允許的,如果實現了的話,返回YES才是允許
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
//如果在緩存中找到了image且options選項包含SDWebImageRefreshCached,先在主線程完成一次回調,使用的是緩存中找的圖片
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
// 如果在緩存中找到了image但是設置了SDWebImageRefreshCached選項,傳遞緩存的image,同時嘗試重新下載它來讓NSURLCache有機會接收服務器端的更新
completedBlock(image, nil, cacheType, YES, url);
});
}
// 如果沒有在緩存中找到image 或者設置了需要請求服務器刷新的選項,則仍需要下載
SDWebImageDownloaderOptions downloaderOptions = 0;
//開始各種options的判斷
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) {
// 如果image已經被緩存但是設置了需要請求服務器刷新的選項,強制關閉漸進式選項
downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
// 如果image已經被緩存但是設置了需要請求服務器刷新的選項,忽略從NSURLCache讀取的image
downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
//創建下載操作,先使用self.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) {
// Do nothing if the operation was cancelled
//如果操作取消了,不做任何事情
// 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
//如果我們調用completedBlock,這個block會和另外一個completedBlock爭奪一個對象,因此這個block被調用后會覆蓋新的數據
}
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];
}
}
}
else {
//如果設置了下載失敗重試,將url從失敗列表中去掉
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
//options包含了SDWebImageRefreshCached選項,且緩存中找到了image且沒有下載成功
if (options & SDWebImageRefreshCached && image && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
// 圖片刷新遇到了NSSURLCache中有緩存的狀況,不調用完成回調。
}
//圖片下載成功并且 設置了需要變形Image的選項且變形的代理方法已經實現
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), ^{
//調用代理方法完成圖片transform
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
//對已經transform的圖片進行緩存
[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);
}
});
});
}
//如果沒有圖片transform的需求并且圖片下載完成且圖片存在就直接緩存
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);
}
});
}
}
if (finished) {
// 從正在進行的操作列表中移除這組合操作
@synchronized (self.runningOperations) {
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
}
}];
//設置組合操作取消得得回調
operation.cancelBlock = ^{
[subOperation cancel];
@synchronized (self.runningOperations) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (strongOperation) {
[self.runningOperations removeObject:strongOperation];
}
}
};
}
//處理其他情況
//case1.在緩存中找到圖片(代理不允許下載 或者沒有設置SDWebImageRefreshCached選項 滿足至少一項)
else if (image) {
//完成回調
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];
}
}
//case2:緩存中沒有扎到圖片且代理不允許下載
else {
//主線程執行完成回調
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];
}
}
}];</code></pre>
總結:
這個方法主要完成了這些工作:1.創建一個組合Operation
,是一個SDWebImageCombinedOperation
對象,這個對象負責對下載operation創建和管理,同時有緩存功能,是對下載和緩存兩個過程的組合。
2.先去尋找這張圖片 內存緩存和磁盤緩存,這兩個功能在self.imageCache
的queryDiskCacheForKey: done:
方法中完成,這個方法的返回值既是一個緩存operation
,最終被賦給上面的Operation
的cacheOperation
屬性。在查找緩存的完成回調中的代碼是重點:它會根據是否設置了SDWebImageRefreshCached
選項和代理是否支持下載決定是否要進行下載,并對下載過程中遇到NSURLCache的情況做處理,還有下載失敗的處理以及下載之后進行緩存,然后查看是否設置了形變選項并調用代理的形變方法進行對圖片形變處理。
3.將上面的下載方法返回的操作命名為subOperation
,并在組合操作operation
的cancelBlock
代碼塊中添加對subOperation
的cancel
方法的調用。
文/missummer(簡書作者)