優秀開源庫SDWebImage原始碼淺析
世人都說閱讀原始碼對於功力的提升是十分顯著的, 但是很多的著名開源框架原始碼動輒上萬行, 複雜度實在太高, 這裡只做基礎的分析。
簡潔的介面
首先來介紹一下這個 SDWebImage 這個著名開源框架吧, 這個開源框架的主要作用就是:
Asynchronous image downloader with cache support with an UIImageView category.
一個非同步下載圖片並且支援快取的UIImageView
分類.
就這麼直譯過來相信各位也能理解, 框架中最最常用的方法其實就是這個:
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]]; 複製程式碼
當然這個框架中還有UIButton
的分類, 可以給UIButton
非同步載入圖片, 不過這個並沒有UIImageView
分類中的這個方法常用.
這個框架的設計還是極其的優雅和簡潔, 主要的功能就是這麼一行程式碼, 而其中複雜的實現細節全部隱藏在這行程式碼之後, 正應了那句話:
把簡潔留給別人, 把複雜留給自己 .
我們已經看到了這個框架簡潔的介面, 接下來我們看一下SDWebImag
e 是用什麼樣的方式優雅地實現非同步載入圖片和快取的功能呢?
複雜的實現
其實複雜只是相對於簡潔而言的, 並不是說SDWebImage
的實現就很糟糕, 相反, 它的實現還是非常amazing
的, 在這裡我們會忽略很多的實現細節, 並不會對每一行原始碼逐一解讀.
首先, 我們從一個很高的層次來看一下這個框架是如何組織的.
UIImageView+WebCache
和UIButton+WebCache
直接為表層的UIKit
框架提供介面, 而SDWebImageManger
負責處理和協調SDWebImageDownloader
和SDWebImageCache
. 並與UIKit
層進行互動, 而底層的一些類為更高層級的抽象提供支援.
UIImageView+WebCache
接下來我們就以UIImageView+WebCache
中的
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder; 複製程式碼
這一方法為入口研究一下SDWebImage
是怎樣工作的. 我們開啟上面這段方法的實現程式碼UIImageView+WebCache.m
當然你也可以git clone [email protected]:rs/SDWebImage.git
到本地來檢視.
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil]; } 複製程式碼
這段方法唯一的作用就是呼叫了另一個方法
[self sd_setImageWithURL:placeholderImage:options:progress:completed:] 複製程式碼
在這個檔案中, 你會看到很多的sd_setImageWithURL......
方法, 它們最終都會呼叫上面這個方法, 只是根據需要傳入不同的引數, 這在很多的開源專案中乃至我們平時寫的專案中都是很常見的. 而這個方法也是UIImageView+WebCache
中的核心方法.
這裡就不再複製出這個方法的全部實現了.
操作的管理
這是這個方法的第一行程式碼:
// UIImageView+WebCache // sd_setImageWithURL:placeholderImage:options:progress:completed: #1 [self sd_cancelCurrentImageLoad]; 複製程式碼
這行看似簡單的程式碼最開始是被我忽略的, 我後來才發現蘊藏在這行程式碼之後的思想, 也就是SDWebImage
管理操作的辦法.
框架中的所有操作實際上都是通過一個operationDictionary
來管理, 而這個字典實際上是動態的新增到UIView
上的一個屬性, 至於為什麼新增到UIView
上, 主要是因為這個operationDictionary
需要在UIButton
和UIImageView
上重用, 所以需要新增到它們的根類上.
這行程式碼是要保證沒有當前正在進行的非同步下載操作, 不會與即將進行的操作發生衝突, 它會呼叫:
// UIImageView+WebCache // sd_cancelCurrentImageLoad #1 [self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"] 複製程式碼
而這個方法會使當前UIImageView
中的所有操作都被cancel
. 不會影響之後進行的下載操作.
佔位圖的實現
// UIImageView+WebCache // sd_setImageWithURL:placeholderImage:options:progress:completed: #4 if (!(options & SDWebImageDelayPlaceholder)) { self.image = placeholder; } 複製程式碼
如果傳入的options
中沒有SDWebImageDelayPlaceholder
(預設情況下options == 0
), 那麼就會為UIImageView
新增一個臨時的image
, 也就是佔位圖.
獲取圖片
// UIImageView+WebCache // sd_setImageWithURL:placeholderImage:options:progress:completed: #8 if (url) 複製程式碼
接下來會檢測傳入的url
是否非空, 如果非空那麼一個全域性的SDWebImageManager
就會呼叫以下的方法獲取圖片:
[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:] 複製程式碼
下載完成後會呼叫(SDWebImageCompletionWithFinishedBlock)completedBlock
為UIImageView.image
賦值, 新增上最終所需要的圖片.
// UIImageView+WebCache // sd_setImageWithURL:placeholderImage:options:progress:completed: #10 dispatch_main_sync_safe(^{ if (!wself) return; if (image) { wself.image = image; [wself setNeedsLayout]; } else { if ((options & SDWebImageDelayPlaceholder)) { wself.image = placeholder; [wself setNeedsLayout]; } } if (completedBlock && finished) { completedBlock(image, error, cacheType, url); } }); 複製程式碼
dispatch_main_sync_safe 巨集定義
上述程式碼中的dispatch_main_sync_safe
是一個巨集定義, 點進去一看發現巨集是這樣定義的
#define dispatch_main_sync_safe(block)\ if ([NSThread isMainThread]) {\ block();\ } else {\ dispatch_sync(dispatch_get_main_queue(), block);\ } 複製程式碼
相信這個巨集的名字已經講他的作用解釋的很清楚了: 因為影象的繪製只能在主執行緒完成, 所以,dispatch_main_sync_safe
就是為了保證block
能在主執行緒中執行.
而最後, 在[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]
返回operation
的同時, 也會向operationDictionary
中新增一個鍵值對, 來表示操作的正在進行:
// UIImageView+WebCache // sd_setImageWithURL:placeholderImage:options:progress:completed: #28 [self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"]; 複製程式碼
它將opertion
儲存到operationDictionary
中方便以後的cancel
.
到此為止我們已經對SDWebImage
框架中的這一方法分析完了, 接下來我們將要分析SDWebImageManager
中的方法
[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:] 複製程式碼
SDWebImageManager
在SDWebImageManager.h
中你可以看到關於SDWebImageManager
的描述:
The SDWebImageManager is the class behind the UIImageView+WebCache category and likes. It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache). You can use this class directly to benefit from web image downloading with caching in another context than a UIView.
這個類就是隱藏在UIImageView+WebCache
背後, 用於處理非同步下載和圖片快取的類, 當然你也可以直接使用SDWebImageManager
的上述方法downloadImageWithURL:options:progress:completed:
來直接下載圖片.
可以看到, 這個類的主要作用就是為UIImageView+WebCache
和SDWebImageDownloader
,SDImageCache
之間構建一個橋樑, 使它們能夠更好的協同工作, 我們在這裡分析這個核心方法的原始碼, 它是如何協調非同步下載和圖片快取的.
// SDWebImageManager // downloadImageWithURL:options:progress:completed: #6 if ([url isKindOfClass:NSString.class]) { url = [NSURL URLWithString:(NSString *)url]; } if (![url isKindOfClass:NSURL.class]) { url = nil; } 複製程式碼
這塊程式碼的功能是確定url
是否被正確傳入, 如果傳入引數的是NSString
型別就會被轉換為NSURL
. 如果轉換失敗, 那麼url
會被賦值為空, 這個下載的操作就會出錯.
SDWebImageCombinedOperation
當url
被正確傳入之後, 會例項一個非常奇怪的 “operation”, 它其實是一個遵循SDWebImageOperation
協議的NSObject
的子類. 而這個協議也非常的簡單:
@protocol SDWebImageOperation <NSObject> - (void)cancel; @end 複製程式碼
這裡僅僅是將這個SDWebImageOperation
類包裝成一個看著像NSOperation
其實並不是NSOperation
的類, 而這個類唯一與NSOperation
的相同之處就是它們都可以響應cancel
方法. (不知道這句看似像繞口令的話, 你看懂沒有, 如果沒看懂..請多讀幾遍).
而呼叫這個類的存在實際是為了使程式碼更加的簡潔, 因為呼叫這個類的cancel
方法, 會使得它持有的兩個operation
都被cancel
.
// SDWebImageCombinedOperation // cancel #1 - (void)cancel { self.cancelled = YES; if (self.cacheOperation) { [self.cacheOperation cancel]; self.cacheOperation = nil; } if (self.cancelBlock) { self.cancelBlock(); _cancelBlock = nil; } } 複製程式碼
而這個類, 應該是為了實現更簡潔的cancel
操作而設計出來的.
既然我們獲取了url
, 再通過url
獲取對應的key
NSString *key = [self cacheKeyForURL:url];
下一步是使用key
在快取中查詢以前是否下載過相同的圖片.
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) { ... }]; 複製程式碼
這裡呼叫SDImageCache
的例項方法queryDiskCacheForKey:done:
來嘗試在快取中獲取圖片的資料. 而這個方法返回的就是貨真價實的NSOperation.
如果我們在快取中查詢到了對應的圖片, 那麼我們直接呼叫completedBlock
回撥塊結束這一次的圖片下載操作.
// SDWebImageManager // downloadImageWithURL:options:progress:completed: #47 dispatch_main_sync_safe(^{ completedBlock(image, nil, cacheType, YES, url); }); 複製程式碼
如果我們沒有找到圖片, 那麼就會呼叫SDWebImageDownloader
的例項方法:
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { ... }]; 複製程式碼
如果這個方法返回了正確的downloadedImage
, 那麼我們就會在全域性的快取中儲存這個圖片的資料:
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; 複製程式碼
並呼叫completedBlock
對UIImageView
或者UIButton
新增圖片, 或者進行其它的操作.
最後, 我們將這個subOperation
的cancel
操作新增到operation.cancelBlock
中. 方便操作的取消.
operation.cancelBlock = ^{ [subOperation cancel]; } 複製程式碼
SDWebImageCache
SDWebImageCache.h 這個類在原始碼中有這樣的註釋:
SDImageCache maintains a memory cache and an optional disk cache.
它維護了一個記憶體快取和一個可選的磁碟快取, 我們先來看一下在上一階段中沒有解讀的兩個方法, 首先是:
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock; 複製程式碼
這個方法的主要功能是非同步的查詢圖片快取. 因為圖片的快取可能在兩個地方, 而該方法首先會在記憶體中查詢是否有圖片的快取.
// SDWebImageCache // queryDiskCacheForKey:done: #9 UIImage *image = [self imageFromMemoryCacheForKey:key]; 複製程式碼
這個imageFromMemoryCacheForKey
方法會在SDWebImageCache
維護的快取memCache
中查詢是否有對應的資料, 而memCache
就是一個NSCache.
如果在記憶體中並沒有找到圖片的快取的話, 就需要在磁碟中尋找了, 這個就比較麻煩了..
在這裡會呼叫一個方法diskImageForKey
這個方法的具體實現我在這裡就不介紹了, 涉及到很多底層Core Foundation
框架的知識, 不過這裡檔名字的儲存使用MD5
處理過後的檔名.
// SDImageCache // cachedFileNameForKey: #6 CC_MD5(str, (CC_LONG)strlen(str), r); 複製程式碼
對於其它的實現細節也就不多說了…
如果在磁碟中查詢到對應的圖片, 我們會將它複製到記憶體中, 以便下次的使用.
// SDImageCache // queryDiskCacheForKey:done: #24 UIImage *diskImage = [self diskImageForKey:key]; if (diskImage) { CGFloat cost = diskImage.size.height * diskImage.size.width * diskImage.scale; [self.memCache setObject:diskImage forKey:key cost:cost]; } 複製程式碼
這些就是SDImageCache
的核心內容了, 而接下來將介紹如果快取沒有命中, 圖片是如何被下載的.
SDWebImageDownloader
按照之前的慣例, 我們先來看一下SDWebImageDownloader.h 中對這個類的描述.
Asynchronous downloader dedicated and optimized for image loading.
專用的並且優化的圖片非同步下載器.
這個類的核心功能就是下載圖片, 而核心方法就是上面提到的:
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock; 複製程式碼
回撥
這個方法直接呼叫了另一個關鍵的方法:
- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback 複製程式碼
它為這個下載的操作添加回調的塊, 在下載進行時, 或者在下載結束時執行一些操作, 先來閱讀一下這個方法的原始碼:
// SDWebImageDownloader // addProgressCallback:andCompletedBlock:forURL:createCallback: #10 BOOL first = NO; if (!self.URLCallbacks[url]) { self.URLCallbacks[url] = [NSMutableArray new]; first = YES; } // Handle single download of simultaneous download request for the same URL 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; if (first) { createCallback(); } 複製程式碼
方法會先檢視這個url
是否有對應的callback
, 使用的是downloader
持有的一個字典URLCallbacks
.
如果是第一次添加回調的話, 就會執行first = YES
, 這個賦值非常的關鍵, 因為first
不為YES
那麼HTTP
請求就不會被初始化, 圖片也無法被獲取.
然後, 在這個方法中會重新修正在URLCallbacks
中儲存的回撥塊.
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; 複製程式碼
如果是第一次添加回調塊, 那麼就會直接執行這個createCallback
這個block
, 而這個block
, 就是我們在前一個方法downloadImageWithURL:options:progress:completed:
中傳入的回撥塊.
// SDWebImageDownloader // downloadImageWithURL:options:progress:completed: #4 [self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{ ... }]; 複製程式碼
我們下面來分析這個傳入的無引數的程式碼. 首先這段程式碼初始化了一個NSMutableURLRequest
:
// SDWebImageDownloader // downloadImageWithURL:options:progress:completed: #11 NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:... timeoutInterval:timeoutInterval]; 複製程式碼
這個request
就用於在之後傳送HTTP
請求.
在初始化了這個request
之後, 又初始化了一個SDWebImageDownloaderOperation
的例項, 這個例項, 就是用於請求網路資源的操作. 它是一個NSOperation
的子類,
// SDWebImageDownloader // downloadImageWithURL:options:progress:completed: #20 operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request options:options progress:... completed:... cancelled:...}]; 複製程式碼
但是在初始化之後, 這個操作並不會開始(NSOperation
例項,只有在呼叫start
方法或者加入NSOperationQueue
才會執行), 我們需要將這個操作加入到一個NSOperationQueue
中.
// SDWebImageDownloader // downloadImageWithURL:option:progress:completed: #59 [wself.downloadQueue addOperation:operation]; 複製程式碼
只有將它加入到這個下載佇列中, 這個操作才會執行.
SDWebImageDownloaderOperation
這個類就是處理HTTP
請求,URL
連線的類, 當這個類的例項被加入佇列之後,start
方法就會被呼叫, 而start
方法首先就會產生一個NSURLConnection
.
// SDWebImageDownloaderOperation // start #1 @synchronized (self) { if (self.isCancelled) { self.finished = YES; [self reset]; return; } self.executing = YES; self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; self.thread = [NSThread currentThread]; } 複製程式碼
而接下來這個connection
就會開始執行:
// SDWebImageDownloaderOperation // start #29 [self.connection start]; 複製程式碼
它會發出一個SDWebImageDownloadStartNotification
通知
// SDWebImageDownloaderOperation // start #35 [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self]; 複製程式碼
代理
在start
方法呼叫之後, 就是NSURLConnectionDataDelegate
中代理方法的呼叫.
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response; - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection; 複製程式碼
在這三個代理方法中的前兩個會不停回撥progressBlock
來提示下載的進度.
而最後一個代理方法會在圖片下載完成之後呼叫completionBlock
來完成最後UIImageView.image
的更新.
而這裡呼叫的progressBlock
completionBlock
cancelBlock
都是在之前儲存在URLCallbacks
字典中的.
到目前為止, 我們就基本解析了SDWebImage
中
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]]; 複製程式碼
這個方法執行的全部過程了.
總結
SDWebImage
的圖片載入過程其實很符合我們的直覺:
檢視快取
快取命中 * 返回圖片
更新UIImageView
快取未命中 * 非同步下載圖片
加入快取
更新UIImageView