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. 下載模塊
- 通過(guò)
[SDWebImageDownloader sharedImageCache]引用到了 SDWebImageDownloader 的單例.
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)NSOperation的cancel方法, 這會(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)用
SDWebImageDownloaderOperation的cancel方法.
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
- 在這兒將
progressBlock和completedBlock兩個(gè)block都復(fù)制了一份,再存儲(chǔ)到_callbackBlocks中. - 在這兒也使用了
dispatch_barrier_async方法, 這是個(gè)異步操作
c. 開(kāi)始(繼承父類(lèi))
- 注意, 這兒雖然覆蓋了父類(lèi)的
start方法, 但是不能調(diào)用[super start]; - 在
SDWebImage中, Operation被加到SDWebImageDownloader的downloadQueue中后會(huì)被自動(dòng)執(zhí)行, (自動(dòng)調(diào)用operation的start方法) - 首先判斷自己是否被取消了
- 再判斷
self.unownedSession是否還在, 一般情況下是還在的, 因?yàn)槟J(rèn)的SDWebImageDownloader是個(gè)單例不會(huì)被釋放, 但如果開(kāi)發(fā)者自己初始化一個(gè)SDWebImageDownloader就會(huì)存在self.unownedSession不再引用一個(gè)session的情況. - 根據(jù)
session和request生成一個(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: start與cancel用@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 分析