
SDWebImage 常用于對圖像的下載及緩存等。作者Olivier Poitrey,法國視頻分享網(wǎng)站Dailymotion(后被法國電信運營商Orange收購,被和諧) 的 CTO,擁有多個開源項目。目前SDWebImage獲得巨大成功,用戶甚眾,并且在github上已獲取超過17.7k star。
和筆者另一篇文章 **iOS 李明杰 MJRefresh源碼解析 **類似,本文主要素材來源有正在學(xué)hybrid開發(fā)的iOS開發(fā)者 J_Knight 的文章、楊千嬅染了紅頭發(fā) 的博客一行行看SDWebImage源碼(一)、一行行看SDWebImage源碼(二) 和 github上作者的用法介紹,再一次表示敬意。
本文分成兩個部分對SDWebImage進行介紹,上部分從框架層次方面進行概述,主要是筆者在學(xué)習(xí)使用和研究過程中的心得感悟;下半部分則是對具體各個類實現(xiàn)代碼的詳細解析。因此筆者建議,若是已經(jīng)對SDWebImage有一定了解的同學(xué)可以看一下上部分,若是剛開始使用此框架的同學(xué)大可略過第一部分,直接從代碼實現(xiàn)看起,以免被筆者可能不太成熟的見解所誤導(dǎo)。水平有限,還望大家不吝賜教。
第一部分
架構(gòu)設(shè)計(干貨)
"捫心自問",假如讓我們自己設(shè)計一個具有圖片異步加載并緩存功能的框架,我們該怎么設(shè)計。冥想3分鐘,好好看看這張類圖,好好看看這張類圖,好好看看這張類圖,在這里就先根據(jù)源碼試著反推整個框架的設(shè)計思路。


1. 封裝、繼承、多態(tài)特性
- 毫無疑問,封裝是單一職責(zé)原則的題中之義。
SDWebImage根據(jù)不同功能進行了不同的類的封裝:
| 類 | 職責(zé) |
|---|---|
UIView+WebCache |
各種視圖的基類,容納圖像的容器,操作綁定的對象 |
SDImageCache、SDImageCacheConfig |
負責(zé)處理圖像緩存、配置緩存參數(shù) |
SDWebImageDownloader |
核心代碼,負責(zé)圖像的異步下載 |
**SDWebImageManager ** |
綁定顯示圖像的視圖、圖像下載、圖像緩存3者的管理類,是操作的中樞 |
SDWebImagePrefetcher |
預(yù)加載 |
SDWebImageDecoder |
圖像解壓縮 |
- 面向接口編碼,而非面向?qū)崿F(xiàn)編碼,繼承中的依賴倒置原則,即基類中定義接口,子類中對基類接口做具體實現(xiàn)。例如
SDWebImage為UIImageView、UIButton等視圖控件提供圖像或Gif加載功能,所以在共同基類UIView中提供圖像異步下載等功能。
2. 設(shè)計模式
-
裝飾模式
裝飾模式,動態(tài)地給一個對象添加一些額外的職責(zé)。就擴展功能來說,裝飾模式相比生成子類更為靈活(四人幫 1994)。 而類目就是OC語言框架對裝飾模式的經(jīng)典應(yīng)用, SDWebImage作者通過類目的方式靈活的給視圖類添加了各種功能,同時也完成了下載、存儲功能與具體視圖的綁定,值得我輩學(xué)習(xí)。
-
單例模式
** 單例模式:保證一個類僅有一個實例,并提供一個訪問它的全局訪問點(四人幫 1994)。** 老生常談的設(shè)計模式,亦是經(jīng)典的設(shè)計模式。SDWebImage框架中對其有大量應(yīng)用。
| 單例類 | 方法 | 職責(zé) |
|---|---|---|
SDImageCache |
sharedImageCache |
負責(zé)處理圖像緩存、配置緩存參數(shù) |
SDWebImageDownloader |
sharedDownloader |
核心代碼,負責(zé)圖像的異步下載 |
SDWebImageManager |
sharedManager |
綁定視圖、下載、緩存的管理類,操作的中樞 |
SDWebImagePrefetcher |
sharedImagePrefetcher |
預(yù)加載 |
-
適配器模式、代理模式
** 適配器模式:將一個類的接口換成客戶希望的另一個接口。使得原本由于接口不兼容而不能一起工作的那些類可以一起工作(四人幫 1994)。** 若不明白直接說協(xié)議--委托就是屬于對象適配器就明白了,也對那句“OC中的多繼承通過協(xié)議實現(xiàn)的”有更深的理解吧。在此不再詳敘,有興趣的同學(xué)可以看看四人幫的《設(shè)計模式》一書。
| 協(xié)議類 | 職責(zé) |
|---|---|
SDWebImageOperation |
提供取消操作的兼容接口cancel
|
SDWebImageDownloaderOperationInterface |
自定義下載操作時使用 |
SDWebImageManagerDelegate |
提供管理類的回調(diào) |
SDWebImagePrefetcherDelegate |
提供預(yù)加載回調(diào) |
-
生成器模式
生成器模式:將一個復(fù)雜對象的構(gòu)建與它的表現(xiàn)分離,使得同樣的構(gòu)建過程可以創(chuàng)建不同的表現(xiàn)。 這也是筆者要著重推薦的一種設(shè)計模式,靈活運用能很好的達到視圖與功能分離的解耦目的,我們編碼中經(jīng)??梢姷母鞣NManager,包括SDWebImage中的SDWebImageManager、SDWebImageDownloader等,根據(jù)不同參數(shù)對視圖表現(xiàn)或功能進行比較復(fù)雜的配置、操作,使用此方法可以很好地進行代碼分層、解耦,使邏輯清晰。
3. 為什么要進行圖像解壓縮
/* UIImage *image = [UIImage sd_imageWithData:data];
image = [UIImage decodedImageWithImage:image];
*/
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image;
SDWebImageDecoder代碼中顯示是從UIImage返回UIImage,自己返回自己,何苦來哉?
請注意,這里有2個步驟.
-
NSData-->UIImage -
UIImage-->UIImage
解壓已經(jīng)下載緩存起來的圖片可以提高性能,但是會消耗大量的內(nèi)存。
具體原理有興趣的同學(xué)可參考以下文章:
SDWebImageDecoder,異步對圖像進行了一次解壓??
談?wù)?iOS 中圖片的解壓縮
4. 圖像加載及緩存邏輯
一般來講緩存分為兩種:
- 永久存儲,又叫磁盤存儲,在iOS 中即存儲在沙盒
- 內(nèi)存存儲,即存儲在進程分配的內(nèi)存,程序關(guān)閉后及消失。經(jīng)常的做法是聲明一全局變量來賦值進去(iOS 常用
NSDictionary/NSCache),SDWebImage用的是NSCache
NSDictionary和NSCache區(qū)別可看下篇文章:
構(gòu)建緩存時選用NSCache而非NSDictionary
-
圖像緩存
- 驗證此URL沒有(是否)被標(biāo)記為不可用
在
SDImageCache里查詢沒有(是否)存在緩存的圖片
- 查看內(nèi)存的緩存,根據(jù)
key從NSCache獲取Value - 查看磁盤的緩存,若存在計算緩存代價若允許存入內(nèi)存,拋入主線程
或
用戶(是否)通過參數(shù)
SDWebImageOptions要求必須網(wǎng)絡(luò)刷新
3.用戶(是否)通過委托設(shè)置允許對此URL進行網(wǎng)絡(luò)下載
-
圖像下載
- 創(chuàng)建下載請求
- 創(chuàng)建下載操作
- url證書
- 優(yōu)先級
- 在下載隊列里添加下載操作,執(zhí)行下載操作
5. 用戶認證 NSURLCredential
當(dāng)連接客戶端與服務(wù)端進行數(shù)據(jù)傳輸?shù)臅r候,web服務(wù)器收到客戶端請求時可能需要先驗證客戶端是否是正常用戶,再決定是否返回該接口的真實數(shù)據(jù)。
iOS7.0之前使用的網(wǎng)絡(luò)框架是
NSURLConnection,在 2013 的 WWDC 上,蘋果推出了NSURLConnection的繼任者:NSURLSession,SDWebImage使用的是NSURLConnection,這兩種網(wǎng)絡(luò)框架的認證調(diào)用的方法也是不一樣的,有興趣的可以去google一下這里只看下NSURLConnection的認證
認證過程:
1. web服務(wù)器接收到來自客戶端的請求
2. web服務(wù)并不直接返回數(shù)據(jù),而是要求客戶端提供認證信息,也就是說挑戰(zhàn)是服務(wù)端向客戶端發(fā)起的
2.1 要求客戶端提供用戶名與密碼挑戰(zhàn)NSInternetPassword
2.2 要求客戶端提供客戶端證書 NSClientCertificate
2.3 要求客戶端信任該服務(wù)器
3. 客戶端回調(diào)執(zhí)行,接收到需要提供認證信息,然后提供認證信息,并再次發(fā)送給web服務(wù)
4. web服務(wù)驗證認證信息
4.1 認證成功,將最終的數(shù)據(jù)結(jié)果發(fā)送給客戶端
4.2 認證失敗,錯誤此次請求,返回錯誤碼401
6. 下載操作的 任務(wù)調(diào)度和多線程安全問題
dispatch_barrier_sync是SD選用的GCD函數(shù),self.barrierQueue是存放任務(wù)的隊列,block里面是要執(zhí)行的任務(wù)。
SD添加下載任務(wù)是同步的,而且都是在self.barrierQueue這個并行隊列中,同步添加任務(wù)。這樣也保證了根據(jù)executionOrder設(shè)置依賴關(guān)是正確的。
換句話說如果創(chuàng)建下載任務(wù)不是使用dispatch_barrier_sync完成的,而是使用異步方法 ,雖然依次添加創(chuàng)建下載操作A、B、C的任務(wù),但實際創(chuàng)建順序可能為A、C、B,這樣當(dāng)executionOrder的值是SDWebImageDownloaderLIFOExecutionOrder,設(shè)置的操作依賴關(guān)系就變成了A依賴C,C依賴B
typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) {
//先進先出, 默認值,所有的下載操作以隊列類型執(zhí)行,先被加入下載隊列的操作先執(zhí)行
SDWebImageDownloaderFIFOExecutionOrder,
// 先進后出,所有的下載操作以棧類型執(zhí)行,后被加入下載隊列的操作先執(zhí)行
SDWebImageDownloaderLIFOExecutionOrder
};
有興趣的同學(xué)可以看看:
通過GCD中的dispatch_barrier_(a)sync加強對sync中所謂等待的理解
dispatch_barrier_sync VS dispatch_barrier_sync
** Dispatch Barrier解決多線程并發(fā)讀寫一個資源發(fā)生死鎖 **
sync說明了這是個同步函數(shù),任務(wù)不會立即返回,會等到任務(wù)執(zhí)行結(jié)束才返回。
使用dispatch_barrier_sync函數(shù)創(chuàng)建的任務(wù)會首先查看隊列是否有別的任務(wù)要執(zhí)行,如果有則等待已有任務(wù)執(zhí)行完畢再執(zhí)行;同時在此方法后添加的任務(wù)必須等到此方法中的任務(wù)執(zhí)行后才能執(zhí)行,利用這個方法可以控制執(zhí)行順序。
Dispatch Barrier確保提交的block是指定隊列中特定時段唯一在執(zhí)行的一個。在所有先于Dispatch Barrier的任務(wù)都完成的情況下這個block才開始執(zhí)行。輪到這個block時barrier會執(zhí)行這個block并且確保隊列在此過程 不會執(zhí)行其他任務(wù)。block完成后才恢復(fù)隊列。
_barrierQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderBarrierQueue", DISPATCH_QUEUE_CONCURRENT);
這是用戶自己創(chuàng)建的隊列,DISPATCH_QUEUE_CONCURRENT代表的是它是一個并行隊列。
為什么選擇并發(fā)隊列而不是串行隊列?
串行隊列可以保證任務(wù)按照添加順序挨個開始執(zhí)行,并且上個任務(wù)結(jié)束才開始下一個任務(wù),這已經(jīng)可以保證任務(wù)的執(zhí)行順序(或者說是任務(wù)結(jié)束的順序)了,但是并發(fā)隊列只能保證任務(wù)的開始,不能保證任務(wù)的結(jié)束順序,解決辦法就是:并發(fā)隊列使用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是一個可變字典,key是NSURL類型,value為NSMutableArray類型,value(數(shù)組里面)只包含一個元素,這個元素的類型是NSMutableDictionary類型,這個字典的key為NSString類型代表著回調(diào)類型,value為block,是對應(yīng)的回調(diào)
繼續(xù)向下看:
if (first) {
createCallback();
}
如果url第一次綁定它的回調(diào),也就是第一次使用這個url創(chuàng)建下載任務(wù),則執(zhí)行一次創(chuàng)建回調(diào)
如何確保同一url對應(yīng)的圖片不會被重復(fù)下載?
在創(chuàng)建回調(diào)中 創(chuàng)建下載操作(下載操作并不是在這里創(chuàng)建的),
dispatch_barrier_sync執(zhí)行確保同一時間只有一個線程操作URLCallbacks屬性,也就是確保了下面創(chuàng)建過程中在給operation傳遞回調(diào)的時候能取到正確的self.URLCallbacks[url]值,同時確保后面有相同的url再次創(chuàng)建的時候if (!self.URLCallbacks[url])分支不再進入,first==NO,也就不再繼續(xù)調(diào)用創(chuàng)建回調(diào),這樣就確保了同一個url對應(yīng)的圖片不會重復(fù)下載
以上這部分代碼總結(jié)起來只做了一件事情:在barrierQueue隊列中創(chuàng)建下載任務(wù)
功能
-
UIImageView,UIButton,MKAnnotationView的類別添加網(wǎng)頁圖像和緩存管理 - 異步圖像下載器
- 具有自動到期處理的異步 內(nèi)存+磁盤 緩存
- 背景圖像解壓縮
- 同一個URL不會下載多次
- 虛假無效網(wǎng)址不會重復(fù)請求
- 主線程永遠不會被阻塞
- 性能優(yōu)勢
- 用GCD 和 ARC
優(yōu)勢
相對于原生NSURLRequest的NSURLCache處理磁盤緩存,SDWebImage有什么優(yōu)勢?
從iOS 105.0起,
NSURLCache在內(nèi)存和磁盤上緩存的是原始HTTP響應(yīng)數(shù)據(jù),每次擊中緩存時,程序都必須將原始緩存數(shù)據(jù)轉(zhuǎn)換為UIImage才能使用。而這個轉(zhuǎn)化過程涉及復(fù)雜且廣泛的操作,如數(shù)據(jù)解析(解析被編碼過的HTTP數(shù)據(jù)),內(nèi)存復(fù)制等。SDWebImage采用UIImage的形式在內(nèi)存中緩存圖片數(shù)據(jù),并將已解碼過的HTTP數(shù)據(jù)的壓縮文件存儲在磁盤上。 使用NSCache將UIImage原樣存儲在內(nèi)存中,因此不會涉及任何副本,并且只要程序或系統(tǒng)需要,內(nèi)存就會被釋放。
一般第一次在
UIImageView中使用UIImage時的圖像解壓縮是在主線程完成的,而SDWebImageDecoder強制其在后臺線程中。SDWebImage完全繞過復(fù)雜且經(jīng)常配置錯誤的HTTP緩存控制協(xié)議,大大加速了緩存查找。
相對于AFNetworking為UIImageView提供的類似功能,SDWebImage有什么優(yōu)勢?
-
AFNetworking默認使用NSCache在UIKit中對UIImageView和UIButton配置內(nèi)存緩存。 -
SDWebImage同時利用Foundation框架中的NSURLCache做URL系統(tǒng)加載緩存,也用到了NSCache。AFNetworking還提供了如圖像數(shù)據(jù)的后臺解壓縮功能。 - 也就是說
AFNetworkingUIKit部分實現(xiàn)了簡單的異步圖像加載類別,是SDWebImage的部分功能
第二部分
在使用這個框架的時候,只需要提供一個下載的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"]];
在最開始先簡單介紹這個框架
這個框架的核心類是SDWebImageManger,在外部有UIImageView+WebCache 和 UIButton+WebCache為下載圖片的操作提供接口。內(nèi)部有SDWebImageManger負責(zé)處理和協(xié)調(diào) SDWebImageDownloader 和 SDWebImageCache:SDWebImageDownloader負責(zé)具體的下載任務(wù),SDWebImageCache負責(zé)關(guān)于緩存的工作:添加,刪除,查詢緩存。
首先我們大致看一下這個框架的調(diào)用流程圖:

從這個流程圖里可以大致看出,該框架分為兩個層:UIKit層(負責(zé)接收下載參數(shù))和工具層(負責(zé)下載操作和緩存)。
OK~基本流程大概清楚了,我們看一下每個層具體實現(xiàn)吧~
UIKit層
該框架最外層的類是UIImageView +WebCache,我們將圖片的URL,占位圖片直接給這個類。下面是這個類的公共接口:
// ============== 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;
可以看出,這個類提供的接口非常靈活,可以根據(jù)我們自己的需求來調(diào)用其中某一個方法,而這些方法到最后都會走到:
// ============== UIImageView + WebCache.m ============== //
- (void)sd_setImageWithURL:(NSURL *)url
placeholderImage:(UIImage *)placeholder
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionBlock)completedBlock;
而這個方法里面,調(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的分類里呢?
因為SDWebImage框架也支持UIButton的下載圖片等方法,所以需要在它們的父類:UIView里面統(tǒng)一一個下載方法。
簡單看一下這個方法的實現(xiàn)(省略的代碼用...代替):
// ============== UIView+ WebCache.m ============== //
//valid key:UIImageView || UIButton
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
//UIView+WebCacheOperation 的 operationDictionary
//下面這行代碼是保證沒有當(dāng)前正在進行的異步下載操作, 使它不會與即將進行的操作發(fā)生沖突
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
/* options & SDWebImageDelayPlaceholder這是一個位運算的與操作,
!(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能在主線程進行
dispatch_main_async_safe(^{
if (!sself) {
return;
}
/* SDWebImageAvoidAutoSetImage,默認情況下圖片會在下載完畢后自動添加
給imageView,但是有些時候我們想在設(shè)置圖片之前加一些圖片的處理,就要下
載成功后去手動設(shè)置圖片了,不會執(zhí)行` wself.image = image; `,而是直接執(zhí)行
完成回調(diào),有用戶自己決定如何處理。
*/
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
//image,而且不自動替換 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 {
//沒有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)前的操作正在進行
[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);
}
});
}
值得一提的是,在這一層,使用一個字典operationDictionary專門用作存儲操作的緩存,隨時添加,刪除操作任務(wù)。
而這個字典是UIView+WebCacheOperation分類的關(guān)聯(lián)對象,它的存取方法使用運行時來操作:
// ============== 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)這個對象呢?我覺得這里作者應(yīng)該是遵從面向?qū)ο蟮膯我宦氊?zé)原則(SRP:Single responsibility principle),就連類都要履行這個職責(zé),何況分類呢?這里作者專門創(chuàng)造一個分類UIView+WebCacheOperation來管理操作緩存(字典)。
到這里,UIKit層上面的東西都講完了,現(xiàn)在開始正式講解工具層。
工具層
上文提到過,SDWebImageManager同時管理SDImageCache和SDWebImageDownloader兩個類,它是這一層的老大哥。在下載任務(wù)開始的時候,SDWebImageManager首先訪問SDImageCache來查詢是否存在緩存,如果有緩存,直接返回緩存的圖片。如果沒有緩存,就命令SDWebImageDownloader來下載圖片,下載成功后,存入緩存,顯示圖片。以上是SDWebImageManager大致的工作流程。
在詳細講解SDWebImageManager是如何下載圖片之前,我們先看一下這個類的幾個重要的屬性:
// ============== SDWebImageManager.m ============== //
/*
*初始化方法
*1.獲得一個SDImageCache的實例
*2.獲得一個SDWebImageDownloader的實例
*3.新建一個MutableSet來存儲下載失敗的url
*4.新建一個用來存儲下載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下載圖片的方法只有一個:
[SDWebImageManager.sharedManager loadImageWithURL:options:progress:completed:]
看一下這個方法的具體實現(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) {
...
//(沒有緩存圖片) || (即使有緩存圖片,也需要更新緩存圖片) || (代理沒有響應(yīng)imageManager:shouldDownloadImageForURL:消息,默認返回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. 如果不存在緩存圖片
...
//開啟下載器下載
//subOperationToken 用來標(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. 如果有錯誤
//2.1 在completedBlock里傳入error
[self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
//2.2 在錯誤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 進行緩存
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
//(即使緩存存在,也要刷新圖片) && 緩存圖片 && 不存在下載后的圖片:不做操作
} else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
//(下載圖片成功 && (沒有動圖||處理動圖) && (下載之后,緩存之前處理圖片) 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)前運行的操作列表里移除當(dāng)前操作
if (finished) {
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];
//取消的block
operation.cancelBlock = ^{
//取消當(dāng)前的token
[self.imageDownloader cancel:subOperationToken];
__strong __typeof(weakOperation) strongOperation = weakOperation;
//從當(dāng)前運行的操作列表里移除當(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 {
//沒有緩存的圖片,而且下載被代理終止了
__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)處理,我們分別看一下
SDImageCache和SDWebImageDownloader內(nèi)部具體是如何工作的。首先看一下SDImageCache:
SDImageCache
屬性
// ============== SDImageCache.m ============== //
@property (strong, nonatomic, nonnull) NSCache *memCache;//內(nèi)存緩存
@property (strong, nonatomic, nonnull) NSString *diskCachePath;//磁盤緩存路徑
@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傳進去
if (image) {
NSData *diskData = nil;
// 如果是gif,就拿到data,后面要傳到doneBlock里。不是gif就傳nil
if ([image isGIF]) {
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
if (doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
// 因為圖片有緩存可供使用,所以不用實例化NSOperation,直接范圍nil
return nil;
}
//================查看磁盤的緩存=================//
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 被用來計算緩存中所有對象的代價。當(dāng)內(nèi)存受限或者所有緩存對象的總代價超過了最大允許的值時,緩存會移除其中的一些對象。
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;//下載隊列
@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請求頭
@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t barrierQueue;//用來阻塞前面的下載線程(串行化)
核心方法:下載圖片
- (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ù)緩存,默認網(wǎng)絡(luò)請求不進行緩存操作
// 創(chuà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;
}
// 針對每次下載操作,SD自創(chuàng)一個操作類,眾多操作放在一個操作隊列中,便于管理眾多下載操作
// 創(chuàng)建下載操作:SDWebImageDownloaderOperation用于請求網(wǎng)絡(luò)資源的操作,它是一個 NSOperation 的子類
SDWebImageDownloaderOperation *operation = [[sself.operationClass alloc] initWithRequest:request inSession:sself.session options:options];
operation.shouldDecompressImages = sself.shouldDecompressImages;
//url證書
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)先級
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}
// 在下載隊列里添加下載操作,執(zhí)行下載操作
[sself.downloadQueue addOperation:operation];
// 如果后進先出
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,實現(xiàn)了后進先出
[sself.lastAddedOperation addDependency:operation];
sself.lastAddedOperation = operation;
}
return operation;
}];
}
這里面還有一個addProgressCallback: progressBlock: completedBlock: forURL: createCallback:方法,用來保存progressBlock和completedBlock。我們看一下這個方法的實現(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 將會做回調(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實例
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
// 如果沒有,就初始化它
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 默認是一個字典,存放 progressBlock 和 completedBlock
id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
token = [SDWebImageDownloadToken new];
token.url = url;
token.downloadOperationCancelToken = downloadOperationCancelToken;
});
return token;
}
這里真正保存兩個block的方法是addHandlersForProgress: completed:
- (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
// 實例化一個SDCallbacksDictionary,存放一個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的核心方法都講解完畢了,其他沒有講到的部分以后會慢慢添加上去。
最后看一下一些比較零散的知識點:
1. 運行時存取關(guān)聯(lián)對象:
存:
// 將operations對象關(guān)聯(lián)給self,地址為&loadOperationKey,語義是OBJC_ASSOCIATION_RETAIN_NONATOMIC。
objc_setAssociatedObject(self, &loadOperationKey, operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
?。?/p>
// 將operations對象通過地址&loadOperationKey從self里取出來
SDOperationsDictionary *operations = objc_getAssociatedObject(self, &loadOperationKey);
2. 數(shù)組的寫操作需要加鎖(多線程訪問,避免覆寫)
//給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,如果是,則編譯器會有警告
5. 容錯,強制轉(zhuǎn)換類型
if ([url isKindOfClass:NSString.class]) {
url = [NSURL URLWithString:(NSString *)url];
}
在傳入的參數(shù)為NSString時(但是方法參數(shù)要求是NSURL),自動轉(zhuǎn)換為NSURL