1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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 }; |
SDWebImage是iOS開發者經常使用的一個開源框架,這個框架的主要作用是:
一個非同步下載圖片並且支援快取的UIImageView分類.
UIImageView+WebCache
我們最常用的方法就是這個:
1 |
[_fineImageView sd_setImageWithURL:picURL placeholderImage:nil]; |
現在開始我們一步步地看這個方法的內部實現:
1 2 3 |
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil]; } |
這裡會呼叫下面這個方法:
1 2 3 4 |
- (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:
方法的實現:
首先執行
1 2 |
//移除UIImageView當前繫結的操作.當TableView的cell包含的UIImageView被重用的時候首先執行這一行程式碼,保證這個ImageView的下載和快取組合操作都被取消 [self sd_cancelCurrentImageLoad]; |
接下來我們來看看[self sd_cancelCurrentImageLoad]
內部是怎麼執行的:
1 2 3 4 5 |
- (void)sd_cancelCurrentImageLoad { [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]; } //然後會呼叫UIView+WebCacheOperation的 - (void)sd_cancelImageLoadOperationWithKey:(NSString *)key |
UIView+WebCacheOperation
下面我們先來看看UIView+WebCacheOperation
裡面都寫了些什麼:
UIView+WebCacheOperation
這個分類提供了三個方法,用於操作繫結關係
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#import #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; |
為了方便管理和找到檢視正在進行的一些操作,WebCacheOperation
將每一個檢視的例項和它正在進行的操作(下載和快取的組合操作)繫結起來,實現操作和檢視一一對應關係,以便可以隨時拿到檢視正在進行的操作,控制其取消等,如何進行繫結我們在下面分析:
UIView+WebCacheOperation.m
檔案內
- (NSMutableDictionary *)operationDictionary
用到了中定義的兩個函式:
- objc_setAssociatedObject
- objc_getAssociatedObject
NSObject+AssociatedObject.h
123@interface NSObject (AssociatedObject)@property (nonatomic, strong) id associatedObject;@endNSObject+AssociatedObject.m
12345678@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獲得與物件繫結的屬性.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
- (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; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
- (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 operation in operations) { if (operation) { [operation cancel]; } } } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){ [(id) operations cancel]; } [operationDictionary removeObjectForKey:key]; } } |
接下來我們繼續探索
- sd_setImageWithURL: placeholderImage: options: progress: completed:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
- (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 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); } }); } } |
SDWebImageManager
-sd_setImageWithURL:forState:placeholderImage:options:completed:
中,下載圖片方法是位於SDWebImageManager
類中- downloadImageWithURL: options:progress:completed:
函式
我們看下文件是對SDWebImageManager
怎麼描述的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
/** * 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: * * <a href='http://www.jobbole.com/members/java12'>@code</a> /* 概述了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 )downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock; |
我們繼續看SDWebImageManager .m
初始化方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
/* *初始化方法 *1.獲得一個SDImageCache的單例 *2.獲得一個SDWebImageDownloader的單例 *3.新建一個MutableSet來儲存下載失敗的url *4.新建一個用來儲存下載operation的可遍陣列 */ - (id)init { if ((self = [super init])) { _imageCache = [self createCache]; _imageDownloader = [SDWebImageDownloader sharedDownloader]; _failedURLs = [NSMutableSet new]; _runningOperations = [NSMutableArray new]; } return self; } |
利用image的url生成一個快取時需要的key,cacheKeyFilter
的定義如下:
@property (nonatomic, copy) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
typedef NSString *(^SDWebImageCacheKeyFilterBlock)(NSURL *url);
是一個可以返回一個字串的block
1 2 3 4 5 6 7 8 9 10 |
//如果檢測到cacheKeyFilter不為空的時候,利用cacheKeyFilter來生成一個key //如果為空,那麼直接返回URL的string內容,當做key. - (NSString *)cacheKeyForURL:(NSURL *)url { if (self.cacheKeyFilter) { return self.cacheKeyFilter(url); } else { return [url absoluteString]; } } |
檢查一個圖片是否被快取的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
- (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]; } |
下面兩個方法比較類似,都是先根據圖片的url建立對應的key
第一個方法先用BOOL isInMemoryCache = ([self.imageCache imageFromMemoryCacheForKey:key] != nil);
判斷圖片有沒有在記憶體快取中,如果圖片在記憶體快取中存在,就在主執行緒裡面回撥block,如果圖片沒有在記憶體快取中就去查詢是不是在磁碟快取裡面,然後在主執行緒裡面回到block
第二個方法只查詢圖片是否在磁碟快取裡面,然後在主執行緒裡面回撥block
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
- (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); } }]; } |
上面的方法都是用於查詢圖片是否在記憶體快取或磁碟的快取下面的方法可以算是SDWebImageManager
核心方法:
1 2 3 4 5 |
//通過url建立一個operation用來下載圖片. - (id )downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock |
1 2 3 4 5 6 7 8 9 |
//防止開發者把傳入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; } |
首先我們先來看看__block
和__weak
的區別
__block
用於指明當前宣告的變數在被block捕獲之後,可以在block中改變變數的值.因為在block宣告的同時會截獲該block所使用的全部自動變數的值,這些值只在block中只有”使用權”而不具有”修改權”.而block說明符就為block提供了變數的修改權,**block不能避免迴圈引用**,這就需要我們在 block 內部將要退出的時候手動釋放掉 blockObj,blockObj = nil
__weak
是所有權修飾符,__weak
本身是可以避免迴圈引用的問題的,但是其會導致外部物件釋放之後,block內部也訪問不到物件的問題,我們可以通過在block內部宣告一個__strong
的變數來指向weakObj,使外部既能在block內部保持住又能避免迴圈引用
1 2 |
__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; __weak SDWebImageCombinedOperation *weakOperation = operation; |
我們再來看看SDWebImageCombinedOperation
到底有一些什麼內容
SDWebImageCombinedOperation
它什麼也不做,儲存了兩個東西(一個block,可以取消下載operation,一個operation,cacheOperation用來下載圖片並且快取的operation)
並且SDWebImageCombineOperation
遵循協議,所以
operation
可以作為返回值返回
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
@interface SDWebImageCombinedOperation : NSObject @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 |
@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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
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; } |
把operation加入到self.runningOperations的陣列裡面,並建立一個互斥執行緒鎖來保護這個操作
獲取image的url對應的key
1 2 3 4 |
@synchronized (self.runningOperations) { [self.runningOperations addObject:operation]; } NSString *key = [self cacheKeyForURL:url]; |
其實看到這裡,下面牽扯的程式碼就會越來越越多了,會牽扯到的類也越來越多,我們先一步步地順著往下看,然後再看涉及到的類裡面寫了些什麼,最後再做總結,在這之前如果你對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使用經驗與技巧淺談,然後再繼續閱讀額
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
- (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; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 |
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 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]; } } }]; |
總結:
這個方法主要完成了這些工作:1.建立一個組合Operation
,是一個SDWebImageCombinedOperation
物件,這個物件負責對下載operation建立和管理,同時有快取功能,是對下載和快取兩個過程的組合。
2.先去尋找這張圖片 記憶體快取和磁碟快取,這兩個功能在self.imageCache
的queryDiskCacheForKey: done:
方法中完成,這個方法的返回值既是一個快取operation
,最終被賦給上面的Operation
的cacheOperation
屬性。在查詢快取的完成回撥中的程式碼是重點:它會根據是否設定了SDWebImageRefreshCached
選項和代理是否支援下載決定是否要進行下載,並對下載過程中遇到NSURLCache的情況做處理,還有下載失敗的處理以及下載之後進行快取,然後檢視是否設定了形變選項並呼叫代理的形變方法進行對圖片形變處理。
3.將上面的下載方法返回的操作命名為subOperation
,並在組合操作operation
的cancelBlock
程式碼塊中新增對subOperation
的cancel
方法的呼叫。
整個大體的操作流程,就是這些,你不感覺少點啥子麼,如何下載,如何快取(快取只是暫時看了兩個相關的方法)我們在下一篇再詳細地看