天天看點

SDWebImage 源碼閱讀(一)

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