世人都說閱讀原始碼對於功力的提升是十分顯著的, 但是很多的著名開源框架原始碼動輒上萬行, 複雜度實在太高, 這裡只做基礎的分析。
簡潔的介面
首先來介紹一下這個 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
分類中的這個方法常用.
這個框架的設計還是極其的優雅和簡潔, 主要的功能就是這麼一行程式碼, 而其中複雜的實現細節全部隱藏在這行程式碼之後, 正應了那句話:
把簡潔留給別人, 把複雜留給自己.
我們已經看到了這個框架簡潔的介面, 接下來我們看一下 SDWebImage
是用什麼樣的方式優雅地實現非同步載入圖片和快取的功能呢?
複雜的實現
其實複雜只是相對於簡潔而言的, 並不是說 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 git@github.com: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