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. 下載模塊
- 通過
[SDWebImageDownloader sharedImageCache]
引用到了 SDWebImageDownloader 的單例.
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都移除掉之后, 會調用父類NSOperation
的cancel
方法, 這會將isCancelled
屬性置為YES, 在start方法調用的時候就不會真正執(zhí)行. 最后調用[self.dataTask cancel];
關閉數據傳輸.
Question: 手動調cancel方法后, 就不會執(zhí)行失敗的block了嗎?
e. 暫停下載
- 直接調用
(self.downloadQueue).suspended = suspended;
, 這兒利用了NSOperationQueue
的功能.
f. 取消所有的下載
[self.downloadQueue cancelAllOperations];
- 自動調用
SDWebImageDownloaderOperation
的cancel
方法.
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
- 在這兒將
progressBlock
和completedBlock
兩個block都復制了一份,再存儲到_callbackBlocks
中. - 在這兒也使用了
dispatch_barrier_async
方法, 這是個異步操作
c. 開始(繼承父類)
- 注意, 這兒雖然覆蓋了父類的
start
方法, 但是不能調用[super start]; - 在
SDWebImage
中, Operation被加到SDWebImageDownloader
的downloadQueue
中后會被自動執(zhí)行, (自動調用operation
的start
方法) - 首先判斷自己是否被取消了
- 再判斷
self.unownedSession
是否還在, 一般情況下是還在的, 因為默認的SDWebImageDownloader
是個單例不會被釋放, 但如果開發(fā)者自己初始化一個SDWebImageDownloader
就會存在self.unownedSession
不再引用一個session的情況. - 根據
session
和request
生成一個dataTask
, 并將自己標記為正在執(zhí)行. - 開始下載, 并觸發(fā)第一次'progressBlock'.
d. 取消(繼承父類)
- 若已經完成, 直接返回.
- 調用[super cancel], 會將
isFinished
標記為YES. - 取消下載操作.
e. 是否在執(zhí)行(繼承父類)
- 用KVO通知值改變.
f. 是否已結束(繼承父類)
- 同上.
g. 取消單個操作
- 根據token將對應的回調從``刪除, 在這兒使用了
[NSArray removeObjectIdenticalTo:]
方法, 利用"本體性"而不是"相等性"去移除對應的回調, 個人猜測是為了提高查找的速度. 具體可以參考Equality這篇文章. - 在這兒使用了
dispatch_barrier_sync
, 注意這兒是一個同步方法, 后面根據移除后_callbackBlocks
是否為空判斷是否要停止當前的下載.
Tips: start
與cancel
用@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 分析