SDWebImage 分析

SDWebImage 分析

Version 4.0.0


導航

按照模塊分析 SDWebImage

1. UI交互的基類 UIView+WebCache

2. SDWebImage 的主要管理者 SDWebImageManager

3. 緩存模塊 SDImageCache

4. 下載模塊 SDWebImageDownloader

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

6. 預加載 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. 下載已經緩存過的圖片

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

c. 下載動圖

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

d. 取消下載動圖

- (void)sd_cancelCurrentAnimationImagesLoad;

2. 分析

a. 下載圖片

下載圖片有數個方法定義, 見UIView+WebCache.h, 最終都調用了UIView+WebCache中的 sd_internalSetImageWithURL 這個方法.

sd_internalSetImageWithURL 方法做的事情:

  • 首先取消了當前 View 所綁定的一切請求.
  • 設置placeholder.
  • 調用SDWebImageManager下載圖片, 并將方法返回的 operation 與當前 View 綁定.
  • 下載圖片回調處理.

Tips
dispatch_main_async_safe 定義了在主線程進行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 時候直接結束避免崩潰或者其他錯誤.

b. 下載已經緩存過的圖片

  • 首先調用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í)行下載的過程中, 如果找到了緩存, 就忽略placeholder, 避免一次無效操作.

c. 下載動圖

  • 取消當前View 綁定的動圖下載操作.
  • 遍歷傳入的URL數組, 對每個URL調用SDWebImageManager下載圖片, 并將方法返回的 operation 裝入數組,再將這個數組與當前 View 綁定.

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

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 為實例增加了變量.

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

d. 取消下載動圖

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

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

3. 小結

UIView+WebCache 模塊中, 只做了一些簡單的操作, 定義好了與 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. 手動設置圖片緩存

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

e. 取消所有的操作

- (void)cancelAll;

f. 當前是否有操作在運行

- (BOOL)isRunning;

g. 異步檢查圖片是否已經被緩存

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

h. 異步檢查圖片是否已經被緩存在了磁盤上

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

i. 獲取URL緩存索引的Key

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

2. 分析

a. 緩存模塊

  • 通過 [SDImageCache sharedImageCache] 引用到了 SDImageCache 的單例.

b. 下載模塊

c. 下載圖片

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

d. 手動設置圖片緩存

  • 直接調用[SDImageCache storeImage:forKey:toDisk:completion:]

e. 取消所有的操作

  • 因為涉及到 self.runningOperations 的讀寫, 因此用 @synchronized 保證線程安全.
  • copy 了一份self.runningOperations, 并使用 [NSArray makeObjectsPerformSelector:] 方法取消隊列中所有的操作.
  • 將復制的隊列中的所有元素從self.runningOperations移除.

f. 當前是否有操作在運行

  • 為保證原子性,使用@synchronized訪問self.runningOperations

g. 異步檢查圖片是否已經被緩存

  • 分別調用[SDImageCache imageFromMemoryCacheForKey:][SDImageCache diskImageExistsWithKey:completion:] 方法檢查是否在內存和硬盤上緩存.
  • 由于檢查硬盤是否緩存要用到專門的IO線程(在SDImageCache中定義), 調用者不可能去等待IO線程,因此此方法被設計為異步方法.

h. 異步檢查圖片是否已經被緩存在了磁盤上

  • 調用[SDImageCache diskImageExistsWithKey:completion:] 方法檢查是是否在硬盤上緩存.

i. 獲取URL緩存索引的Key

  • 如果定義了self.cacheKeyFilter自定義存儲Key,則使用該回調獲取用于緩存索引的Key

3. 小結

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


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. 緩存圖片到硬盤上(只能從IO Queue 調用)

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

c. 檢查圖片是否在硬盤上緩存(只檢查, 不會把圖片加到內存)

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

d. 檢查是否有緩存

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

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

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

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

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

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

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

h. 從內存和硬盤刪除圖片緩存(異步)

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

i. 從內存移除緩存, 選擇是否從硬盤移除

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

j. 清除內存緩存

- (void)clearMemory;

k. 異步清除磁盤緩存

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

l. 異步清除硬盤上已經過期的緩存

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

2. 分析

初始化方法

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

初始化各個屬性:

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

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

生成的一個串行隊列,專用于IO操作. 不再使用的時候應該使用dispatch_release釋放隊列.

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

NSCache是iOS系統(tǒng)提供的緩存類,通過鍵值對對需要緩存的對象作強引用來達到緩存的目的.

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

注意,生成_fileManager在ioQueue中,并且是是一個同步操作, 之后_fileManager都要在ioQueue中進行.

a. 緩存圖片

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

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

b. 緩存圖片到硬盤上(只能從IO Queue 調用)

此方法只能在ioQueue中調用,奇怪的是SDImageCache并沒有暴露ioQueue訪問, 因此, 將此方法暴露在.h文件是沒有意義的.

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

c. 檢查圖片是否在硬盤上緩存(只檢查, 不會把圖片加到內存)

  • 先后用key參數的md5編碼組合路徑找是都存在文件,在用key參數的md5編碼+文件后綴尋找是否存在
  • 在主線程中回調

在某個版本之前, 硬盤緩存沒有文件后綴名, 為了兼容, 要做兩次查找

d. 檢查是否有緩存

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

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

  • 直接在self.memCache讀取, 可能情況是沒有緩存-返回nil, 有緩存但是緩存已經被釋放-返回nil, 或是尋找緩存命中-返回目標圖片.

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

  • 在硬盤上找目標二進制文件,找到后調用UIImage *image = [UIImage sd_imageWithData:data];image = [self scaledImageForKey:key image:image];生成目標圖片,若允許,在內存中緩存該圖片.

理論上來說, 這句話放在ioQueue中執(zhí)行會好一些, 猜測可能是需要同步執(zhí)行

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

  • 直接調用上面兩個方法.

h. 從內存和硬盤刪除圖片緩存(異步)

  • 直接調用下方的方法.

i. 從內存移除緩存, 選擇是否從硬盤移除

  • 直接從內存緩存中移除, (NSCache是線程安全的)
  • 在ioQueue中移除目標文件, 并在主線程回調

j. 清除內存緩存

  • 直接移除self.memCache所有對象

k. 異步清除磁盤緩存

  • 調用下方方法

l. 異步清除硬盤上已經過期的緩存

  • 遍歷緩存目錄下每一個文件, 獲取其屬性, 若獲取失敗或是該文件是文件夾則跳過
  • 若文件的修改時間早于設定時間,則將文件地址加入待刪除列表.
  • 遍歷結束, 從硬盤移除待刪除列表中的文件.
  • 計算未刪除的文件的總大小, 若仍大于目標大小, 進一步刪除最早改動的文件.

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

3. 小結

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

SDWebImage下載模塊 -- SDWebImageDownloader

1. 接口定義

a. 初始化

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

b. 設置請求的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 是一個NSOperationQueue實例, 每個URL的請依賴這個queue進行管理.
  • _URLOperations 用于存儲所有的operation實例, 每個URL對應一個operation.
  • _barrierQueue 是一個并行隊列, operation的創(chuàng)建于取消都在這個隊列中完成.
  • self.session 是用于網絡請求的NSURLSession組件, 所有的operation對這個session保持了弱引用.

b. 設置請求的Header

c. 下載圖片

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

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

d. 取消下載

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

Tips : [SDWebImageDownloaderOperation cancel:]首先將token對應的callback移除掉. 當所有的callbacl都移除掉之后, 會調用父類NSOperationcancel方法, 這會將isCancelled屬性置為YES, 在start方法調用的時候就不會真正執(zhí)行. 最后調用[self.dataTask cancel];關閉數據傳輸.

Question: 手動調cancel方法后, 就不會執(zhí)行失敗的block了嗎?

e. 暫停下載

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

f. 取消所有的下載

  • [self.downloadQueue cancelAllOperations];
  • 自動調用SDWebImageDownloaderOperationcancel方法.

3. 小結

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

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


1. 接口定義

a. 初始化

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

b. 存儲回調Block

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

c. 開始(繼承父類)

- (void)start;

d. 取消(繼承父類)

- (void)cancel;

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

- (void)setFinished:(BOOL)finished;

f. 是否已結束(繼承父類)

- (void)setExecuting:(BOOL)executing;

g. 取消單個操作

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

2. 分析

a. 初始化

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

b. 存儲回調Block

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

c. 開始(繼承父類)

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

d. 取消(繼承父類)

  • 若已經完成, 直接返回.
  • 調用[super cancel], 會將isFinished標記為YES.
  • 取消下載操作.

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

  • 用KVO通知值改變.

f. 是否已結束(繼承父類)

  • 同上.

g. 取消單個操作

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

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

3. 小結

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

SDWebImage 預加載 -- SDWebImagePrefetcher


1. 接口定義

1. 初始化

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

2. 執(zhí)行預加載

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

3. 取消

- (void)cancelPrefetching;

2. 分析

1. 初始化

  • 默認的init方法生成了一個新的SDWebImageManager實例, 在這兒使用了[SDWebImageManager new], 調用的是SDWebImageManager的方法默認初始化方法. 因此, 這兒的manager和[SDWebImageManager sharedManager]不是一個實例, 但是由于SDWebImageManager的默認初始化方法中使用的[SDImageCache sharedImageCache][SDWebImageDownloader sharedDownloader]單例, 所以在這兒初始化的manager和[SDWebImageManager sharedManager]共享的同一個_imageCache_imageDownloader實例.
  • 舉個例子, 在我寫的一個測試程序中, [SDWebImageManager sharedManager]的內存地址是0x61000107f280, 而SDWebImagePrefetcher所持有的manager地址是0x000060000106ef00, 他們不是同一個manager, 當我打印[SDWebImageManager sharedManager]的各個屬性時候, 如下方結果, _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í)行預加載

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

3. 取消

  • 情況當前所有的記錄, 并使用持有的SDWebImageManager結束正在下載的任務.

3. 小結

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

SDWebImage 子模塊 GIF -- FLAnimatedImage

SDWebImage 支持動態(tài)圖的, 建立在Flipboard的開源項目FLAnimatedImage的基礎之上, 增加的一個擴展, 使用方法是pod 'SDWebImage/GIF', 或者手動把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

  • 直接調用[UIView sd_setImageWithURL:placeholderImage:options:progress:completed:]方法,這個方法在最開始對UIView+WebCache模塊有介紹.
  • 回調結果中有UIImage *image, NSData *imageData, 如果imageData是Gif, 使用[FLAnimatedImage animatedImageWithGIFData:imageData]方法初始化gif并使用.
  • 注意:[FLAnimatedImage animatedImageWithGIFData:imageData]方法耗時比較長, 本模塊又是在主線程做這個操作, 假如下載Gif的同時, 用戶在進行UI操作, 比如滑動頁面等會造成掉幀, 可以將這一步丟到后臺線程完成, 完成后在主線程進行展示. 這樣做的下一個問題是, 下載GIF是在后臺, SD下載完成回調丟回主線程, 在主線程丟到后臺去生成一個FLAnimatedImage實例, 再回到主線程進行展示, 中間本不該回到主線程造成資源浪費. 前面說了, 這個模塊式通過調用[UIView sd_setImageWithURL:placeholderImage:options:progress:completed:]來完成下載操作, 我們可以自己調用該方法(不需要引入SDWebImage/GIF子模塊). 例子如下(self.gifImageView 是一個FLAnimatedImageView實例):
#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. 小結

這個模塊被應該做三件事情, 一件是下載, 這個在下載模塊完成了; 第二個是從下載下來的二進制文件中生成一張圖片, 這個在UIImage+MultiFormat模塊中完成的, 有興趣的同學可以看看這個文件; 第三個是展示二進制文件, 這個是FLAnimatedImage做的.


寫在最后

最近想看一下一些優(yōu)秀的開源庫是如何編寫的, SDWebImage是我看的第一份源碼(以前草草看的不算), 受益匪淺. 這次我邊看邊寫筆記, 最終整理這篇博客, 不僅僅是對源碼的流程講解, 有一些小的細節(jié)小技巧我也有單獨標出來. 平時碼代碼的過程還是太隨意了, 因為工程的量級決定不需要太注重一些細節(jié), 但是對于這些細節(jié), 能注意的還是應該注意.

作者:wyanassert

原地址:SDWebImage4.0 分析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末邦尊,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件有滑,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機役听,發(fā)現死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人典予,你說我怎么就攤上這事甜滨。” “怎么了瘤袖?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵衣摩,是天一觀的道長。 經常有香客問我捂敌,道長艾扮,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任黍匾,我火速辦了婚禮栏渺,結果婚禮上,老公的妹妹穿的比我還像新娘锐涯。我一直安慰自己磕诊,他們只是感情好,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布纹腌。 她就那樣靜靜地躺著霎终,像睡著了一般。 火紅的嫁衣襯著肌膚如雪升薯。 梳的紋絲不亂的頭發(fā)上莱褒,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音涎劈,去河邊找鬼广凸。 笑死,一個胖子當著我的面吹牛蛛枚,可吹牛的內容都是我干的谅海。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼蹦浦,長吁一口氣:“原來是場噩夢啊……” “哼扭吁!你這毒婦竟也來了?” 一聲冷哼從身側響起盲镶,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤侥袜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后溉贿,有當地人在樹林里發(fā)現了一具尸體枫吧,經...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年宇色,在試婚紗的時候發(fā)現自己被綠了由蘑。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片闽寡。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖尼酿,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情植影,我是刑警寧澤裳擎,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站思币,受9級特大地震影響鹿响,放射性物質發(fā)生泄漏。R本人自食惡果不足惜谷饿,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一惶我、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧博投,春花似錦绸贡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至虑绵,卻和暖如春尿瞭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背翅睛。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工声搁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人捕发。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓疏旨,卻偏偏與公主長得像,于是被迫代替她去往敵國和親爬骤。 傳聞我的和親對象是個殘疾皇子充石,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

推薦閱讀更多精彩內容