世人都說閱讀源代碼對於功力的提高是十分顯著的, 可是不少的著名開源框架源代碼動輒上萬行, 複雜度實在過高, 這裏只作基礎的分析。git
首先來介紹一下這個 SDWebImage 這個著名開源框架吧, 這個開源框架的主要做用就是:github
Asynchronous image downloader with cache support with an UIImageView category.
一個異步下載圖片而且支持緩存的 UIImageView
分類.web
就這麼直譯過來相信各位也能理解, 框架中最最經常使用的方法其實就是這個:緩存
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
固然這個框架中還有 UIButton
的分類, 能夠給 UIButton
異步加載圖片, 不過這個並無 UIImageView
分類中的這個方法經常使用.網絡
這個框架的設計仍是極其的優雅和簡潔, 主要的功能就是這麼一行代碼, 而其中複雜的實現細節所有隱藏在這行代碼以後, 正應了那句話:框架
把簡潔留給別人, 把複雜留給本身.
咱們已經看到了這個框架簡潔的接口, 接下來咱們看一下 SDWebImage
是用什麼樣的方式優雅地實現異步加載圖片和緩存的功能呢?異步
其實複雜只是相對於簡潔而言的, 並非說 SDWebImage
的實現就很糟糕, 相反, 它的實現仍是很是 amazing
的, 在這裏咱們會忽略不少的實現細節, 並不會對每一行源代碼逐一解讀.async
首先, 咱們從一個很高的層次來看一下這個框架是如何組織的.post
UIImageView+WebCache
和 UIButton+WebCache
直接爲表層的 UIKit
框架提供接口, 而 SDWebImageManger
負責處理和協調 SDWebImageDownloader
和 SDWebImageCache
. 並與 UIKit
層進行交互, 而底層的一些類爲更高層級的抽象提供支持.優化
接下來咱們就以 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
是一個宏定義, 點進去一看發現宏是這樣定義的
#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.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
會被賦值爲空, 這個下載的操做就會出錯.
當 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.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.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];
只有將它加入到這個下載隊列中, 這個操做纔會執行.
這個類就是處理 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