優秀開源庫SDWebImage原始碼淺析

蘆葦科技App技術團隊發表於2018-11-29

世人都說閱讀原始碼對於功力的提升是十分顯著的, 但是很多的著名開源框架原始碼動輒上萬行, 複雜度實在太高, 這裡只做基礎的分析。

簡潔的介面

首先來介紹一下這個 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 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)completedBlockUIImageView.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

相關文章