iOS SDWebImage 源碼分析及架構(gòu)設(shè)計(jì)探索

SDWebImage 常用于對(duì)圖像的下載及緩存等。作者Olivier Poitrey蜻展,法國(guó)視頻分享網(wǎng)站Dailymotion(后被法國(guó)電信運(yùn)營(yíng)商O(píng)range收購(gòu)焊唬,被和諧) 的 CTO媒惕,擁有多個(gè)開(kāi)源項(xiàng)目拼苍。目前SDWebImage獲得巨大成功贬芥,用戶甚眾吐辙,并且在github上已獲取超過(guò)17.7k star。

和筆者另一篇文章 **iOS 李明杰 MJRefresh源碼解析 **類似蘸劈,本文主要素材來(lái)源有正在學(xué)hybrid開(kāi)發(fā)的iOS開(kāi)發(fā)者 J_Knight 的文章昏苏、楊千嬅染了紅頭發(fā) 的博客一行行看SDWebImage源碼(一)一行行看SDWebImage源碼(二)github上作者的用法介紹威沫,再一次表示敬意贤惯。

本文分成兩個(gè)部分對(duì)SDWebImage進(jìn)行介紹上部分從框架層次方面進(jìn)行概述棒掠,主要是筆者在學(xué)習(xí)使用和研究過(guò)程中的心得感悟孵构;下半部分則是對(duì)具體各個(gè)類實(shí)現(xiàn)代碼的詳細(xì)解析因此筆者建議烟很,若是已經(jīng)對(duì)SDWebImage有一定了解的同學(xué)可以看一下上部分颈墅,若是剛開(kāi)始使用此框架的同學(xué)大可略過(guò)第一部分蜡镶,直接從代碼實(shí)現(xiàn)看起,以免被筆者可能不太成熟的見(jiàn)解所誤導(dǎo)恤筛。水平有限官还,還望大家不吝賜教。

第一部分

架構(gòu)設(shè)計(jì)(干貨)

"捫心自問(wèn)"叹俏,假如讓我們自己設(shè)計(jì)一個(gè)具有圖片異步加載并緩存功能的框架妻枕,我們?cè)撛趺丛O(shè)計(jì)僻族。冥想3分鐘粘驰,好好看看這張類圖,好好看看這張類圖述么,好好看看這張類圖蝌数,在這里就先根據(jù)源碼試著反推整個(gè)框架的設(shè)計(jì)思路。


SDWebImage-類圖
SDWebImage-UML序列圖

1. 封裝度秘、繼承顶伞、多態(tài)特性

  • 毫無(wú)疑問(wèn),封裝是單一職責(zé)原則的題中之義剑梳。SDWebImage根據(jù)不同功能進(jìn)行了不同的類的封裝:
職責(zé)
UIView+WebCache 各種視圖的基類唆貌,容納圖像的容器,操作綁定的對(duì)象
SDImageCache垢乙、SDImageCacheConfig 負(fù)責(zé)處理圖像緩存锨咙、配置緩存參數(shù)
SDWebImageDownloader 核心代碼,負(fù)責(zé)圖像的異步下載
**SDWebImageManager ** 綁定顯示圖像的視圖追逮、圖像下載酪刀、圖像緩存3者的管理類,是操作的中樞
SDWebImagePrefetcher 預(yù)加載
SDWebImageDecoder 圖像解壓縮
  • 面向接口編碼钮孵,而非面向?qū)崿F(xiàn)編碼骂倘,繼承中的依賴倒置原則,即基類中定義接口巴席,子類中對(duì)基類接口做具體實(shí)現(xiàn)历涝。例如 SDWebImageUIImageView、UIButton等視圖控件提供圖像或Gif加載功能漾唉,所以在共同基類UIView中提供圖像異步下載等功能荧库。

2. 設(shè)計(jì)模式

  • 裝飾模式

裝飾模式,動(dòng)態(tài)地給一個(gè)對(duì)象添加一些額外的職責(zé)毡证。就擴(kuò)展功能來(lái)說(shuō)电爹,裝飾模式相比生成子類更為靈活(四人幫 1994)。 而類目就是OC語(yǔ)言框架對(duì)裝飾模式的經(jīng)典應(yīng)用料睛, SDWebImage作者通過(guò)類目的方式靈活的給視圖類添加了各種功能丐箩,同時(shí)也完成了下載摇邦、存儲(chǔ)功能與具體視圖的綁定,值得我輩學(xué)習(xí)屎勘。

  • 單例模式

** 單例模式:保證一個(gè)類僅有一個(gè)實(shí)例施籍,并提供一個(gè)訪問(wèn)它的全局訪問(wèn)點(diǎn)(四人幫 1994)。** 老生常談的設(shè)計(jì)模式概漱,亦是經(jīng)典的設(shè)計(jì)模式丑慎。SDWebImage框架中對(duì)其有大量應(yīng)用。

單例類 方法 職責(zé)
SDImageCache sharedImageCache 負(fù)責(zé)處理圖像緩存瓤摧、配置緩存參數(shù)
SDWebImageDownloader sharedDownloader 核心代碼竿裂,負(fù)責(zé)圖像的異步下載
SDWebImageManager sharedManager 綁定視圖、下載照弥、緩存的管理類腻异,操作的中樞
SDWebImagePrefetcher sharedImagePrefetcher 預(yù)加載
  • 適配器模式、代理模式

** 適配器模式:將一個(gè)類的接口換成客戶希望的另一個(gè)接口这揣。使得原本由于接口不兼容而不能一起工作的那些類可以一起工作(四人幫 1994)悔常。** 若不明白直接說(shuō)協(xié)議--委托就是屬于對(duì)象適配器就明白了,也對(duì)那句“OC中的多繼承通過(guò)協(xié)議實(shí)現(xiàn)的”有更深的理解吧给赞。在此不再詳敘机打,有興趣的同學(xué)可以看看四人幫的《設(shè)計(jì)模式》一書(shū)。

協(xié)議類 職責(zé)
SDWebImageOperation 提供取消操作的兼容接口cancel
SDWebImageDownloaderOperationInterface 自定義下載操作時(shí)使用
SDWebImageManagerDelegate 提供管理類的回調(diào)
SDWebImagePrefetcherDelegate 提供預(yù)加載回調(diào)
  • 生成器模式

生成器模式:將一個(gè)復(fù)雜對(duì)象的構(gòu)建與它的表現(xiàn)分離片迅,使得同樣的構(gòu)建過(guò)程可以創(chuàng)建不同的表現(xiàn)残邀。 這也是筆者要著重推薦的一種設(shè)計(jì)模式,靈活運(yùn)用能很好的達(dá)到視圖與功能分離的解耦目的障涯,我們編碼中經(jīng)彻奁欤可見(jiàn)的各種Manager,包括SDWebImage中的SDWebImageManager唯蝶、SDWebImageDownloader等九秀,根據(jù)不同參數(shù)對(duì)視圖表現(xiàn)或功能進(jìn)行比較復(fù)雜的配置、操作粘我,使用此方法可以很好地進(jìn)行代碼分層鼓蜒、解耦,使邏輯清晰征字。

3. 為什么要進(jìn)行圖像解壓縮

/* UIImage *image = [UIImage sd_imageWithData:data];
  image = [UIImage decodedImageWithImage:image];
*/
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image;

SDWebImageDecoder代碼中顯示是從UIImage返回UIImage都弹,自己返回自己,何苦來(lái)哉匙姜?

請(qǐng)注意畅厢,這里有2個(gè)步驟.

  1. NSData-->UIImage
  2. UIImage-->UIImage

解壓已經(jīng)下載緩存起來(lái)的圖片可以提高性能,但是會(huì)消耗大量的內(nèi)存。
具體原理有興趣的同學(xué)可參考以下文章:
SDWebImageDecoder氮昧,異步對(duì)圖像進(jìn)行了一次解壓??
談?wù)?iOS 中圖片的解壓縮

4. 圖像加載及緩存邏輯

一般來(lái)講緩存分為兩種:

  • 永久存儲(chǔ)框杜,又叫磁盤(pán)存儲(chǔ)浦楣,在iOS 中即存儲(chǔ)在沙盒
  • 內(nèi)存存儲(chǔ),即存儲(chǔ)在進(jìn)程分配的內(nèi)存咪辱,程序關(guān)閉后及消失振劳。經(jīng)常的做法是聲明一全局變量來(lái)賦值進(jìn)去(iOS 常用NSDictionary/NSCache),SDWebImage用的是NSCache

NSDictionaryNSCache區(qū)別可看下篇文章:
構(gòu)建緩存時(shí)選用NSCache而非NSDictionary

  • 圖像緩存
  1. 驗(yàn)證此URL沒(méi)有(是否)被標(biāo)記為不可用

SDImageCache里查詢沒(méi)有(是否)存在緩存的圖片

  • 查看內(nèi)存的緩存,根據(jù)keyNSCache獲取Value
  • 查看磁盤(pán)的緩存油狂,若存在計(jì)算緩存代價(jià)若允許存入內(nèi)存历恐,拋入主線程

用戶(是否)通過(guò)參數(shù)SDWebImageOptions要求必須網(wǎng)絡(luò)刷新

3.用戶(是否)通過(guò)委托設(shè)置允許對(duì)此URL進(jìn)行網(wǎng)絡(luò)下載

  • 圖像下載
  1. 創(chuàng)建下載請(qǐng)求
  2. 創(chuàng)建下載操作
  3. url證書(shū)
  4. 優(yōu)先級(jí)
  5. 在下載隊(duì)列里添加下載操作,執(zhí)行下載操作

5. 用戶認(rèn)證 NSURLCredential

當(dāng)連接客戶端與服務(wù)端進(jìn)行數(shù)據(jù)傳輸?shù)臅r(shí)候,web服務(wù)器收到客戶端請(qǐng)求時(shí)可能需要先驗(yàn)證客戶端是否是正常用戶,再?zèng)Q定是否返回該接口的真實(shí)數(shù)據(jù)专筷。

iOS7.0之前使用的網(wǎng)絡(luò)框架是NSURLConnection,在 2013 的 WWDC 上弱贼,蘋(píng)果推出了NSURLConnection的繼任者:NSURLSession,SDWebImage使用的是NSURLConnection,這兩種網(wǎng)絡(luò)框架的認(rèn)證調(diào)用的方法也是不一樣的,有興趣的可以去google一下這里只看下NSURLConnection的認(rèn)證

認(rèn)證過(guò)程:

1. web服務(wù)器接收到來(lái)自客戶端的請(qǐng)求

2. web服務(wù)并不直接返回?cái)?shù)據(jù),而是要求客戶端提供認(rèn)證信息,也就是說(shuō)挑戰(zhàn)是服務(wù)端向客戶端發(fā)起的

2.1 要求客戶端提供用戶名與密碼挑戰(zhàn)NSInternetPassword

2.2 要求客戶端提供客戶端證書(shū) NSClientCertificate

2.3 要求客戶端信任該服務(wù)器

3. 客戶端回調(diào)執(zhí)行,接收到需要提供認(rèn)證信息,然后提供認(rèn)證信息,并再次發(fā)送給web服務(wù)

4. web服務(wù)驗(yàn)證認(rèn)證信息

4.1 認(rèn)證成功,將最終的數(shù)據(jù)結(jié)果發(fā)送給客戶端

4.2 認(rèn)證失敗,錯(cuò)誤此次請(qǐng)求,返回錯(cuò)誤碼401

6. 下載操作的 任務(wù)調(diào)度和多線程安全問(wèn)題

dispatch_barrier_sync是SD選用的GCD函數(shù),self.barrierQueue是存放任務(wù)的隊(duì)列,block里面是要執(zhí)行的任務(wù)仁堪。
SD添加下載任務(wù)是同步的哮洽,而且都是在self.barrierQueue這個(gè)并行隊(duì)列中填渠,同步添加任務(wù)弦聂。這樣也保證了根據(jù)executionOrder設(shè)置依賴關(guān)是正確的。
換句話說(shuō)如果創(chuàng)建下載任務(wù)不是使用dispatch_barrier_sync完成的氛什,而是使用異步方法 莺葫,雖然依次添加創(chuàng)建下載操作A、B枪眉、C的任務(wù)捺檬,但實(shí)際創(chuàng)建順序可能為A、C贸铜、B堡纬,這樣當(dāng)executionOrder的值是SDWebImageDownloaderLIFOExecutionOrder,設(shè)置的操作依賴關(guān)系就變成了A依賴C蒿秦,C依賴B

typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
//先進(jìn)先出烤镐, 默認(rèn)值,所有的下載操作以隊(duì)列類型執(zhí)行,先被加入下載隊(duì)列的操作先執(zhí)行
SDWebImageDownloaderFIFOExecutionOrder,
// 先進(jìn)后出棍鳖,所有的下載操作以棧類型執(zhí)行,后被加入下載隊(duì)列的操作先執(zhí)行
SDWebImageDownloaderLIFOExecutionOrder
};

有興趣的同學(xué)可以看看:
通過(guò)GCD中的dispatch_barrier_(a)sync加強(qiáng)對(duì)sync中所謂等待的理解

dispatch_barrier_sync VS dispatch_barrier_sync

** Dispatch Barrier解決多線程并發(fā)讀寫(xiě)一個(gè)資源發(fā)生死鎖 **

sync說(shuō)明了這是個(gè)同步函數(shù),任務(wù)不會(huì)立即返回,會(huì)等到任務(wù)執(zhí)行結(jié)束才返回炮叶。

使用dispatch_barrier_sync函數(shù)創(chuàng)建的任務(wù)會(huì)首先查看隊(duì)列是否有別的任務(wù)要執(zhí)行,如果有則等待已有任務(wù)執(zhí)行完畢再執(zhí)行渡处;同時(shí)在此方法后添加的任務(wù)必須等到此方法中的任務(wù)執(zhí)行后才能執(zhí)行镜悉,利用這個(gè)方法可以控制執(zhí)行順序。

Dispatch Barrier確保提交的block是指定隊(duì)列中特定時(shí)段唯一在執(zhí)行的一個(gè)医瘫。在所有先于Dispatch Barrier的任務(wù)都完成的情況下這個(gè)block才開(kāi)始執(zhí)行侣肄。輪到這個(gè)block時(shí)barrier會(huì)執(zhí)行這個(gè)block并且確保隊(duì)列在此過(guò)程 不會(huì)執(zhí)行其他任務(wù)。block完成后才恢復(fù)隊(duì)列醇份。

_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);

這是用戶自己創(chuàng)建的隊(duì)列稼锅,DISPATCH_QUEUE_CONCURRENT代表的是它是一個(gè)并行隊(duì)列叮喳。

為什么選擇并發(fā)隊(duì)列而不是串行隊(duì)列?

串行隊(duì)列可以保證任務(wù)按照添加順序挨個(gè)開(kāi)始執(zhí)行,并且上個(gè)任務(wù)結(jié)束才開(kāi)始下一個(gè)任務(wù)缰贝,這已經(jīng)可以保證任務(wù)的執(zhí)行順序(或者說(shuō)是任務(wù)結(jié)束的順序)了馍悟,但是并發(fā)隊(duì)列只能保證任務(wù)的開(kāi)始,不能保證任務(wù)的結(jié)束順序剩晴,解決辦法就是:并發(fā)隊(duì)列使用Barrier保證控制任務(wù)結(jié)束順序锣咒。

這部分就先到這里繼續(xù)向下看:

dispatch_barrier_sync(self.barrierQueue, ^{
        BOOL first = NO;
        if (!self.URLCallbacks[url]) {
            self.URLCallbacks[url] = [NSMutableArray new];
            first = YES;
        }
        NSMutableArray *callbacksForURL = self.URLCallbacks[url];
        NSMutableDictionary *callbacks = [NSMutableDictionary new];
        if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
        if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
        [callbacksForURL addObject:callbacks];
        self.URLCallbacks[url] = callbacksForURL;
這些代碼的目的都是為了給url綁定回調(diào)

URLCallbacks是一個(gè)可變字典,keyNSURL類型赞弥,valueNSMutableArray類型毅整,value(數(shù)組里面)只包含一個(gè)元素,這個(gè)元素的類型是NSMutableDictionary類型绽左,這個(gè)字典的keyNSString類型代表著回調(diào)類型悼嫉,valueblock,是對(duì)應(yīng)的回調(diào)

繼續(xù)向下看:

   if (first) {
            createCallback();
    }

如果url第一次綁定它的回調(diào)拼窥,也就是第一次使用這個(gè)url創(chuàng)建下載任務(wù)戏蔑,則執(zhí)行一次創(chuàng)建回調(diào)

如何確保同一url對(duì)應(yīng)的圖片不會(huì)被重復(fù)下載?

在創(chuàng)建回調(diào)中 創(chuàng)建下載操作(下載操作并不是在這里創(chuàng)建的)鲁纠,dispatch_barrier_sync執(zhí)行確保同一時(shí)間只有一個(gè)線程操作URLCallbacks屬性总棵,也就是確保了下面創(chuàng)建過(guò)程中在給operation傳遞回調(diào)的時(shí)候能取到正確的self.URLCallbacks[url]值,同時(shí)確保后面有相同的url再次創(chuàng)建的時(shí)候if (!self.URLCallbacks[url])分支不再進(jìn)入改含,first==NO情龄,也就不再繼續(xù)調(diào)用創(chuàng)建回調(diào),這樣就確保了同一個(gè)url對(duì)應(yīng)的圖片不會(huì)重復(fù)下載

以上這部分代碼總結(jié)起來(lái)只做了一件事情:在barrierQueue隊(duì)列中創(chuàng)建下載任務(wù)

功能

  • UIImageView捍壤,UIButton骤视,MKAnnotationView的類別添加網(wǎng)頁(yè)圖像和緩存管理
  • 異步圖像下載器
  • 具有自動(dòng)到期處理的異步 內(nèi)存+磁盤(pán) 緩存
  • 背景圖像解壓縮
  • 同一個(gè)URL不會(huì)下載多次
  • 虛假無(wú)效網(wǎng)址不會(huì)重復(fù)請(qǐng)求
  • 主線程永遠(yuǎn)不會(huì)被阻塞
  • 性能優(yōu)勢(shì)
  • 用GCD 和 ARC

優(yōu)勢(shì)

相對(duì)于原生NSURLRequestNSURLCache處理磁盤(pán)緩存,SDWebImage有什么優(yōu)勢(shì)鹃觉?
  • 從iOS 105.0起专酗, NSURLCache在內(nèi)存和磁盤(pán)上緩存的是原始HTTP響應(yīng)數(shù)據(jù),每次擊中緩存時(shí)帜慢,程序都必須將原始緩存數(shù)據(jù)轉(zhuǎn)換為UIImage才能使用笼裳。而這個(gè)轉(zhuǎn)化過(guò)程涉及復(fù)雜且廣泛的操作,如數(shù)據(jù)解析(解析被編碼過(guò)的HTTP數(shù)據(jù))粱玲,內(nèi)存復(fù)制等躬柬。

  • SDWebImage采用UIImage的形式在內(nèi)存中緩存圖片數(shù)據(jù),并將已解碼過(guò)的HTTP數(shù)據(jù)的壓縮文件存儲(chǔ)在磁盤(pán)上抽减。 使用NSCache將UIImage原樣存儲(chǔ)在內(nèi)存中允青,因此不會(huì)涉及任何副本,并且只要程序或系統(tǒng)需要卵沉,內(nèi)存就會(huì)被釋放颠锉。

  • 一般第一次在UIImageView中使用UIImage時(shí)的圖像解壓縮是在主線程完成的法牲,而SDWebImageDecoder強(qiáng)制其在后臺(tái)線程中。

  • SDWebImage完全繞過(guò)復(fù)雜且經(jīng)常配置錯(cuò)誤的HTTP緩存控制協(xié)議琼掠,大大加速了緩存查找拒垃。

相對(duì)于AFNetworkingUIImageView提供的類似功能,SDWebImage有什么優(yōu)勢(shì)瓷蛙?
  • AFNetworking默認(rèn)使用NSCacheUIKit中對(duì)UIImageViewUIButton配置內(nèi)存緩存悼瓮。
  • SDWebImage同時(shí)利用Foundation框架中的NSURLCacheURL系統(tǒng)加載緩存,也用到了NSCache艰猬。 AFNetworking還提供了如圖像數(shù)據(jù)的后臺(tái)解壓縮功能横堡。
  • 也就是說(shuō)AFNetworking UIKit部分實(shí)現(xiàn)了簡(jiǎn)單的異步圖像加載類別祠墅,是SDWebImage的部分功能

第二部分

在使用這個(gè)框架的時(shí)候坦袍,只需要提供一個(gè)下載的url和占位圖就可以在回調(diào)里拿到下載后的圖片:

[imageview sd_setImageWithURL:[NSURL URLWithString:@"pic.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder"] completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {

        imageview.image = image;
        NSLog(@"圖片加載完成");

    }];

而且我們還可以不設(shè)置占位圖片廊酣,也可以不使用回調(diào)的block苍苞,非常靈

//圖片下載完成后直接顯示下載后的圖片
[imageview sd_setImageWithURL:[NSURL URLWithString:@"pic.jpg"]];

在最開(kāi)始先簡(jiǎn)單介紹這個(gè)框架

這個(gè)框架的核心類是SDWebImageManger,在外部有UIImageView+WebCacheUIButton+WebCache為下載圖片的操作提供接口熟空。內(nèi)部有SDWebImageManger負(fù)責(zé)處理和協(xié)調(diào) SDWebImageDownloaderSDWebImageCacheSDWebImageDownloader負(fù)責(zé)具體的下載任務(wù)昏名,SDWebImageCache負(fù)責(zé)關(guān)于緩存的工作:添加索抓,刪除碳蛋,查詢緩存胚泌。

首先我們大致看一下這個(gè)框架的調(diào)用流程圖:

SDWebImage框架結(jié)構(gòu)圖.png

從這個(gè)流程圖里可以大致看出,該框架分為兩個(gè)層:UIKit層(負(fù)責(zé)接收下載參數(shù))和工具層(負(fù)責(zé)下載操作和緩存)肃弟。

OK~基本流程大概清楚了,我們看一下每個(gè)層具體實(shí)現(xiàn)吧~


UIKit層

該框架最外層的類是UIImageView +WebCache零蓉,我們將圖片的URL笤受,占位圖片直接給這個(gè)類。下面是這個(gè)類的公共接口:

 // ==============  UIImageView + WebCache.h ============== //
- (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;


可以看出敌蜂,這個(gè)類提供的接口非常靈活箩兽,可以根據(jù)我們自己的需求來(lái)調(diào)用其中某一個(gè)方法,而這些方法到最后都會(huì)走到:

// ==============  UIImageView + WebCache.m ============== //
- (void)sd_setImageWithURL:(NSURL *)url
          placeholderImage:(UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(SDWebImageCompletionBlock)completedBlock;

而這個(gè)方法里面章喉,調(diào)用的是UIView+WebCache分類的:

// ==============  UIView+ WebCache.m ============== //
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock;

為什么不是UIImageView+WebCache而要上一層到UIView的分類里呢汗贫?
因?yàn)镾DWebImage框架也支持UIButton的下載圖片等方法,所以需要在它們的父類:UIView里面統(tǒng)一一個(gè)下載方法秸脱。

簡(jiǎn)單看一下這個(gè)方法的實(shí)現(xiàn)(省略的代碼用...代替):

 // ==============  UIView+ WebCache.m ============== //

    //valid key:UIImageView || UIButton
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    //UIView+WebCacheOperation 的 operationDictionary
    //下面這行代碼是保證沒(méi)有當(dāng)前正在進(jìn)行的異步下載操作, 使它不會(huì)與即將進(jìn)行的操作發(fā)生沖突
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];


    /* options & SDWebImageDelayPlaceholder這是一個(gè)位運(yùn)算的與操作,
       !(options & SDWebImageDelayPlaceholder)的意思就是options參數(shù)
       不是SDWebImageDelayPlaceholder,就執(zhí)行以下操作 
    */
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }

    //如果url存在
    if (url) {

       ...
        __weak __typeof(self)wself = self;

       //SDWebImageManager下載圖片
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {

            ...
            //dispatch_main_sync_safe : 保證block能在主線程進(jìn)行
            dispatch_main_async_safe(^{

                if (!sself) {
                    return;
                }

            /* SDWebImageAvoidAutoSetImage,默認(rèn)情況下圖片會(huì)在下載完畢后自動(dòng)添加
               給imageView,但是有些時(shí)候我們想在設(shè)置圖片之前加一些圖片的處理,就要下
               載成功后去手動(dòng)設(shè)置圖片了,不會(huì)執(zhí)行` wself.image = image; `,而是直接執(zhí)行
               完成回調(diào)落包,有用戶自己決定如何處理。
            */
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                     //image摊唇,而且不自動(dòng)替換 placeholder image
                    completedBlock(image, error, cacheType, url);
                    return;

                } else if (image) {
                    //存在image咐蝇,需要馬上替換 placeholder image
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    [sself sd_setNeedsLayout];

                } else {                    
                    //沒(méi)有image,在圖片下載完之后顯示 placeholder image
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }

                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];

        //在操作緩存字典(operationDictionary)里添加operation巷查,表示當(dāng)前的操作正在進(jìn)行
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];

    } else {
        //如果url不存在有序,就在completedBlock里傳入error(url為空)
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }

值得一提的是抹腿,在這一層,使用一個(gè)字典operationDictionary專門(mén)用作存儲(chǔ)操作的緩存旭寿,隨時(shí)添加警绩,刪除操作任務(wù)。
而這個(gè)字典是UIView+WebCacheOperation分類的關(guān)聯(lián)對(duì)象盅称,它的存取方法使用運(yùn)行時(shí)來(lái)操作:

 // ==============  UIView+WebCacheOperation.m ============== //
- (SDOperationsDictionary *)operationDictionary {
    SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
    if (operations) {
        return operations;
    }
    operations = [NSMutableDictionary dictionary];
    objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    return operations;
}

為什么不直接在UIImageView+WebCache里直接關(guān)聯(lián)這個(gè)對(duì)象呢房蝉?我覺(jué)得這里作者應(yīng)該是遵從面向?qū)ο蟮膯我宦氊?zé)原則(SRP:Single responsibility principle),就連類都要履行這個(gè)職責(zé)微渠,何況分類呢搭幻?這里作者專門(mén)創(chuàng)造一個(gè)分類UIView+WebCacheOperation來(lái)管理操作緩存(字典)。

到這里逞盆,UIKit層上面的東西都講完了檀蹋,現(xiàn)在開(kāi)始正式講解工具層。

工具層

上文提到過(guò)云芦,SDWebImageManager同時(shí)管理SDImageCacheSDWebImageDownloader兩個(gè)類俯逾,它是這一層的老大哥。在下載任務(wù)開(kāi)始的時(shí)候舅逸,SDWebImageManager首先訪問(wèn)SDImageCache來(lái)查詢是否存在緩存桌肴,如果有緩存,直接返回緩存的圖片琉历。如果沒(méi)有緩存坠七,就命令SDWebImageDownloader來(lái)下載圖片,下載成功后旗笔,存入緩存彪置,顯示圖片。以上是SDWebImageManager大致的工作流程蝇恶。

在詳細(xì)講解SDWebImageManager是如何下載圖片之前拳魁,我們先看一下這個(gè)類的幾個(gè)重要的屬性:

 // ==============  SDWebImageManager.m ============== //
/*
*初始化方法
*1.獲得一個(gè)SDImageCache的實(shí)例
*2.獲得一個(gè)SDWebImageDownloader的實(shí)例
*3.新建一個(gè)MutableSet來(lái)存儲(chǔ)下載失敗的url
*4.新建一個(gè)用來(lái)存儲(chǔ)下載operation的可變數(shù)組
*/
@property (strong, nonatomic, readwrite, nonnull) SDImageCache *imageCache;
@property (strong, nonatomic, readwrite, nonnull) SDWebImageDownloader *imageDownloader;
@property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;
@property (strong, nonatomic, nonnull) NSMutableArray<SDWebImageCombinedOperation *> *runningOperations;

SDWebImageManager下載圖片的方法只有一個(gè):

[SDWebImageManager.sharedManager loadImageWithURL:options:progress:completed:]

看一下這個(gè)方法的具體實(shí)現(xiàn):

 // ==============  SDWebImageManager.m ============== //
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {
     ...                             
    //在SDImageCache里查詢是否存在緩存的圖片
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {

        ...
        //(沒(méi)有緩存圖片) || (即使有緩存圖片,也需要更新緩存圖片) || (代理沒(méi)有響應(yīng)imageManager:shouldDownloadImageForURL:消息撮弧,默認(rèn)返回yes潘懊,需要下載圖片)|| (imageManager:shouldDownloadImageForURL:返回yes,需要下載圖片)
        if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {

            //1. 存在緩存圖片 && 即使有緩存圖片也要下載更新圖片
            if (cachedImage && options & SDWebImageRefreshCached) {
                [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }


            // 2. 如果不存在緩存圖片
            ...

            //開(kāi)啟下載器下載
            //subOperationToken 用來(lái)標(biāo)記當(dāng)前的下載任務(wù)贿衍,便于被取消
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                    // 1. 如果任務(wù)被取消授舟,則什么都不做,避免和其他的completedBlock重復(fù)

                } else if (error) {

                    //2. 如果有錯(cuò)誤
                    //2.1 在completedBlock里傳入error
                    [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];

                            //2.2 在錯(cuò)誤url名單中添加當(dāng)前的url
                    if (   error.code != NSURLErrorNotConnectedToInternet
                        && error.code != NSURLErrorCancelled
                        && error.code != NSURLErrorTimedOut
                        && error.code != NSURLErrorInternationalRoamingOff
                        && error.code != NSURLErrorDataNotAllowed
                        && error.code != NSURLErrorCannotFindHost
                        && error.code != NSURLErrorCannotConnectToHost) {

                       @synchronized (self.failedURLs) {
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {

                    //3. 下載成功
                    //3.1 如果需要下載失敗后重新下載舌厨,則將當(dāng)前url從失敗url名單里移除
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }

                    //3.2 進(jìn)行緩存
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);


                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {

                        //(即使緩存存在岂却,也要刷新圖片) && 緩存圖片 && 不存在下載后的圖片:不做操作

                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {

          //(下載圖片成功 && (沒(méi)有動(dòng)圖||處理動(dòng)圖) && (下載之后,緩存之前處理圖片)               dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

                            if (transformedImage && finished) {
                                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                                // pass nil if the image was transformed, so we can recalculate the data from the image
                                //緩存圖片
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            //將圖片傳入completedBlock
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {

                        //(圖片下載成功并結(jié)束)
                        if (downloadedImage && finished) {
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }

                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }

                     //如果完成,從當(dāng)前運(yùn)行的操作列表里移除當(dāng)前操作
                if (finished) {
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];

            //取消的block
            operation.cancelBlock = ^{

                //取消當(dāng)前的token
                [self.imageDownloader cancel:subOperationToken];
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                //從當(dāng)前運(yùn)行的操作列表里移除當(dāng)前操作
                [self safelyRemoveOperationFromRunning:strongOperation];
            };

        } else if (cachedImage) {

            //存在緩存圖片
            __strong __typeof(weakOperation) strongOperation = weakOperation;

            //調(diào)用完成的block
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];

            //刪去當(dāng)前的的下載操作(線程安全)
            [self safelyRemoveOperationFromRunning:operation];

        } else {

            //沒(méi)有緩存的圖片躏哩,而且下載被代理終止了
            __strong __typeof(weakOperation) strongOperation = weakOperation;

            // 調(diào)用完成的block
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];

            //刪去當(dāng)前的下載操作
            [self safelyRemoveOperationFromRunning:operation];
        }
    }];

    return operation;                                                             
}

看完了SDWebImageManager的回調(diào)處理署浩,我們分別看一下
SDImageCacheSDWebImageDownloader內(nèi)部具體是如何工作的。首先看一下SDImageCache

SDImageCache

屬性

 // ==============  SDImageCache.m ============== //
@property (strong, nonatomic, nonnull) NSCache *memCache;//內(nèi)存緩存
@property (strong, nonatomic, nonnull) NSString *diskCachePath;//磁盤(pán)緩存路徑
@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;//
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t //ioQueue唯一子線程;

核心方法:查詢緩存

 // ==============  SDImageCache.m ============== //
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

     //================查看內(nèi)存的緩存=================//
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    // 如果存在扫尺,直接調(diào)用block筋栋,將image,data正驻,CaheType傳進(jìn)去
    if (image) {
        NSData *diskData = nil;

        // 如果是gif弊攘,就拿到data,后面要傳到doneBlock里姑曙。不是gif就傳nil
        if ([image isGIF]) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
       // 因?yàn)閳D片有緩存可供使用襟交,所以不用實(shí)例化NSOperation,直接范圍nil
        return nil;
    }

    //================查看磁盤(pán)的緩存=================//
    NSOperation *operation = [NSOperation new];

  //唯一的子線程:self.ioQueue
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            // 在用之前就判斷operation是否被取消了伤靠,作者考慮的非常嚴(yán)謹(jǐn)
            return;
        }

        @autoreleasepool {
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];c
            if (diskImage && self.config.shouldCacheImagesInMemory) {

                //  cost 被用來(lái)計(jì)算緩存中所有對(duì)象的代價(jià)捣域。當(dāng)內(nèi)存受限或者所有緩存對(duì)象的總代價(jià)超過(guò)了最大允許的值時(shí),緩存會(huì)移除其中的一些對(duì)象宴合。
                NSUInteger cost = SDCacheCostForImage(diskImage);

                // 存入內(nèi)存緩存中
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });

    return operation;
}

SDWebImageDownloader

屬性

 // ==============  SDWebImageDownloader.m ============== //
@property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue;//下載隊(duì)列
@property (weak, nonatomic, nullable) NSOperation *lastAddedOperation;//最后添加的下載操作
@property (assign, nonatomic, nullable) Class operationClass;//操作類
@property (strong, nonatomic, nonnull) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;//操作數(shù)組
@property (strong, nonatomic, nullable) SDHTTPHeadersMutableDictionary *HTTPHeaders;//HTTP請(qǐng)求頭
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t barrierQueue;//用來(lái)阻塞前面的下載線程(串行化)

核心方法:下載圖片

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    __weak SDWebImageDownloader *wself = self;

    return [self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^SDWebImageDownloaderOperation *{
        __strong __typeof (wself) sself = wself;
        NSTimeInterval timeoutInterval = sself.downloadTimeout;
        if (timeoutInterval == 0.0) {
            timeoutInterval = 15.0;
        }

        // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
        // 為防止重復(fù)緩存焕梅,默認(rèn)網(wǎng)絡(luò)請(qǐng)求不進(jìn)行緩存操作
        // 創(chuàng)建下載請(qǐng)求,配置相關(guān)參數(shù)
        NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
        request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
        request.HTTPShouldUsePipelining = YES;
        if (sself.headersFilter) {
            request.allHTTPHeaderFields = sself.headersFilter(url, [sself.HTTPHeaders copy]);
        }
        else {
            request.allHTTPHeaderFields = sself.HTTPHeaders;
        }
        // 針對(duì)每次下載操作卦洽,SD自創(chuàng)一個(gè)操作類贞言,眾多操作放在一個(gè)操作隊(duì)列中,便于管理眾多下載操作
        // 創(chuàng)建下載操作:SDWebImageDownloaderOperation用于請(qǐng)求網(wǎng)絡(luò)資源的操作阀蒂,它是一個(gè) NSOperation 的子類
        SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
        operation.shouldDecompressImages = sself.shouldDecompressImages;

        //url證書(shū)
        if (sself.urlCredential) {
            operation.credential = sself.urlCredential;
        } else if (sself.username && sself.password) {
            operation.credential = [NSURLCredential credentialWithUser:sself.username password:sself.password persistence:NSURLCredentialPersistenceForSession];
        }
        
        // 優(yōu)先級(jí)
        if (options & SDWebImageDownloaderHighPriority) {
            operation.queuePriority = NSOperationQueuePriorityHigh;
        } else if (options & SDWebImageDownloaderLowPriority) {
            operation.queuePriority = NSOperationQueuePriorityLow;
        }

         // 在下載隊(duì)列里添加下載操作该窗,執(zhí)行下載操作
        [sself.downloadQueue addOperation:operation];

        // 如果后進(jìn)先出
        if (sself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
            //  Emulate LIFO execution order by systematically adding new operations as last operation's dependency
           //  addDependency:參數(shù)opertaion倍添加到NSOperationQueue后,只有等該opertion結(jié)束后才能執(zhí)行其他的operation脂新,實(shí)現(xiàn)了后進(jìn)先出
            [sself.lastAddedOperation addDependency:operation];
            sself.lastAddedOperation = operation;
        }

        return operation;
    }];
}

這里面還有一個(gè)addProgressCallback: progressBlock: completedBlock: forURL: createCallback:方法挪捕,用來(lái)保存progressBlockcompletedBlock。我們看一下這個(gè)方法的實(shí)現(xiàn):

- (nullable SDWebImageDownloadToken *)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock
                                           completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock
                                                   forURL:(nullable NSURL *)url
                                           createCallback:(SDWebImageDownloaderOperation *(^)())createCallback {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. 
If it is nil immediately call the completed block with no image or data.
    // URL 將會(huì)做回調(diào)字典的key争便,不能為nil
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }

    __block SDWebImageDownloadToken *token = nil;

     // 串行化前面所有的操作
    dispatch_barrier_sync(self.barrierQueue, ^{

        // 當(dāng)前下載操作中取出SDWebImageDownloaderOperation實(shí)例
        SDWebImageDownloaderOperation *operation = self.URLOperations[url];

        // 如果沒(méi)有,就初始化它
        if (!operation) {
            operation = createCallback();
            self.URLOperations[url] = operation;

            __weak SDWebImageDownloaderOperation *woperation = operation;
            operation.completionBlock = ^{
              SDWebImageDownloaderOperation *soperation = woperation;
              if (!soperation) return;
              if (self.URLOperations[url] == soperation) {
                  [self.URLOperations removeObjectForKey:url];
              };
            };
        }

       // 這里 downloadOperationCancelToken 默認(rèn)是一個(gè)字典断医,存放 progressBlock 和 completedBlock
        id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];

        token = [SDWebImageDownloadToken new];
        token.url = url;
        token.downloadOperationCancelToken = downloadOperationCancelToken;
    });

    return token;
}

這里真正保存兩個(gè)block的方法是addHandlersForProgress: completed:

- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                            completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {

   // 實(shí)例化一個(gè)SDCallbacksDictionary滞乙,存放一個(gè)progressBlock 和 completedBlock
    SDCallbacksDictionary *callbacks = [NSMutableDictionary new];
    if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
    if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
    dispatch_barrier_async(self.barrierQueue, ^{
        // 添加到緩存中 self.callbackBlocks
        [self.callbackBlocks addObject:callbacks];
    });
    return callbacks;
}

到這里SDWebImage的核心方法都講解完畢了,其他沒(méi)有講到的部分以后會(huì)慢慢添加上去鉴嗤。

最后看一下一些比較零散的知識(shí)點(diǎn):


1. 運(yùn)行時(shí)存取關(guān)聯(lián)對(duì)象:

存:

// 將operations對(duì)象關(guān)聯(lián)給self斩启,地址為&loadOperationKey,語(yǔ)義是OBJC_ASSOCIATION_RETAIN_NONATOMIC醉锅。
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

韧么亍:

// 將operations對(duì)象通過(guò)地址&loadOperationKey從self里取出來(lái)
SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);

2. 數(shù)組的寫(xiě)操作需要加鎖(多線程訪問(wèn),避免覆寫(xiě))

//給self.runningOperations加鎖
//self.runningOperations數(shù)組的添加操作
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }

//self.runningOperations數(shù)組的刪除操作
- (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
    @synchronized (self.runningOperations) {
        if (operation) {
            [self.runningOperations removeObject:operation];
        }
    }
}

3. 確保在主線程的宏:

dispatch_main_async_safe(^{
                  //將下面這段代碼放在主線程中
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });

//宏定義:
#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

4. 設(shè)置不能為nil的參數(shù)

- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
    if ((self = [super init])) {
        _imageCache = cache;
        _imageDownloader = downloader;
        _failedURLs = [NSMutableSet new];
        _runningOperations = [NSMutableArray new];
    }
    return self;
}

如果在參數(shù)里添加了nonnull關(guān)鍵字,那么編譯器就可以檢查傳入的參數(shù)是否為nil垄琐,如果是边酒,則編譯器會(huì)有警告

5. 容錯(cuò),強(qiáng)制轉(zhuǎn)換類型

if ([url isKindOfClass:NSString.class]) {
        url = [NSURL URLWithString:(NSString *)url];
}

在傳入的參數(shù)為NSString時(shí)(但是方法參數(shù)要求是NSURL)狸窘,自動(dòng)轉(zhuǎn)換為NSURL

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末墩朦,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子翻擒,更是在濱河造成了極大的恐慌氓涣,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件陋气,死亡現(xiàn)場(chǎng)離奇詭異劳吠,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)巩趁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)痒玩,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人晶渠,你說(shuō)我怎么就攤上這事凰荚。” “怎么了褒脯?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵便瑟,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我番川,道長(zhǎng)到涂,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任颁督,我火速辦了婚禮践啄,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘沉御。我一直安慰自己屿讽,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布吠裆。 她就那樣靜靜地躺著伐谈,像睡著了一般。 火紅的嫁衣襯著肌膚如雪试疙。 梳的紋絲不亂的頭發(fā)上诵棵,一...
    開(kāi)封第一講書(shū)人閱讀 49,036評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音祝旷,去河邊找鬼履澳。 笑死嘶窄,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的距贷。 我是一名探鬼主播柄冲,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼储耐!你這毒婦竟也來(lái)了羊初?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤什湘,失蹤者是張志新(化名)和其女友劉穎长赞,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體闽撤,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡得哆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了哟旗。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贩据。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖闸餐,靈堂內(nèi)的尸體忽然破棺而出饱亮,到底是詐尸還是另有隱情,我是刑警寧澤舍沙,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布近上,位于F島的核電站,受9級(jí)特大地震影響拂铡,放射性物質(zhì)發(fā)生泄漏壹无。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一感帅、第九天 我趴在偏房一處隱蔽的房頂上張望斗锭。 院中可真熱鬧,春花似錦失球、人聲如沸岖是。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)璧微。三九已至,卻和暖如春硬梁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背胞得。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工荧止, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓跃巡,卻偏偏與公主長(zhǎng)得像危号,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子素邪,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

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

  • 圖片下載的這些回調(diào)信息存儲(chǔ)在SDWebImageDownloader類的URLOperations屬性中外莲,該屬性是...
    怎樣m閱讀 2,360評(píng)論 0 1
  • 前不久做了一個(gè)生成快照的需求,其中用到 SDWebImage 來(lái)下載圖片兔朦,在使用該框架的過(guò)程中也遇到了一些問(wèn)題偷线,索...
    ShannonChenCHN閱讀 14,053評(píng)論 12 241
  • 下載 下載管理器 SDWebImageDownLoader作為一個(gè)單例來(lái)管理圖片的下載操作。圖片的下載是放在一個(gè)N...
    wind_dy閱讀 1,450評(píng)論 0 1
  • SDWebImage是一個(gè)開(kāi)源的第三方庫(kù)沽甥,它提供了UIImageView的一個(gè)分類声邦,以支持從遠(yuǎn)程服務(wù)器下載并緩存圖...
    devning閱讀 418評(píng)論 0 0
  • 多線程、特別是NSOperation 和 GCD 的內(nèi)部原理摆舟。運(yùn)行時(shí)機(jī)制的原理和運(yùn)用場(chǎng)景亥曹。SDWebImage的原...
    LZM輪回閱讀 2,004評(píng)論 0 12