優秀開源庫SDWebImage源碼淺析

世人都說閱讀源代碼對於功力的提高是十分顯著的, 可是不少的著名開源框架源代碼動輒上萬行, 複雜度實在過高, 這裏只作基礎的分析。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+WebCacheUIButton+WebCache 直接爲表層的 UIKit 框架提供接口, 而 SDWebImageManger 負責處理和協調 SDWebImageDownloaderSDWebImageCache. 並與 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 須要在 UIButtonUIImageView 上重用, 因此須要添加到它們的根類上.

這行代碼是要保證沒有當前正在進行的異步下載操做, 不會與即將進行的操做發生衝突, 它會調用:

// 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+WebCacheSDWebImageDownloader, 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];

並調用 completedBlockUIImageView 或者 UIButton 添加圖片, 或者進行其它的操做.

最後, 咱們將這個 subOperationcancel 操做添加到 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

相關文章
相關標籤/搜索