SDWebImage源碼解析(一)

1孝宗、概述

SDWebImage基本是iOS項目的標配穷躁。他以靈活簡單的api,提供了圖片從加載因妇、解析问潭、處理、緩存婚被、清理等一些列功能狡忙。讓我們專心于業(yè)務的處理。但是并不意味著會用就可以了址芯,通過源碼分析和學習灾茁,讓我們知道如何用好它。學習分析優(yōu)秀源碼也可以從潛移默化中給我們提供很多解決日常需求的思路谷炸。下面就是一張圖來概述SDWebImage的所有類:

類關系圖.png

通過對這個圖片的分析北专,我們可以把SDWebImage的源碼分為三種:

  • 各種分類:
    • UIButton(WebCache)UIButton類添加載圖片的方法。比如正常情況下旬陡、點擊情況下拓颓、的image屬性和背景圖片等。
    • MKAnnotationView(WebCache)MKAnnotationView類添加各種加載圖片的方法描孟。
    • UIImageView(WebCache)UIImageView類添加加載圖片的方法驶睦。
    • UIImageView(HighlightedWebCache)為UIImageView`類添加高亮狀態(tài)下加載圖片的方法。
    • FLAnimatedImageView(WebCache)FLAnimatedImageView類添加加載動態(tài)的方法,這個分類需要引入FLAnimatedImage框架匿醒。SDWebImage推薦使用這個框架來處理動態(tài)圖片(GIF)的加載场航。
    • UIImageViewUIButton廉羔、FLAnimatedImageView通過sd_setImageWithURL等api來做圖片加載請求溉痢。這也是我們唯一需要做的。
    • 上面的幾個UIView子類都會調(diào)用UIView(WebCache)分類的sd_internalSetImageWithURL方法來做圖片加載請求。具體是通過SDWebImageManager調(diào)用來實現(xiàn)的适室。同時實現(xiàn)了 Operation取消嫡意、ActivityIndicator的添加與取消举瑰。
  • 各種工具類:
    • NSData+ImageContentType: 根據(jù)圖片數(shù)據(jù)獲取圖片的類型捣辆,比如GIF、PNG等此迅。
    • SDWebImageCompat: 根據(jù)屏幕的分辨倍數(shù)成倍放大或者縮小圖片大小汽畴。
    • SDImageCacheConfig: 圖片緩存策略記錄。比如是否解壓縮耸序、是否允許iCloud忍些、是否允許內(nèi)存緩存、緩存時間等坎怪。默認的緩存時間是一周罢坝。
    • UIImage+MultiFormat: 獲取UIImage對象對應的data、或者根據(jù)data生成指定格式的UIImage搅窿,其實就是UIImage和NSData之間的轉換處理嘁酿。
    • UIImage+GIF: 對于一張圖片是否GIF做判斷∧杏Γ可以根據(jù)NSData返回一張GIF的UIImage對象闹司,并且只返回GIF的第一張圖片生成的GIF。如果要顯示多張GIF沐飘,使用FLAnimatedImageView游桩。
    • SDWebImageDecoder: 根據(jù)圖片的情況,做圖片的解壓縮處理耐朴。并且根據(jù)圖片的情況決定如何處理解壓縮借卧。
  • 核心類:
    • SDImageCache 負責SDWebImage的整個緩存工作,是一個單列對象筛峭。緩存路徑處理铐刘、緩存名字處理、管理內(nèi)存緩存和磁盤緩存的創(chuàng)建和刪除蜒滩、根據(jù)指定key獲取圖片滨达、存入圖片的類型處理、根據(jù)緩存的創(chuàng)建和修改日期刪除緩存俯艰。
    • SDWebImageManager: 擁有一個SDWebImageCache和SDWebImageDownloader屬性分別用于圖片的緩存和加載處理捡遍。為UIView及其子類提供了加載圖片的統(tǒng)一接口。管理正在加載操作的集合竹握。這個類是一個單列画株。還有就是各種加載選項的處理。
    • SDWebImageDownloader: 實現(xiàn)了圖片加載的具體處理,如果圖片在緩存存在則從緩存區(qū)谓传。如果緩存不存在蜈项,則直接創(chuàng)建一個。 SDWebImageDownloaderOperation對象來下載圖片续挟。管理NSURLRequest對象請求頭的封裝紧卒、緩存、cookie的設置诗祸。加載選項的處理等功能跑芳。管理Operation之間的依賴關系。這個類是一個單列.
    • SDWebImageDownloaderOperation: 一個自定義的并行Operation子類直颅。這個類主要實現(xiàn)了圖片下載的具體操作博个、以及圖片下載完成以后的圖片解壓縮、Operation生命周期管理等功偿。
    • UIView+WebCache: 所有的UIButton盆佣、UIImageView都回調(diào)用這個分類的方法來完成圖片加載的處理。同時通過UIView+WebCacheOperation分類來管理請求的取消和記錄工作械荷。所有 UIView及其子類的分類都是用這個類的sd_intemalSetImageWithURL:來實現(xiàn)圖片的加載共耍。
    • FLAnimatedImageView: 動態(tài)圖片的數(shù)據(jù)通過ALAnimatedImage對象來封裝。-FLAnimatedImageView是UIImageView的子類养葵。通過他完全可以實現(xiàn)動態(tài)圖片的加載顯示和管理征堪。并且比UIImageView做了流程優(yōu)化。

2关拒、實現(xiàn)流程

SDWebImage為我們實現(xiàn)了圖片加載佃蚜、數(shù)據(jù)處理、圖片緩存等一些列工作着绊。通過下圖我們可以分析一下他的流程:

流程圖.png

通過這個圖谐算,我們發(fā)現(xiàn)SDWebImage加載的過程是首先從緩存中加載數(shù)據(jù)。而且緩存加載又是優(yōu)先從內(nèi)存緩存中加載归露,然后才是磁盤加載洲脂。最后如果緩存沒有,才從網(wǎng)絡上加載剧包。同時網(wǎng)絡成功加載圖片以后恐锦,存入本地緩存。

3疆液、UIView+WebCache 一铅、UIView+WebCacheOperation分析

UIImageViewUIButton堕油、FLAnimatedImageView都會調(diào)用UIView(WebCache)分類的sd_internalSetImageWithURL方法來做圖片加載請求潘飘。具體是通過SDWebImageManager調(diào)用來實現(xiàn)的肮之。同時實現(xiàn)了Operation取消、ActivityIndicator的添加與取消卜录。我們首先來看sd_internalSetImageWithURL方法的實現(xiàn):

/**
 《核心方法》
 使用image的url和占位符設置imageView的image

 @param url 圖像的URL
 @param placeholder 初始化的占位圖戈擒,直到url請求完成
 @param options 下載圖片時的選項,詳見 SDWebImageOptions
 @param operationKey 操作的關鍵字艰毒,如果為空筐高,則為類的名字
 @param setImageBlock 設置圖片時的回調(diào)代碼
 @param progressBlock 圖片下載時的回調(diào),注意執(zhí)行是在后臺的隊列
 @param completedBlock 所有操作完成時回調(diào)现喳,不返回值
                         *并將請求的uiimage作為第一個參數(shù)凯傲。如果出現(xiàn)錯誤犬辰,圖像參數(shù)
                         *為零嗦篱,第二個參數(shù)可能包含nserrror。第三個參數(shù)是布爾值
                         *指示是從本地緩存還是從網(wǎng)絡檢索圖像幌缝。
                         *第四個參數(shù)是原始圖像URL灸促。
 @param context 具有執(zhí)行指定更改或進程的額外信息的上下文。
 */
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
             internalSetImageBlock:(nullable SDInternalSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock
                           context:(nullable NSDictionary<NSString *, id> *)context {
    
    /**
     1涵卵、停止所有和這個View相關的Operation浴栽。
     這里還有一個operation 字典:UIView+WebCacheOperation  有一個關聯(lián)對象,用來保存這個View相關的Operation
     @param operationKey 操作的關鍵字轿偎,如果為空典鸡,則為類的名字
     UIView+WebCacheOperation 分類用來保存view相關的操作
     */
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    
    /** 存url
     *  把UIImageView的加載圖片操作和他自身用關聯(lián)對象關聯(lián)起來,方便后面取消等操作坏晦。關聯(lián)的key就是UIImageView對應的類名
     */
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    /** 如果有設置SDWebImageDelayPlaceholder萝玷,則先顯示站位圖 */
    dispatch_group_t group = context[SDWebImageInternalSetImageGroupKey];
    if (!(options & SDWebImageDelayPlaceholder)) {
        if (group) {
            dispatch_group_enter(group);
        }
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
    if (url) {
#if SD_UIKIT
        // check if activityView is enabled or not
        /** 如果UIImageView對象有設置添加轉動菊花數(shù)據(jù),加載的時候添加轉動的菊花 */
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }
#endif
        // reset the progress
        self.sd_imageProgress.totalUnitCount = 0;
        self.sd_imageProgress.completedUnitCount = 0;
        
        SDWebImageManager *manager = [context objectForKey:SDWebImageExternalCustomManagerKey];
        if (!manager) {
            manager = [SDWebImageManager sharedManager];
        }
        
        // 定義完成進度的block回調(diào)
        __weak __typeof(self)wself = self;
        SDWebImageDownloaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            wself.sd_imageProgress.totalUnitCount = expectedSize;
            wself.sd_imageProgress.completedUnitCount = receivedSize;
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };
        id <SDWebImageOperation> operation = [manager loadImageWithURL:url options:options progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
            __strong __typeof (wself) sself = wself;
            if (!sself) { return; }
#if SD_UIKIT
            //停止菊花
            [sself sd_removeActivityIndicator];
#endif
            // if the progress not been updated, mark it to complete state
            /** 如果progress沒有更新昆婿,設置總量和完成量為未知狀態(tài) */
            if (finished && !error && sself.sd_imageProgress.totalUnitCount == 0 && sself.sd_imageProgress.completedUnitCount == 0) {
                sself.sd_imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown;
                sself.sd_imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown;
            }
            
             /** 如果設置了不自動顯示圖片球碉,則直接調(diào)用completedBlock,讓調(diào)用者處理圖片的顯示
              *
              * finished or SDWebImageAvoidAutoSetImage --->應該 調(diào)用CompletedBlock
              */
            BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage);
            BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) ||
                                      (!image && !(options & SDWebImageDelayPlaceholder)));
            
            SDWebImageNoParamsBlock callCompletedBlockClojure = ^{
                if (!sself) { return; }
                if (!shouldNotSetImage) {
                    [sself sd_setNeedsLayout];
                }
                if (completedBlock && shouldCallCompletedBlock) {
                    completedBlock(image, error, cacheType, url);
                }
            };
            // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set
            // OR
            // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set
            /**
             *  得到image and  (SDWebImageAvoidAutoSetImage在completeblock 中設置圖片)---> 不自動顯示
             *  沒得到image and  (默認顯示了SDWebImageDelayPlaceholder占位圖) ----> 不自動顯示仓蛆。
             *  不自動顯示 ---> 代用completedBlock
             */
            if (shouldNotSetImage) {
                dispatch_main_async_safe(callCompletedBlockClojure);
                return;
            }
            /** ?  設置imageview的image */
            UIImage *targetImage = nil;
            NSData *targetData = nil;
            if (image) {
                // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set
                /**
                 *  得到image and 沒有設置SDWebImageAvoidAutoSetImage
                 */
                targetImage = image;
                targetData = data;
            } else if (options & SDWebImageDelayPlaceholder) {
                // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set
                /**
                 *  沒得到image and 沒有設置SDWebImageDelayPlaceholder
                 */
                targetImage = placeholder;
                targetData = nil;
            }
            
#if SD_UIKIT || SD_MAC
            // check whether we should use the image transition
            SDWebImageTransition *transition = nil;
            if (finished && (options & SDWebImageForceTransition || cacheType == SDImageCacheTypeNone)) {
                transition = sself.sd_imageTransition;
            }
#endif
            dispatch_main_async_safe(^{
                if (group) {
                    dispatch_group_enter(group);
                }
#if SD_UIKIT || SD_MAC
                [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL];
#else
                [sself sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL];
#endif
                if (group) {
                    // compatible code for FLAnimatedImage, because we assume completedBlock called after image was set. This will be removed in 5.x
                    BOOL shouldUseGroup = [objc_getAssociatedObject(group, &SDWebImageInternalSetImageGroupKey) boolValue];
                    if (shouldUseGroup) {
                        dispatch_group_notify(group, dispatch_get_main_queue(), callCompletedBlockClojure);
                    } else {
                        callCompletedBlockClojure();
                    }
                } else {
                    callCompletedBlockClojure();
                }
            });
        }];
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
        dispatch_main_async_safe(^{
#if SD_UIKIT
            [self sd_removeActivityIndicator];
#endif
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

還有就是通過UIView+WebCacheOperation類來實現(xiàn)UIView的圖片下載Operation的關聯(lián)和取消睁冬。具體key的值可以從sd_internalSetImageWithURL中找到具體獲取方式,通過在這個方法中實現(xiàn)Operation的關聯(lián)與取消看疙。

/**
 關聯(lián)Operation對象與key對象
 
 @param operation Operation對象
 @param key key
 */
- (void)sd_setImageLoadOperation:(nullable id<SDWebImageOperation>)operation forKey:(nullable NSString *)key {
    if (key) {
        [self sd_cancelImageLoadOperationWithKey:key];
        if (operation) {
            SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
            @synchronized (self) {
                [operationDictionary setObject:operation forKey:key];
            }
        }
    }
}
/**
 取消當前key對應的所有實現(xiàn)了SDWebImageOperation協(xié)議的Operation對象
 
 @param key Operation對應的key
 */
- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        // Cancel in progress downloader from queue
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id<SDWebImageOperation> operation;
        
        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                [operation cancel];
            }
            @synchronized (self) {
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末豆拨,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子能庆,更是在濱河造成了極大的恐慌施禾,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,204評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件相味,死亡現(xiàn)場離奇詭異拾积,居然都是意外死亡殉挽,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,091評論 3 395
  • 文/潘曉璐 我一進店門拓巧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來斯碌,“玉大人,你說我怎么就攤上這事。” “怎么了艇纺?”我有些...
    開封第一講書人閱讀 164,548評論 0 354
  • 文/不壞的土叔 我叫張陵诉字,是天一觀的道長。 經(jīng)常有香客問我裂允,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,657評論 1 293
  • 正文 為了忘掉前任凛辣,我火速辦了婚禮,結果婚禮上职烧,老公的妹妹穿的比我還像新娘扁誓。我一直安慰自己,他們只是感情好蚀之,可當我...
    茶點故事閱讀 67,689評論 6 392
  • 文/花漫 我一把揭開白布蝗敢。 她就那樣靜靜地躺著,像睡著了一般足删。 火紅的嫁衣襯著肌膚如雪寿谴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,554評論 1 305
  • 那天失受,我揣著相機與錄音讶泰,去河邊找鬼。 笑死贱纠,一個胖子當著我的面吹牛峻厚,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播谆焊,決...
    沈念sama閱讀 40,302評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼惠桃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了辖试?” 一聲冷哼從身側響起辜王,我...
    開封第一講書人閱讀 39,216評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎罐孝,沒想到半個月后呐馆,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,661評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡莲兢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,851評論 3 336
  • 正文 我和宋清朗相戀三年汹来,在試婚紗的時候發(fā)現(xiàn)自己被綠了续膳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,977評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡收班,死狀恐怖坟岔,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情摔桦,我是刑警寧澤社付,帶...
    沈念sama閱讀 35,697評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站邻耕,受9級特大地震影響鸥咖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜兄世,卻給世界環(huán)境...
    茶點故事閱讀 41,306評論 3 330
  • 文/蒙蒙 一啼辣、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧碘饼,春花似錦熙兔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,898評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽麸锉。三九已至钠绍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間花沉,已是汗流浹背柳爽。 一陣腳步聲響...
    開封第一講書人閱讀 33,019評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留碱屁,地道東北人磷脯。 一個月前我還...
    沈念sama閱讀 48,138評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像娩脾,于是被迫代替她去往敵國和親赵誓。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,927評論 2 355

推薦閱讀更多精彩內(nèi)容