1. 前言
一直沒有系統的閱讀過整套源碼,僅僅是看過幾篇總結的文章,了解了設計思路,年前項目剛結束,終于有大塊時間仔細閱讀一下 SDWebImage 架構了。開始閱讀的時候感覺很簡單,也沒有添加自己的注釋,最後越看越亂,發現很多細節處理和邏輯完整性和我之前自己思考的不太一樣,就這樣亂糟糟的看了一下午,感覺這樣通讀的效果不是很好。今天換了一種思維來了解,看别人的源碼和做英語閱讀一樣,先籠統的浏覽一遍,然後帶着問題閱讀,效果特别好。簡單看了一遍,記錄一下學習筆記。
2. 概況
首先看 UIImageView+WebCache 這個擴充類,給 imageView 添加圖檔的方法一共有以下幾種:
- (void)sd_setImageWithURL:(NSURL *)url;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
- (void)sd_setImageWithURL:(NSURL *)url completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
- (void)sd_setImageWithPreviousCachedImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;
以上方法最後都會調用下面的方法:
也就可以确定,這個方法就是我們閱讀源碼的入口了。大概知道這些參數都是什麼意思:
- url 和 placeholder 不用再介紹了
- SDWebImageOptions 應該是枚舉,進入所在類檢視,是位掩碼,每個字段含義不明确,後面再分析
typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
...
};
- SDWebImageDownloaderProgressBlock 表示正在下載下傳的時候所要處理的事情
- SDWebImageCompletionBlock 表示下載下傳完成後所要做的事
3. 詳解
接下來看看方法裡面的具體實作:
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
// 先将原來的 operation 停止(詳情見 3.1)
[self sd_cancelCurrentImageLoad];
// 給 imageView 關聯要下載下傳的圖檔的url
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// SDWebImageDelayPlaceholder : 占位圖延遲加載辨別
// 預設情況下,占位圖先指派給imageView,然後加載圖像。這個标志是圖像加載完畢之前占位圖不先指派給imageVIew。
// 在沒有發送請求擷取網絡端圖檔之前、如果 options 不等于 SDWebImageDelayPlaceholder
if (!(options & SDWebImageDelayPlaceholder)) {
// 剛看到這裡懵逼了一下,印象中沒有 dispatch_main_async_safe 這個方法啊,進去發現是一個宏定義,目的是UI相關操作要在主線程,沒啥技術點,略過
dispatch_main_async_safe(^{
self.image = placeholder;
});
}
// 如果 url 存在
if (url) {
// setShowActivityIndicatorView: 暴露在頭檔案中,設定加載圖檔的時候是否在 imageView 中央添加菊花
// showActivityIndicatorView : 是 setShowActivityIndicatorView 的 get 方法、傳回 BOOL
// 此狀态是用 runtime 關聯到 imageView 上的
// check if activityView is enabled or not
// 是否添加菊花
if ([self showActivityIndicatorView]) {
// 添加菊花
[self addActivityIndicator];
}
__weak __typeof(self)wself = self;
// 下載下傳圖檔(詳情見 3.2)
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
// 圖檔加載完成、移除菊花
[wself removeActivityIndicator];
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
// SDWebImageAvoidAutoSetImage : 不允許自動将 image 添加到 imageView
// 預設情況下,圖像下載下傳後直接添加到 imageView,但是在某些情況下,我們希望在設定圖像之前先有互動(例如應用濾鏡或添加交叉淡入淡出動畫),如果要在成功完成時手動設定圖像,請使用此标志.
// 如果圖檔下載下傳成功 并且 (options = SDWebImageAvoidAutoSetImage)
// 這個就是我之前常用的方法,獲得 image 再賦予 button,現在才發現有 UIButton+WebCache 擴充類
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
{
// 直接傳回 image,不添加到 imagView
completedBlock(image, error, cacheType, url);
return;
}
else if (image) {
// 如果image擷取成功,直接添加 image 到 imageView
wself.image = image;
[wself setNeedsLayout];
} else {
// 如果 image擷取失敗,并且 (options = SDWebImageDelayPlaceholder),才将 placeholder 添加到 imageView,赤裸裸的備胎
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
// 将正在下載下傳的 operation 添加到 operation 字典中(詳情見 3.3)
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else { // 如果 url 為空,則抛出錯誤
dispatch_main_async_safe(^{
// 移除菊花
[self removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:- userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}
3.1. 将原來的 operation 停止
如果剛剛已經請求了圖檔,當 imageView 再次加載圖檔之前,需要取消之前的請求。
進入 sd_cancelCurrentImageLoad 方法
- (void)sd_cancelCurrentImageLoad {
[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}
再進入 sd_cancelImageLoadOperationWithKey: 方法
- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
// Cancel in progress downloader from queue
// 取消正在進行的下載下傳隊列
// 擷取正在下載下傳的隊列
NSMutableDictionary *operationDictionary = [self operationDictionary];
// 擷取 @"UIImageViewImageLoad" 對應的 value
id operations = [operationDictionary objectForKey:key];
if (operations) {
// 如果 value 是數組類型
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];
}
// 最後移除key對應的value
[operationDictionary removeObjectForKey:key];
}
}
代碼很容易了解,先擷取到 operation 的序列,即 [self operationDictionary]。然後根據 key 索引到對應的 operation,如果 operation 存在,就要取消該 operation。這裡需要注意的地方就是,索引到的 operation 其實是一組 operation 集合,那麼久需要周遊一個個取消序列中的 operation。最後移除 key 對應的 object。
這裡有個疑惑:為啥 operation 都是 id ?而且進入 SDWebImageOperation 後發現隻有一個 cancel 方法。為什麼這樣設計,還有待進一步研究。
3.2. 下載下傳圖檔
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
...
}];
進入後發現這是 SDWebImageManager 的方法,這就是我們繼續探索源碼核心的下一個入口,這裡不做過多介紹,我們将在下一篇中重點介紹。
3.3. 添加新的operation 到隊列
進入 sd_setImageLoadOperation: forKey: 方法
- (void)sd_setImageLoadOperation:(id)operation forKey:(NSString *)key {
// 取消正在進行的下載下傳隊列
[self sd_cancelImageLoadOperationWithKey:key];
NSMutableDictionary *operationDictionary = [self operationDictionary];
[operationDictionary setObject:operation forKey:key];
}
看到方法内的實作,就感覺沒有什麼難度了,sd_cancelImageLoadOperationWithKey: 方法在 3.1 中介紹了,接下來将該 operation 添加到下載下傳隊列中。
4. 總結
到此為止,這個類中主要的方法、屬性都解析出來了。其他的比如運用 runtime 關聯屬性等等就不一一介紹了,都比較簡單。這個擴充類其實沒有太多核心思想,主要就是設計了嚴密的判斷,保證運用該架構的其他開發者能夠在各種環境下請求圖檔。遺留問題就是 SDWebImageManager 下載下傳圖檔方法内的實作,我們将在下一篇着重介紹。
SDWebImage 源碼解析 github 位址:https://github.com/Mayan29/SDWebImageAnalysis