SDWebImage源碼分析 1

前言

開發iOS有一段時間了,平時工作中主要還是完成業務功能。類似網絡請求,圖片加載等等都直接使用現成的開源類庫,項目主要還是以穩定為先。
但長期這樣感覺難以進步,想要進階除了看書外就得多看看開源類庫的源碼了。
于是就從SDWebImage入手,在深入學習后發現它的代碼各層職責分工明確,代碼量也不是很多,利用業余時間斷斷續續學習花費了大約三周時間,感覺比較適合作為第一個供學習的開源類庫。

大致涉及到的知識點:

  • Block
  • GCD
  • NSOperation
  • Associated Objects
  • NSURLRequest
  • NSCache
  • 圖片類型識別與處理

文章中難免出現問題,望各位給予糾正,有問題歡迎一起討論。

源碼分析

SDWebImage使用起來非常簡單,只需調用sd_setImageWithURL方法,就可以將圖片異步的加載并顯示在UIImageView上。

所以接下來我們就從sd_setImageWithURL開始說起:

NSURL * url = [NSURL URLWithString:@"http://hbimg.b0.upaiyun.com/ddd2cee8ff21d4a09a86b68972b78b15ba7bc2a035fa4-sGYzEJ_fw658"];
[imageView sd_setImageWithURL:url];

上面代碼所使用的是sd_setImageWithURL最簡單的版本,我們跟進去看一下,發現方法里其實幫我們設置好了默認參數,最終調用到的是另一個方法:

- (void)sd_setImageWithURL:(NSURL *)url {
    [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil];
}

我們跟進去看看,通過注釋可以得知這個方法的用途:

/**
 * ?根據url給imageView設置image,占位圖和各種自定義設置
 *
 * 使用異步下載和緩存
 *
 * @param url            圖片的url
 * @param placeholder    占位圖
 * @param options        下載圖片時的各種設置. @see SDWebImageOptions.
 * @param progressBlock  當圖片正在下載時會被回調到
 * @param completedBlock 當任務完成時會被回調到 。該block沒有返回值使用UIImage作為第一個參數
 *                       如果下載中出現錯誤UIIMage為nil并且第二個參數會包含NSError
 *                       第三個參數是一個枚舉(*原注釋這塊寫的是布爾值),表示圖片是從本地緩存中還是網絡中取回的
 *                       第四個參數是原生的image url
 */
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

//completedBlock,參數與注釋對應
typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL);

接著我們看代碼,然后一步步分析:

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
    //取消當前UIImageView正在加載的圖片任務
    [self sd_cancelCurrentImageLoad];

    //相當于給當前UIImageView對象上綁定圖片url屬性
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    //如果options中沒有傳入SDWebImageDelayPlaceholder參數,則設置占位圖
    //這里出現了dispatch_main_async_safe,其實是SDWebImage定義的宏,其實就是將UI操作放入主線程中用的
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }

    if (url) {

        // 檢查是否打開了"會轉動的菊花"選項
        if ([self showActivityIndicatorView]) {
            [self addActivityIndicator]; //< 界面上會出現轉動的菊花
        }

        __weak __typeof(self)wself = self;
        //從方法名中可以猜出它是用來下載圖片用的,目前只需要這么理解就好,后面章節會具體談到
        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添加進UIViewImage對象,而是放置到completedBlock中交由調用方自己處理,比如做個濾鏡或者添加淡出淡入效果什么的
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
                {
                    completedBlock(image, error, cacheType, url);
                    return;
                }
                else if (image) {
                    wself.image = image; //< 設置image
                    [wself setNeedsLayout];
                } else { //< 當image為nil
                    if ((options & SDWebImageDelayPlaceholder)) {
                        wself.image = placeholder;//< 此時再將占位圖設置進去
                        [wself setNeedsLayout];
                    }
                }
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        //保存本次operation,如果發生多次圖片請求加載可以用來取消
        //先取消當前UIImageView正在加載的任務,再保存operation
        [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
    } else {
        dispatch_main_async_safe(^{
            [self removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

這里先提幾個點:
1.在代碼中我們會發現有dispatch_main_async_safe這么一個神奇的東東,其實它是SDWebImage定義的宏,將UI操作放入主線程中用的:

#define dispatch_main_async_safe(block)
     if ([NSThread isMainThread]) { //< 如果當前在主線程中
         block();
     } else { //< 不在主線程就將它放入主線程
         dispatch_async(dispatch_get_main_queue(), block);
     }

2.代碼中偶爾會出現objc_setAssociatedObject,簡單的說使用該技巧可以很方便的將變量動態綁定在該實例下,原因在于Category中是不允許添加實例變量。

回到主題來,代碼在請求下載圖片前執行了[self sd_cancelCurrentImageLoad],從方法名上可以猜出它的大意“取消當前圖片的加載”,他是作什么用的呢,為什么在加載圖片前會需要用到取消這么一個方法?帶著疑問我們繼續,發現調用了另一個方法,看來這里只負責傳入對應的“key”

- (void)sd_cancelCurrentImageLoad {
    [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"];
}

再跟進來我們可以看到具體的實現了

- (void)sd_cancelImageLoadOperationWithKey:(NSString *)key {
    //利用AssociatedObject維護的字典,用于存放當前任務中的operation(圖片請求)
    NSMutableDictionary *operationDictionary = [self operationDictionary];
    //key為"UIImageViewImageLoad"
    id operations = [operationDictionary objectForKey:key];

    if (operations) { //< 當前有正在執行的operation,需要取消任務
        //多個operation的是gif(多幀),單個的是普通圖片
        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的對象
        //每次對應UIView有一個圖片請求的任務時,都會設置對應的key,所以可以根據這個key來判斷是否有正在執行的任務
        [operationDictionary removeObjectForKey:key];
    }
}

看完上面這段代碼后,我們大致有了一個概念,同時也發現這兩段代碼的“key”是一樣的:

//取消當前UIImageView正在加載的圖片任務
[self sd_cancelCurrentImageLoad];
...
//保存本次operation,如果發生多次圖片請求加載可以用來取消
//先取消當前UIImageView正在加載的任務,再保存operation
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];

再回到剛才的疑問,舉個例子來說就能明白方法的意圖和具體流程了:

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 50, 50)];
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.example.com/1.png"] placeholderImage:nil];
[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.example.com/2.png"] placeholderImage:nil];

一個imageView請求了兩張圖片,1.png 和 2.png,但我們只希望顯示 2.png,所以需要取消 1.png的請求。原因有兩點:
1.在異步請求中(先后順序不定),有可能 1.png 會在 2.png 后面獲取到,會覆蓋掉2.png
2.減少網絡請求,網絡請求是一個很耗時的操作

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 172,662評論 25 708
  • 技術無極限,從菜鳥開始,從源碼開始。 由于公司目前項目還是用OC寫的項目,沒有升級swift 所以暫時SDWebI...
    充滿活力的早晨閱讀 12,675評論 0 2
  • 發現 關注 消息 iOS 第三方庫、插件、知名博客總結 作者大灰狼的小綿羊哥哥關注 2017.06.26 09:4...
    肇東周閱讀 12,153評論 4 61
  • 今天我們來聊聊“眼光”吧…… 話說,在中國,家喻戶曉,被認可最有眼光,最有影響力的商人,非馬云是也。 大家認同嗎?...
    曹陽CY閱讀 270評論 0 0
  • 上一章:大漢王朝(下) [前言] 近來,項目的事情基本成型,業已走向正規...
    獨孤一鳴閱讀 683評論 56 41