優秀開源庫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 分類中的這個方法常用.

這個框架的設計還是極其的優雅和簡潔, 主要的功能就是這麼一行代碼, 而其中複雜的實現細節全部隱藏在這行代碼之後, 正應了那句話:

把簡潔留給別人, 把複雜留給自己.

我們已經看到了這個框架簡潔的接口, 接下來我們看一下 SDWebImage 是用什麼樣的方式優雅地實現異步加載圖片和緩存的功能呢?

複雜的實現

其實複雜只是相對於簡潔而言的, 並不是說 SDWebImage 的實現就很糟糕, 相反, 它的實現還是非常 amazing 的, 在這裏我們會忽略很多的實現細節, 並不會對每一行源代碼逐一解讀.

首先, 我們從一個很高的層次來看一下這個框架是如何組織的.

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 [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 需要在 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章