SDWebImage 分析

SDWebImage 分析

Version 4.0.0


導(dǎo)航

按照模塊分析 SDWebImage

1. UI交互的基類(lèi) UIView+WebCache

2. SDWebImage 的主要管理者 SDWebImageManager

3. 緩存模塊 SDImageCache

4. 下載模塊 SDWebImageDownloader

5. 下載的執(zhí)行者 SDWebImageDownloaderOperation

6. 預(yù)加載 SDWebImagePrefetcher

7. GIF子模塊 FLAnimatedImage


UIKit 交互1 -- UIView+WebCache

1. 接口定義

a. 下載圖片

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

b. 下載已經(jīng)緩存過(guò)的圖片

- (void)sd_setImageWithPreviousCachedImageWithURL:(nullable NSURL *)url
                                 placeholderImage:(nullable UIImage *)placeholder
                                          options:(SDWebImageOptions)options
                                         progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                        completed:(nullable SDExternalCompletionBlock)completedBlock;

c. 下載動(dòng)圖

- (void)sd_setAnimationImagesWithURLs:(nonnull NSArray<NSURL *> *)arrayOfURLs;

d. 取消下載動(dòng)圖

- (void)sd_cancelCurrentAnimationImagesLoad;

2. 分析

a. 下載圖片

下載圖片有數(shù)個(gè)方法定義, 見(jiàn)UIView+WebCache.h, 最終都調(diào)用了UIView+WebCache中的 sd_internalSetImageWithURL 這個(gè)方法.

sd_internalSetImageWithURL 方法做的事情:

  • 首先取消了當(dāng)前 View 所綁定的一切請(qǐng)求.
  • 設(shè)置placeholder.
  • 調(diào)用SDWebImageManager下載圖片, 并將方法返回的 operation 與當(dāng)前 View 綁定.
  • 下載圖片回調(diào)處理.

Tips
dispatch_main_async_safe 定義了在主線(xiàn)程進(jìn)行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

weakSelf 為 nil 時(shí)候直接結(jié)束避免崩潰或者其他錯(cuò)誤.

b. 下載已經(jīng)緩存過(guò)的圖片

  • 首先調(diào)用SDImageCache從緩存中讀取Image.
  • 再直接執(zhí)行上一步下載圖片的代碼.
NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:url];
UIImage *lastPreviousCachedImage = [[SDImageCache sharedImageCache] imageFromCacheForKey:key];

[self sd_setImageWithURL:url placeholderImage:lastPreviousCachedImage ?: placeholder options:options progress:progressBlock completed:completedBlock];

Tips : 在執(zhí)行下載的過(guò)程中, 如果找到了緩存, 就忽略placeholder, 避免一次無(wú)效操作.

c. 下載動(dòng)圖

  • 取消當(dāng)前View 綁定的動(dòng)圖下載操作.
  • 遍歷傳入的URL數(shù)組, 對(duì)每個(gè)URL調(diào)用SDWebImageManager下載圖片, 并將方法返回的 operation 裝入數(shù)組,再將這個(gè)數(shù)組與當(dāng)前 View 綁定.

PS. 這也解釋了 - (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key 為什么會(huì)有如下看起來(lái)很奇怪的代碼, operation的類(lèi)型并不是固定的.

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];
}

Tips : [self operationDictionary] 使用 Runtime 為實(shí)例增加了變量.

Question : 如何保證下載的順序?

d. 取消下載動(dòng)圖

  • 直接調(diào)用取消方法 - (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key

Tips : 關(guān)于id <SDWebImageOperation> operation解釋:
每個(gè) operaton 都有實(shí)現(xiàn)一個(gè) - (void)cancel; 方法, 這個(gè)是在SDWebImageOperation協(xié)議中定義, 無(wú)論是什么類(lèi)型實(shí)例, 只要實(shí)現(xiàn)了該協(xié)議, 都可以統(tǒng)一調(diào)用,詳細(xì)解釋可以搜索iOS+面向接口編程.

3. 小結(jié)

UIView+WebCache 模塊中, 只做了一些簡(jiǎn)單的操作, 定義好了與 UIKit 交互接口, 下載與取消交給了 SDWebImageManager 處理, 緩存交給了 SDImageCache 處理.


SDWebImage幕后管理者 -- SDWebImageManager

1. 接口定義

a. 緩存模塊

@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;

b. 下載模塊

@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;

c. 下載圖片

- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                             progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                            completed:(nullable SDInternalCompletionBlock)completedBlock;

d. 手動(dòng)設(shè)置圖片緩存

- (void)saveImageToCache:(nullable UIImage *)image forURL:(nullable NSURL *)url;

e. 取消所有的操作

- (void)cancelAll;

f. 當(dāng)前是否有操作在運(yùn)行

- (BOOL)isRunning;

g. 異步檢查圖片是否已經(jīng)被緩存

- (void)cachedImageExistsForURL:(nullable NSURL *)url
                     completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

h. 異步檢查圖片是否已經(jīng)被緩存在了磁盤(pán)上

- (void)diskImageExistsForURL:(nullable NSURL *)url
                   completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

i. 獲取URL緩存索引的Key

- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url;

2. 分析

a. 緩存模塊

  • 通過(guò) [SDImageCache sharedImageCache] 引用到了 SDImageCache 的單例.

b. 下載模塊

c. 下載圖片

  • 判斷輸入錯(cuò)誤處理等, 并生成一個(gè) SDWebImageCombinedOperation 實(shí)例, 也就是上文提到過(guò)的一個(gè)operation, 然后將改operation加入self.runningOperations方便管理;
  • 使用 [SDImageCache queryCacheOperationForKey: done:] 生成了一個(gè)NSOperation實(shí)例 并賦值給了上一步所使用的operation的cacheOperation屬性, 方便執(zhí)行cancel方法.
  • 上一步中, 在 SDImageCache 中先在內(nèi)存中找圖片的緩存,找到直接執(zhí)行回調(diào),若沒(méi)找到則在硬盤(pán)上找緩存,若找到切可以在內(nèi)存做緩存,則在內(nèi)存中做緩存, 然后執(zhí)行回調(diào).
  • queryCacheOperationForKey方法的回調(diào)中,如果發(fā)現(xiàn)當(dāng)前Operation被取消了,則將該operation從self.runningOperations移除,并使用@synchronized保證線(xiàn)程安全.
  • 如果在緩存找到了圖片,執(zhí)行回調(diào),并移除operation
  • 如果沒(méi)有找到, 則調(diào)用[SDWebImageDownloader downloadImageWithURL:options:progress:completed:]方法下載圖片, 并將該方法換回的token標(biāo)志綁定到operation的cancelBlock中, 方便取消下載請(qǐng)求;
  • 下載完成之后之后, 執(zhí)行imageManager:transformDownloadedImage:withURL:方便使用者在下載完成時(shí)候立刻對(duì)圖片做一些自定義處理,再將該圖片進(jìn)行緩存. 最后執(zhí)行回調(diào)并移除改operation.

d. 手動(dòng)設(shè)置圖片緩存

  • 直接調(diào)用[SDImageCache storeImage:forKey:toDisk:completion:]

e. 取消所有的操作

  • 因?yàn)樯婕暗?self.runningOperations 的讀寫(xiě), 因此用 @synchronized 保證線(xiàn)程安全.
  • copy 了一份self.runningOperations, 并使用 [NSArray makeObjectsPerformSelector:] 方法取消隊(duì)列中所有的操作.
  • 將復(fù)制的隊(duì)列中的所有元素從self.runningOperations移除.

f. 當(dāng)前是否有操作在運(yùn)行

  • 為保證原子性,使用@synchronized訪(fǎng)問(wèn)self.runningOperations

g. 異步檢查圖片是否已經(jīng)被緩存

  • 分別調(diào)用[SDImageCache imageFromMemoryCacheForKey:][SDImageCache diskImageExistsWithKey:completion:] 方法檢查是否在內(nèi)存和硬盤(pán)上緩存.
  • 由于檢查硬盤(pán)是否緩存要用到專(zhuān)門(mén)的IO線(xiàn)程(在SDImageCache中定義), 調(diào)用者不可能去等待IO線(xiàn)程,因此此方法被設(shè)計(jì)為異步方法.

h. 異步檢查圖片是否已經(jīng)被緩存在了磁盤(pán)上

  • 調(diào)用[SDImageCache diskImageExistsWithKey:completion:] 方法檢查是是否在硬盤(pán)上緩存.

i. 獲取URL緩存索引的Key

  • 如果定義了self.cacheKeyFilter自定義存儲(chǔ)Key,則使用該回調(diào)獲取用于緩存索引的Key

3. 小結(jié)

SDWebImage可以看出作者考慮到了很多一般開(kāi)發(fā)者不會(huì)去考慮的事情, 簡(jiǎn)單的如線(xiàn)程安全, 更細(xì)致的如imageManager:transformDownloadedImage:withURL:方法, 方便使用SDWebImage的人在使用之前先處理, 再緩存, 一個(gè)個(gè)簡(jiǎn)單的應(yīng)用場(chǎng)景是用戶(hù)想對(duì)一張網(wǎng)絡(luò)圖片進(jìn)行模糊處理, 一般的步驟是先用SDWebImage下載,然后自行模糊處理,再展示. 但如果有大量圖片要處理, 又涉及到tableView的復(fù)用問(wèn)題, 為了提高性能, 使用者要自己對(duì)模糊之后的圖片做緩存, 優(yōu)化緩存策略和IO潛在地問(wèn)題等等. 實(shí)際上SDWebImage 已經(jīng)可以處理這個(gè)問(wèn)題而不需要使用者再去考慮.


SDWebImage緩存模塊 -- SDImageCache

1. 接口定義

a. 緩存圖片

- (void)storeImage:(nullable UIImage *)image
         imageData:(nullable NSData *)imageData
            forKey:(nullable NSString *)key
            toDisk:(BOOL)toDisk
        completion:(nullable SDWebImageNoParamsBlock)completionBlock;

b. 緩存圖片到硬盤(pán)上(只能從IO Queue 調(diào)用)

- (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key;

c. 檢查圖片是否在硬盤(pán)上緩存(只檢查, 不會(huì)把圖片加到內(nèi)存)

- (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDWebImageCheckCacheCompletionBlock)completionBlock;

d. 檢查是否有緩存

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;

e. 從內(nèi)存中取緩存的圖片(同步)

- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key;

f. 從硬盤(pán)取緩存的圖片(同步)

- (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key;

g. 從緩存中取圖片(同步)

- (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key;

h. 從內(nèi)存和硬盤(pán)刪除圖片緩存(異步)

- (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion;

i. 從內(nèi)存移除緩存, 選擇是否從硬盤(pán)移除

- (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion;

j. 清除內(nèi)存緩存

- (void)clearMemory;

k. 異步清除磁盤(pán)緩存

- (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion;

l. 異步清除硬盤(pán)上已經(jīng)過(guò)期的緩存

- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;

2. 分析

初始化方法

- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
                       diskCacheDirectory:(nonnull NSString *)directory;

初始化各個(gè)屬性:

@property (strong, nonatomic, nullable) dispatch_queue_t ioQueue;

_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL);

生成的一個(gè)串行隊(duì)列,專(zhuān)用于IO操作. 不再使用的時(shí)候應(yīng)該使用dispatch_release釋放隊(duì)列.

@property (strong, nonatomic, nonnull) NSCache *memCache;

NSCache是iOS系統(tǒng)提供的緩存類(lèi),通過(guò)鍵值對(duì)對(duì)需要緩存的對(duì)象作強(qiáng)引用來(lái)達(dá)到緩存的目的.

NSFileManager *_fileManager;
dispatch_sync(_ioQueue, ^{
            _fileManager = [NSFileManager new];
        });

注意,生成_fileManager在ioQueue中,并且是是一個(gè)同步操作, 之后_fileManager都要在ioQueue中進(jìn)行.

a. 緩存圖片

  • 首先根據(jù)SDImageCacheConfig判斷是否在在內(nèi)存中緩存,使用[NSCache setObject:forKey:cost:]方法緩存.
  • 根據(jù)toDisk參數(shù)判斷是否在磁盤(pán)中緩存,在ioQueue中調(diào)用[self storeImageDataToDisk:data forKey:key];緩存到硬盤(pán),使用@autoreleasepool釋放臨時(shí)變量.
  • 最后在主線(xiàn)程執(zhí)行回調(diào).

Tips
NSCache 有最大緩存容積的設(shè)置totalCostLimit, 但是這個(gè)設(shè)置只有在設(shè)置緩存的時(shí)候指定要緩存對(duì)象占用的字節(jié)數(shù)(cost)才能生效. 但是對(duì)象的內(nèi)存占用計(jì)算十分復(fù)雜, SDWebImage只是給出了一個(gè)大致值image.size.height * image.size.width * image.scale * image.scale;.

b. 緩存圖片到硬盤(pán)上(只能從IO Queue 調(diào)用)

此方法只能在ioQueue中調(diào)用,奇怪的是SDImageCache并沒(méi)有暴露ioQueue訪(fǎng)問(wèn), 因此, 將此方法暴露在.h文件是沒(méi)有意義的.

  • 首先檢查是不是在ioQueue
  • 使用key的16位MD5編碼+文件后綴作為緩存文件名生成存儲(chǔ)路徑, 如果目標(biāo)路徑不存在,則創(chuàng)建該路徑
  • 使用[NSfileManager createFileAtPath:contents:attributes:]將下載下來(lái)的圖片的原始二進(jìn)制數(shù)據(jù)寫(xiě)磁盤(pán)

c. 檢查圖片是否在硬盤(pán)上緩存(只檢查, 不會(huì)把圖片加到內(nèi)存)

  • 先后用key參數(shù)的md5編碼組合路徑找是都存在文件,在用key參數(shù)的md5編碼+文件后綴尋找是否存在
  • 在主線(xiàn)程中回調(diào)

在某個(gè)版本之前, 硬盤(pán)緩存沒(méi)有文件后綴名, 為了兼容, 要做兩次查找

d. 檢查是否有緩存

  • 先調(diào)用imageFromMemoryCacheForKey, 并用結(jié)果執(zhí)行回調(diào), 注意這一步是同步操作, 因此不需要 NSOperation 來(lái)取消操作, 故返回nil.
  • 再調(diào)用diskImageDataBySearchingAllPathsForKey在硬盤(pán)找緩存, 如果找到并且條件允許, 在內(nèi)存緩存該圖片. 這一步要在ioQueue中異步執(zhí)行, 可以利用 NSOperation 取消這一步操作. 因此在執(zhí)行回調(diào)后返回該NSOperation.

e. 從內(nèi)存中取緩存的圖片(同步)

  • 直接在self.memCache讀取, 可能情況是沒(méi)有緩存-返回nil, 有緩存但是緩存已經(jīng)被釋放-返回nil, 或是尋找緩存命中-返回目標(biāo)圖片.

f. 從硬盤(pán)取緩存的圖片(同步)

  • 在硬盤(pán)上找目標(biāo)二進(jìn)制文件,找到后調(diào)用UIImage *image = [UIImage sd_imageWithData:data];image = [self scaledImageForKey:key image:image];生成目標(biāo)圖片,若允許,在內(nèi)存中緩存該圖片.

理論上來(lái)說(shuō), 這句話(huà)放在ioQueue中執(zhí)行會(huì)好一些, 猜測(cè)可能是需要同步執(zhí)行

g. 從緩存中取圖片(同步)

  • 直接調(diào)用上面兩個(gè)方法.

h. 從內(nèi)存和硬盤(pán)刪除圖片緩存(異步)

  • 直接調(diào)用下方的方法.

i. 從內(nèi)存移除緩存, 選擇是否從硬盤(pán)移除

  • 直接從內(nèi)存緩存中移除, (NSCache是線(xiàn)程安全的)
  • 在ioQueue中移除目標(biāo)文件, 并在主線(xiàn)程回調(diào)

j. 清除內(nèi)存緩存

  • 直接移除self.memCache所有對(duì)象

k. 異步清除磁盤(pán)緩存

  • 調(diào)用下方方法

l. 異步清除硬盤(pán)上已經(jīng)過(guò)期的緩存

  • 遍歷緩存目錄下每一個(gè)文件, 獲取其屬性, 若獲取失敗或是該文件是文件夾則跳過(guò)
  • 若文件的修改時(shí)間早于設(shè)定時(shí)間,則將文件地址加入待刪除列表.
  • 遍歷結(jié)束, 從硬盤(pán)移除待刪除列表中的文件.
  • 計(jì)算未刪除的文件的總大小, 若仍大于目標(biāo)大小, 進(jìn)一步刪除最早改動(dòng)的文件.

Tips : [[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]這個(gè)比較挺有意思.

3. 小結(jié)

這一個(gè)模塊中,有內(nèi)存與硬盤(pán)兩級(jí)緩存, NSCache 在系統(tǒng)級(jí)別保證了線(xiàn)程安全,相對(duì)來(lái)說(shuō)處理容易. 但是IO操作本身較為耗時(shí), 單獨(dú)創(chuàng)建一個(gè)隊(duì)列作為ioQueue來(lái)進(jìn)行IO操作, 達(dá)到在硬盤(pán)上緩存的目的.

SDWebImage下載模塊 -- SDWebImageDownloader

1. 接口定義

a. 初始化

- (nonnull instancetype)initWithSessionConfiguration:(nullable NSURLSessionConfiguration *)sessionConfiguration NS_DESIGNATED_INITIALIZER;

b. 設(shè)置請(qǐng)求的Header

- (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field;

c. 下載圖片

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

d. 取消下載

- (void)cancel:(nullable SDWebImageDownloadToken *)token;

e. 暫停下載

- (void)setSuspended:(BOOL)suspended;

f. 取消所有的下載

- (void)cancelAllDownloads;

2. 分析

a. 初始化

  • _downloadQueue 是一個(gè)NSOperationQueue實(shí)例, 每個(gè)URL的請(qǐng)依賴(lài)這個(gè)queue進(jìn)行管理.
  • _URLOperations 用于存儲(chǔ)所有的operation實(shí)例, 每個(gè)URL對(duì)應(yīng)一個(gè)operation.
  • _barrierQueue 是一個(gè)并行隊(duì)列, operation的創(chuàng)建于取消都在這個(gè)隊(duì)列中完成.
  • self.session 是用于網(wǎng)絡(luò)請(qǐng)求的NSURLSession組件, 所有的operation對(duì)這個(gè)session保持了弱引用.

b. 設(shè)置請(qǐng)求的Header

c. 下載圖片

  • 直接調(diào)用并返回了[SDWebImageDownloader addProgressCallback:completedBlock:forURL:createCallback:]方法, 以下分析addProgressCallback.
  • 首先使用dispatch_barrier_sync方法, 這是一個(gè)同步方法, 但是參數(shù)self.barrierQueue是一個(gè)并發(fā)隊(duì)列, 因此當(dāng)前線(xiàn)程會(huì)等待bolck中執(zhí)行完(由于使用的是dispatch_barrier_sync, 而不是dispatch_sync,所以當(dāng)前block也會(huì)等待self.barrierQueue中已經(jīng)添加的任務(wù)執(zhí)行完).
  • 如果 執(zhí)行addProgressCallback最后一個(gè)參數(shù)createCallback(), 并返回一個(gè)operation, 注意,執(zhí)行createCallback又回到了上一層方法downloadImageWithURL方法中.
  • downloadImageWithURL方法, 首先組裝好了一個(gè)request, 然后生成了一個(gè)SDWebImageDownloaderOperation或者其子類(lèi)的的實(shí)例, 并將這個(gè)operaion加入了self.downloadQueue隊(duì)列中. 如果這個(gè)隊(duì)列嚴(yán)格采用LIFO(是棧不是隊(duì)), 那么上一個(gè)加入的operation要依賴(lài)于這個(gè)operation, 用[sself.lastAddedOperation addDependency:operation];達(dá)成目的. 最后返回這個(gè)operation. 然后又回到了addProgressCallback這個(gè)方法. 吐下槽, 思路有點(diǎn)糾結(jié).
  • 回到addProgressCallback方法后,執(zhí)行[operation addHandlersForProgress:progressBlock completed:completedBlock]將兩個(gè)Block綁定到operation中, 復(fù)制使用的[NSBlock copy]方法, 避免不必要的引用. 注意這兒同一個(gè)url可能被請(qǐng)求多次, 因此一個(gè)url綁定一個(gè)operation, 一個(gè)operation綁定多個(gè)執(zhí)行回調(diào)
  • 返回cancelToken 下載方法執(zhí)行結(jié)束.

Tips : 怎么開(kāi)始下載的? SDWebImageDownloaderOperation繼承了NSOperation, 并重寫(xiě)了start()方法, 并在start()方法中調(diào)用了[self.dataTask resume];開(kāi)始下載.

d. 取消下載

  • 放在 self.barrierQueue 異步執(zhí)行.
  • 執(zhí)行[SDWebImageDownloaderOperation cancel:]方法.

Tips : [SDWebImageDownloaderOperation cancel:]首先將token對(duì)應(yīng)的callback移除掉. 當(dāng)所有的callbacl都移除掉之后, 會(huì)調(diào)用父類(lèi)NSOperationcancel方法, 這會(huì)將isCancelled屬性置為YES, 在start方法調(diào)用的時(shí)候就不會(huì)真正執(zhí)行. 最后調(diào)用[self.dataTask cancel];關(guān)閉數(shù)據(jù)傳輸.

Question: 手動(dòng)調(diào)cancel方法后, 就不會(huì)執(zhí)行失敗的block了嗎?

e. 暫停下載

  • 直接調(diào)用(self.downloadQueue).suspended = suspended;, 這兒利用了NSOperationQueue的功能.

f. 取消所有的下載

  • [self.downloadQueue cancelAllOperations];
  • 自動(dòng)調(diào)用SDWebImageDownloaderOperationcancel方法.

3. 小結(jié)

這一個(gè)模塊開(kāi)始進(jìn)行圖片下載相關(guān)代碼的執(zhí)行, 然而真正的下載代碼還是被放在了SDWebImageDownloaderOperation中, 'SDWebImageDownloader'模塊的分析只是對(duì)SDWebImageDownloaderOperation做了簡(jiǎn)單的描述, 主要還是重點(diǎn)分析本模塊所做的事情--管理所有的下載行為. 此外, self.downloadQueue保證了對(duì)self.URLOperations操作能并發(fā), 但又不相互干擾(同時(shí)保證異步和并發(fā), 但實(shí)際上并沒(méi)有并發(fā)).

SDWebImage下載的執(zhí)行者 -- SDWebImageDownloaderOperation


1. 接口定義

a. 初始化

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options;

b. 存儲(chǔ)回調(diào)Block

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

c. 開(kāi)始(繼承父類(lèi))

- (void)start;

d. 取消(繼承父類(lèi))

- (void)cancel;

e. 是否在執(zhí)行(繼承父類(lèi))

- (void)setFinished:(BOOL)finished;

f. 是否已結(jié)束(繼承父類(lèi))

- (void)setExecuting:(BOOL)executing;

g. 取消單個(gè)操作

- (BOOL)cancel:(nullable id)token;

2. 分析

a. 初始化

  • 首先將參數(shù)中的request復(fù)制了一份, 注意, NSURLRequest實(shí)現(xiàn)了NSCopying協(xié)議.
  • 初始化了存儲(chǔ)block回調(diào)的數(shù)組_callbackBlocks.
  • 將參數(shù)session復(fù)制給了_unownedSession屬性, 注意這兒是弱引用, 避免不必要的引用.
  • 生成了一個(gè)并發(fā)隊(duì)列_barrierQueue,用于_callbackBlocks的增刪操作,保證線(xiàn)程安全.

b. 存儲(chǔ)回調(diào)Block

  • 在這兒將progressBlockcompletedBlock兩個(gè)block都復(fù)制了一份,再存儲(chǔ)到_callbackBlocks中.
  • 在這兒也使用了dispatch_barrier_async方法, 這是個(gè)異步操作

c. 開(kāi)始(繼承父類(lèi))

  • 注意, 這兒雖然覆蓋了父類(lèi)的start方法, 但是不能調(diào)用[super start];
  • SDWebImage中, Operation被加到SDWebImageDownloaderdownloadQueue中后會(huì)被自動(dòng)執(zhí)行, (自動(dòng)調(diào)用operationstart方法)
  • 首先判斷自己是否被取消了
  • 再判斷self.unownedSession是否還在, 一般情況下是還在的, 因?yàn)槟J(rèn)的SDWebImageDownloader是個(gè)單例不會(huì)被釋放, 但如果開(kāi)發(fā)者自己初始化一個(gè)SDWebImageDownloader就會(huì)存在self.unownedSession不再引用一個(gè)session的情況.
  • 根據(jù)sessionrequest生成一個(gè)dataTask, 并將自己標(biāo)記為正在執(zhí)行.
  • 開(kāi)始下載, 并觸發(fā)第一次'progressBlock'.

d. 取消(繼承父類(lèi))

  • 若已經(jīng)完成, 直接返回.
  • 調(diào)用[super cancel], 會(huì)將isFinished標(biāo)記為YES.
  • 取消下載操作.

e. 是否在執(zhí)行(繼承父類(lèi))

  • 用KVO通知值改變.

f. 是否已結(jié)束(繼承父類(lèi))

  • 同上.

g. 取消單個(gè)操作

  • 根據(jù)token將對(duì)應(yīng)的回調(diào)從``刪除, 在這兒使用了[NSArray removeObjectIdenticalTo:]方法, 利用"本體性"而不是"相等性"去移除對(duì)應(yīng)的回調(diào), 個(gè)人猜測(cè)是為了提高查找的速度. 具體可以參考Equality這篇文章.
  • 在這兒使用了dispatch_barrier_sync, 注意這兒是一個(gè)同步方法, 后面根據(jù)移除后_callbackBlocks是否為空判斷是否要停止當(dāng)前的下載.

Tips: startcancel@synchronized保證的線(xiàn)程安全, 對(duì)_callbackBlocks的操作使用一個(gè)隊(duì)列保障線(xiàn)程安全. 此外, operation持有兩個(gè)session, 一個(gè)是unownedSession, 這個(gè)由SDWebImageDownloader持有, operation對(duì)它保持弱引用, 還有一個(gè)是ownedSession, 當(dāng)初始化的session被釋放時(shí)候, 使用自己生成的session, 并用ownedSession保持引用, 并在[self reset]中釋放這個(gè)session.

3. 小結(jié)

這個(gè)Operation完成了SDWebImage最重要的下載功能. 將一個(gè)URL的下載下載封裝成一個(gè)NSOperation, 特別是在線(xiàn)程安全上做了一些優(yōu)化, 和使用異步或是同步, 哪些操作需要保證線(xiàn)程安全, 哪些元素需要復(fù)制, 值得思考. 在SDWebImage的issue中有很多于此模塊有關(guān)的, 值得細(xì)看.

SDWebImage 預(yù)加載 -- SDWebImagePrefetcher


1. 接口定義

1. 初始化

- (nonnull instancetype)initWithImageManager:(nonnull SDWebImageManager *)manager;

2. 執(zhí)行預(yù)加載

- (void)prefetchURLs:(nullable NSArray<NSURL *> *)urls
            progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock
           completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock;

3. 取消

- (void)cancelPrefetching;

2. 分析

1. 初始化

  • 默認(rèn)的init方法生成了一個(gè)新的SDWebImageManager實(shí)例, 在這兒使用了[SDWebImageManager new], 調(diào)用的是SDWebImageManager的方法默認(rèn)初始化方法. 因此, 這兒的manager和[SDWebImageManager sharedManager]不是一個(gè)實(shí)例, 但是由于SDWebImageManager的默認(rèn)初始化方法中使用的[SDImageCache sharedImageCache][SDWebImageDownloader sharedDownloader]單例, 所以在這兒初始化的manager和[SDWebImageManager sharedManager]共享的同一個(gè)_imageCache_imageDownloader實(shí)例.
  • 舉個(gè)例子, 在我寫(xiě)的一個(gè)測(cè)試程序中, [SDWebImageManager sharedManager]的內(nèi)存地址是0x61000107f280, 而SDWebImagePrefetcher所持有的manager地址是0x000060000106ef00, 他們不是同一個(gè)manager, 當(dāng)我打印[SDWebImageManager sharedManager]的各個(gè)屬性時(shí)候, 如下方結(jié)果, _imageCache_imageDownloader的地址是一致的.
(lldb) pinternals 0x61000107f280
(SDWebImageManager) $12 = {
  NSObject = {
    isa = SDWebImageManager
  }
  _delegate = nil
  _imageCache = 0x00006080010661c0
  _imageDownloader = 0x00006080000ff800
  _cacheKeyFilter = (null)
  _failedURLs = 0x00006080006406f0 0 elements
  _runningOperations = 0x00006080010671c0 @"1 element"
}

(lldb) pinternals 0x000060000106ef00
(SDWebImageManager) $8 = {
  NSObject = {
    isa = SDWebImageManager
  }
  _delegate = nil
  _imageCache = 0x00006080010661c0
  _imageDownloader = 0x00006080000ff800
  _cacheKeyFilter = (null)
  _failedURLs = 0x00006000006524b0 0 elements
  _runningOperations = 0x0000600001277f80 @"1 element"
}

2. 執(zhí)行預(yù)加載

  • 首先會(huì)取消掉所有的預(yù)加載, 所以確保這個(gè)方法不要被頻繁調(diào)用.
  • 記錄當(dāng)前的時(shí)間, Tips:使用的是CFAbsoluteTimeGetCurrent()比較高效的獲取時(shí)間的方法, 雖然后面好像沒(méi)用到這個(gè)屬性.
  • 對(duì)每個(gè)url調(diào)用一次使用startPrefetchingAtIndex方法, 在該方法中使用SDWebImageManager執(zhí)行下載URL, 緩存也是在SDWebImageManager中做的, 詳細(xì)可以參考SDWebImageManager內(nèi)容.
  • 需要注意的是, 為了避免隊(duì)列中由于某些未知原因?qū)е履硞€(gè)請(qǐng)求未被調(diào)用, 最終導(dǎo)致無(wú)法完全結(jié)束, 在startPrefetchingAtIndex中一個(gè)URL緩存完成之后,方法中有如下一段代碼, 目的是通過(guò)強(qiáng)制執(zhí)行下一個(gè)請(qǐng)求緩存的目的來(lái)增加self.requestedCount的值, 已達(dá)到處理這種弄異常的目的, 但是一般情況下不會(huì)只想到這里面來(lái).
if (self.prefetchURLs.count > self.requestedCount) {
    dispatch_async(self.prefetcherQueue, ^{
        [self startPrefetchingAtIndex:self.requestedCount];
    });
}

3. 取消

  • 情況當(dāng)前所有的記錄, 并使用持有的SDWebImageManager結(jié)束正在下載的任務(wù).

3. 小結(jié)

這一個(gè)模塊大部分是依靠SDWebImageManager來(lái)完成主體功能, 我曾經(jīng)在某篇博客上看到有人說(shuō)SDWebImagePrefetcher是不支持并發(fā)的, 至少在目前這個(gè)版本看來(lái), 是完全支持一組URL并發(fā)的, 但是不支持同時(shí)預(yù)加載多組URL.

SDWebImage 子模塊 GIF -- FLAnimatedImage

SDWebImage 支持動(dòng)態(tài)圖的, 建立在Flipboard的開(kāi)源項(xiàng)目FLAnimatedImage的基礎(chǔ)之上, 增加的一個(gè)擴(kuò)展, 使用方法是pod 'SDWebImage/GIF', 或者手動(dòng)把SDWebImage文件夾中的FLAnimatedImage文件夾拖入工程.

1. 接口定義

1. 加載Gif

- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;

2. 分析

1. 加載Gif

  • 直接調(diào)用[UIView sd_setImageWithURL:placeholderImage:options:progress:completed:]方法,這個(gè)方法在最開(kāi)始對(duì)UIView+WebCache模塊有介紹.
  • 回調(diào)結(jié)果中有UIImage *image, NSData *imageData, 如果imageData是Gif, 使用[FLAnimatedImage animatedImageWithGIFData:imageData]方法初始化gif并使用.
  • 注意:[FLAnimatedImage animatedImageWithGIFData:imageData]方法耗時(shí)比較長(zhǎng), 本模塊又是在主線(xiàn)程做這個(gè)操作, 假如下載Gif的同時(shí), 用戶(hù)在進(jìn)行UI操作, 比如滑動(dòng)頁(yè)面等會(huì)造成掉幀, 可以將這一步丟到后臺(tái)線(xiàn)程完成, 完成后在主線(xiàn)程進(jìn)行展示. 這樣做的下一個(gè)問(wèn)題是, 下載GIF是在后臺(tái), SD下載完成回調(diào)丟回主線(xiàn)程, 在主線(xiàn)程丟到后臺(tái)去生成一個(gè)FLAnimatedImage實(shí)例, 再回到主線(xiàn)程進(jìn)行展示, 中間本不該回到主線(xiàn)程造成資源浪費(fèi). 前面說(shuō)了, 這個(gè)模塊式通過(guò)調(diào)用[UIView sd_setImageWithURL:placeholderImage:options:progress:completed:]來(lái)完成下載操作, 我們可以自己調(diào)用該方法(不需要引入SDWebImage/GIF子模塊). 例子如下(self.gifImageView 是一個(gè)FLAnimatedImageView實(shí)例):
#import "NSData+ImageContentType.h"
#import "UIView+WebCache.h"

[self.gifImageView sd_internalSetImageWithURL:[NSURL URLWithString:self.resource.previewImageUrl]
                             placeholderImage:nil
                                      options:0
                                 operationKey:nil
                                setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData) {
                                    SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:imageData];
                                    if (imageFormat == SDImageFormatGIF) {
                                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                            FLAnimatedImage *animatedImage = [FLAnimatedImage animatedImageWithGIFData:imageData];;
                                            dispatch_async(dispatch_get_main_queue(), ^{
                                                weakSelf.gifImageView.animatedImage = animatedImage;
                                            });
                                        });
                                        weakSelf.gifImageView.image = nil;
                                    } else {
                                        weakSelf.gifImageView.image = image;
                                        weakSelf.gifImageView.animatedImage = nil;
                                    }
                                }
                                     progress:nil completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
                                     }];

3. 小結(jié)

這個(gè)模塊被應(yīng)該做三件事情, 一件是下載, 這個(gè)在下載模塊完成了; 第二個(gè)是從下載下來(lái)的二進(jìn)制文件中生成一張圖片, 這個(gè)在UIImage+MultiFormat模塊中完成的, 有興趣的同學(xué)可以看看這個(gè)文件; 第三個(gè)是展示二進(jìn)制文件, 這個(gè)是FLAnimatedImage做的.


寫(xiě)在最后

最近想看一下一些優(yōu)秀的開(kāi)源庫(kù)是如何編寫(xiě)的, SDWebImage是我看的第一份源碼(以前草草看的不算), 受益匪淺. 這次我邊看邊寫(xiě)筆記, 最終整理這篇博客, 不僅僅是對(duì)源碼的流程講解, 有一些小的細(xì)節(jié)小技巧我也有單獨(dú)標(biāo)出來(lái). 平時(shí)碼代碼的過(guò)程還是太隨意了, 因?yàn)楣こ痰牧考?jí)決定不需要太注重一些細(xì)節(jié), 但是對(duì)于這些細(xì)節(jié), 能注意的還是應(yīng)該注意.

作者:wyanassert

原地址:SDWebImage4.0 分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 一個(gè)異步下載圖片并且支持緩存的UIImageView分類(lèi).參考文章(https://github.com/Drav...
    心里的另一個(gè)你閱讀 714評(píng)論 0 1
  • 下載 下載管理器 SDWebImageDownLoader作為一個(gè)單例來(lái)管理圖片的下載操作。圖片的下載是放在一個(gè)N...
    wind_dy閱讀 1,660評(píng)論 0 1
  • 圖片下載的這些回調(diào)信息存儲(chǔ)在SDWebImageDownloader類(lèi)的URLOperations屬性中,該屬性是...
    怎樣m閱讀 2,667評(píng)論 0 1
  • 技術(shù)無(wú)極限,從菜鳥(niǎo)開(kāi)始,從源碼開(kāi)始。 由于公司目前項(xiàng)目還是用OC寫(xiě)的項(xiàng)目,沒(méi)有升級(jí)swift 所以暫時(shí)SDWebI...
    充滿(mǎn)活力的早晨閱讀 12,832評(píng)論 0 2
  • 項(xiàng)目中一直都有使用SDWebImage,對(duì)這個(gè)框架有一定的了解,但是體系卻未能貫通,因此特地整理下,主要參考: i...
    林大鵬閱讀 1,656評(píng)論 2 13

友情鏈接更多精彩內(nèi)容