好文分享

iOS 源代碼分析 --- 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"]];

當(dāng)然這個框架中還有 UIButton 的分類, 可以給 UIButton 異步加載圖片, 不過這個并沒有 UIImageView 分類中的這個方法常用.

這個框架的設(shè)計還是極其的優(yōu)雅和簡潔, 主要的功能就是這么一行代碼, 而其中復(fù)雜的實現(xiàn)細節(jié)全部隱藏在這行代碼之后, 正應(yīng)了那句話:

把簡潔留給別人, 把復(fù)雜留給自己.

我們已經(jīng)看到了這個框架簡潔的接口, 接下來我們看一下 SDWebImage 是用什么樣的方式優(yōu)雅地實現(xiàn)異步加載圖片和緩存的功能呢?

復(fù)雜的實現(xiàn)

其實復(fù)雜只是相對于簡潔而言的, 并不是說 SDWebImage 的實現(xiàn)就很糟糕, 相反, 它的實現(xiàn)還是非常 amazing 的, 在這里我們會忽略很多的實現(xiàn)細節(jié), 并不會對每一行源代碼逐一解讀.

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

這張圖片已經(jīng)將這個框架是如何組織的基本展示了出來, UIImageView+WebCacheUIButton+WebCache 直接為表層的 UIKit 框架提供接口, 而 SDWebImageManger 負(fù)責(zé)處理和協(xié)調(diào) SDWebImageDownloaderSDWebImageCache. 并與 UIKit 層進行交互, 而底層的一些類為更高層級的抽象提供支持.

UIImageView+WebCache

接下來我們就以 UIImageView+WebCache 中的

- (void)sd_setImageWithURL:(NSURL *)url 
          placeholderImage:(UIImage *)placeholder;

這一方法為入口研究一下 SDWebImage 是怎樣工作的. 我們打開上面這段方法的實現(xiàn)代碼 UIImageView+WebCache.m

當(dāng)然你也可以 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];
}

這段方法唯一的作用就是調(diào)用了另一個方法

[self sd_setImageWithURL:placeholderImage:options:progress:completed:]

在這個文件中, 你會看到很多的 sd_setImageWithURL...... 方法, 它們最終都會調(diào)用上面這個方法, 只是根據(jù)需要傳入不同的參數(shù), 這在很多的開源項目中乃至我們平時寫的項目中都是很常見的. 而這個方法也是 UIImageView+WebCache 中的核心方法.

這里就不再復(fù)制出這個方法的全部實現(xiàn)了.

操作的管理

這是這個方法的第一行代碼:

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #1

[self sd_cancelCurrentImageLoad];

這行看似簡單的代碼最開始是被我忽略的, 我后來才發(fā)現(xiàn)蘊藏在這行代碼之后的思想, 也就是 SDWebImage 管理操作的辦法.

框架中的所有操作實際上都是通過一個 operationDictionary 來管理, 而這個字典實際上是動態(tài)的添加到 UIView 上的一個屬性, 至于為什么添加到 UIView 上, 主要是因為這個 operationDictionary 需要在 UIButtonUIImageView 上重用, 所以需要添加到它們的根類上.

這行代碼是要保證沒有當(dāng)前正在進行的異步下載操作, 不會與即將進行的操作發(fā)生沖突, 它會調(diào)用:

// UIImageView+WebCache
// sd_cancelCurrentImageLoad #1

[self sd_cancelImageLoadOperationWithKey:@"UIImageViewImageLoad"]

而這個方法會使當(dāng)前 UIImageView 中的所有操作都被 cancel. 不會影響之后進行的下載操作.


占位圖的實現(xiàn)

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #4

if (!(options & SDWebImageDelayPlaceholder)) {
    self.image = placeholder;
}

如果傳入的 options 中沒有 SDWebImageDelayPlaceholder(默認(rèn)情況下 options == 0), 那么就會為 UIImageView 添加一個臨時的 image, 也就是占位圖.


獲取圖片

// UIImageView+WebCache
// sd_setImageWithURL:placeholderImage:options:progress:completed: #8

if (url)

接下來會檢測傳入的 url 是否非空, 如果非空那么一個全局的 SDWebImageManager 就會調(diào)用以下的方法獲取圖片:

[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]

下載完成后會調(diào)用 (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 是一個宏定義, 點進去一看發(fā)現(xiàn)宏是這樣定義的

#define dispatch_main_sync_safe(block)\
    if ([NSThread isMainThread]) {\
        block();\
    } else {\
        dispatch_sync(dispatch_get_main_queue(), block);\
    }

相信這個宏的名字已經(jīng)講他的作用解釋的很清楚了: 因為圖像的繪制只能在主線程完成, 所以, dispatch_main_sync_safe 就是為了保證 block 能在主線程中執(zhí)行.


而最后, 在 [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.

到此為止我們已經(jīng)對 SDWebImage 框架中的這一方法分析完了, 接下來我們將要分析 SDWebImageManager 中的方法

[SDWebImageManager.sharedManager downloadImageWithURL:options:progress:completed:]

SDWebImageManager

SDWebImageManager.h 中你可以看到關(guān)于 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 背后, 用于處理異步下載和圖片緩存的類, 當(dāng)然你也可以直接使用 SDWebImageManager 的上述方法 downloadImageWithURL:options:progress:completed: 來直接下載圖片.

可以看到, 這個類的主要作用就是為 UIImageView+WebCacheSDWebImageDownloader, SDImageCache 之間構(gòu)建一個橋梁, 使它們能夠更好的協(xié)同工作, 我們在這里分析這個核心方法的源代碼, 它是如何協(xié)調(diào)異步下載和圖片緩存的.

// SDWebImageManager
// downloadImageWithURL:options:progress:completed: #6

if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}

if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}

這塊代碼的功能是確定 url 是否被正確傳入, 如果傳入?yún)?shù)的是 NSString 類型就會被轉(zhuǎn)換為 NSURL. 如果轉(zhuǎn)換失敗, 那么 url 會被賦值為空, 這個下載的操作就會出錯.


SDWebImageCombinedOperation

當(dāng) url 被正確傳入之后, 會實例一個非常奇怪的 "operation", 它其實是一個遵循 SDWebImageOperation 協(xié)議的 NSObject 的子類. 而這個協(xié)議也非常的簡單:

@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

這里僅僅是將這個 SDWebImageOperation 類包裝成一個看著像 NSOperation 其實并不是 NSOperation 的類, 而這個類唯一與 NSOperation 的相同之處就是它們都可以響應(yīng) cancel 方法. (不知道這句看似像繞口令的話, 你看懂沒有, 如果沒看懂..請多讀幾遍).

而調(diào)用這個類的存在實際是為了使代碼更加的簡潔, 因為調(diào)用這個類的 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;
    }
}

而這個類, 應(yīng)該是為了實現(xiàn)更簡潔的 cancel 操作而設(shè)計出來的.


既然我們獲取了 url, 再通過 url 獲取對應(yīng)的 key

NSString *key = [self cacheKeyForURL:url];

下一步是使用 key 在緩存中查找以前是否下載過相同的圖片.

operation.cacheOperation = [self.imageCache 
        queryDiskCacheForKey:key 
                        done:^(UIImage *image, SDImageCacheType cacheType) { ... }];

這里調(diào)用 SDImageCache 的實例方法 queryDiskCacheForKey:done: 來嘗試在緩存中獲取圖片的數(shù)據(jù). 而這個方法返回的就是貨真價實的 NSOperation.

如果我們在緩存中查找到了對應(yīng)的圖片, 那么我們直接調(diào)用 completedBlock 回調(diào)塊結(jié)束這一次的圖片下載操作.

// SDWebImageManager
// downloadImageWithURL:options:progress:completed: #47

dispatch_main_sync_safe(^{
    completedBlock(image, nil, cacheType, YES, url);
});

如果我們沒有找到圖片, 那么就會調(diào)用 SDWebImageDownloader 的實例方法:

id <SDWebImageOperation> subOperation =
  [self.imageDownloader downloadImageWithURL:url 
                                     options:downloaderOptions 
                                    progress:progressBlock 
                                   completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) { ... }];

如果這個方法返回了正確的 downloadedImage, 那么我們就會在全局的緩存中存儲這個圖片的數(shù)據(jù):

[self.imageCache storeImage:downloadedImage 
       recalculateFromImage:NO 
                  imageData:data 
                     forKey:key 
                     toDisk:cacheOnDisk];

并調(diào)用 completedBlockUIImageView 或者 UIButton 添加圖片, 或者進行其它的操作.

最后, 我們將這個 subOperationcancel 操作添加到 operation.cancelBlock 中. 方便操作的取消.

operation.cancelBlock = ^{
    [subOperation cancel];
    }

SDWebImageCache

SDWebImageCache.h 這個類在源代碼中有這樣的注釋:

SDImageCache maintains a memory cache and an optional disk cache.

它維護了一個內(nèi)存緩存和一個可選的磁盤緩存, 我們先來看一下在上一階段中沒有解讀的兩個方法, 首先是:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key 
                                 done:(SDWebImageQueryCompletedBlock)doneBlock;

這個方法的主要功能是異步的查詢圖片緩存. 因為圖片的緩存可能在兩個地方, 而該方法首先會在內(nèi)存中查找是否有圖片的緩存.

// SDWebImageCache
// queryDiskCacheForKey:done: #9

UIImage *image = [self imageFromMemoryCacheForKey:key];

這個 imageFromMemoryCacheForKey 方法會在 SDWebImageCache 維護的緩存 memCache 中查找是否有對應(yīng)的數(shù)據(jù), 而 memCache 就是一個 NSCache.

如果在內(nèi)存中并沒有找到圖片的緩存的話, 就需要在磁盤中尋找了, 這個就比較麻煩了..

在這里會調(diào)用一個方法 diskImageForKey 這個方法的具體實現(xiàn)我在這里就不介紹了, 涉及到很多底層 Core Foundation 框架的知識, 不過這里文件名字的存儲使用 MD5 處理過后的文件名.

// SDImageCache
// cachedFileNameForKey: #6

CC_MD5(str, (CC_LONG)strlen(str), r);

對于其它的實現(xiàn)細節(jié)也就不多說了...

如果在磁盤中查找到對應(yīng)的圖片, 我們會將它復(fù)制到內(nèi)存中, 以便下次的使用.

// 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 的核心內(nèi)容了, 而接下來將介紹如果緩存沒有命中, 圖片是如何被下載的.

SDWebImageDownloader

按照之前的慣例, 我們先來看一下 SDWebImageDownloader.h 中對這個類的描述.

Asynchronous downloader dedicated and optimized for image loading.

專用的并且優(yōu)化的圖片異步下載器.

這個類的核心功能就是下載圖片, 而核心方法就是上面提到的:

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url 
        options:(SDWebImageDownloaderOptions)options 
       progress:(SDWebImageDownloaderProgressBlock)progressBlock 
      completed:(SDWebImageDownloaderCompletedBlock)completedBlock;

回調(diào)

這個方法直接調(diào)用了另一個關(guān)鍵的方法:

- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock 
          andCompletedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock 
                     forURL:(NSURL *)url 
             createCallback:(SDWebImageNoParamsBlock)createCallback

它為這個下載的操作添加回調(diào)的塊, 在下載進行時, 或者在下載結(jié)束時執(zhí)行一些操作, 先來閱讀一下這個方法的源代碼:

// 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 是否有對應(yīng)的 callback, 使用的是 downloader 持有的一個字典 URLCallbacks.

如果是第一次添加回調(diào)的話, 就會執(zhí)行 first = YES, 這個賦值非常的關(guān)鍵, 因為 first 不為 YES 那么 HTTP 請求就不會被初始化, 圖片也無法被獲取.

然后, 在這個方法中會重新修正在 URLCallbacks 中存儲的回調(diào)塊.

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;

如果是第一次添加回調(diào)塊, 那么就會直接運行這個 createCallback 這個 block, 而這個 block, 就是我們在前一個方法 downloadImageWithURL:options:progress:completed: 中傳入的回調(diào)塊.

// SDWebImageDownloader
// downloadImageWithURL:options:progress:completed: #4

[self addProgressCallback:progressBlock andCompletedBlock:completedBlock forURL:url createCallback:^{ ... }];

我們下面來分析這個傳入的無參數(shù)的代碼. 首先這段代碼初始化了一個 NSMutableURLRequest:

// SDWebImageDownloader
// downloadImageWithURL:options:progress:completed: #11

NSMutableURLRequest *request = [[NSMutableURLRequest alloc] 
        initWithURL:url 
        cachePolicy:...
    timeoutInterval:timeoutInterval];

這個 request 就用于在之后發(fā)送 HTTP 請求.

在初始化了這個 request 之后, 又初始化了一個 SDWebImageDownloaderOperation 的實例, 這個實例, 就是用于請求網(wǎng)絡(luò)資源的操作. 它是一個 NSOperation 的子類,

// SDWebImageDownloader
// downloadImageWithURL:options:progress:completed: #20

operation = [[SDWebImageDownloaderOperation alloc] 
        initWithRequest:request
                options:options
               progress:...
              completed:...
              cancelled:...}];

但是在初始化之后, 這個操作并不會開始(NSOperation 實例只有在調(diào)用 start 方法或者加入 NSOperationQueue 才會執(zhí)行), 我們需要將這個操作加入到一個 NSOperationQueue 中.

// SDWebImageDownloader
// downloadImageWithURL:options:progress:completed: #59

[wself.downloadQueue addOperation:operation];

只有將它加入到這個下載隊列中, 這個操作才會執(zhí)行.

SDWebImageDownloaderOperation

這個類就是處理 HTTP 請求, URL 連接的類, 當(dāng)這個類的實例被加入隊列之后, start 方法就會被調(diào)用, 而 start 方法首先就會產(chǎn)生一個 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];

它會發(fā)出一個 SDWebImageDownloadStartNotification 通知

// SDWebImageDownloaderOperation
// start #35

[[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];

代理

start 方法調(diào)用之后, 就是 NSURLConnectionDataDelegate 中代理方法的調(diào)用.

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response;
- (void)connectionDidFinishLoading:(NSURLConnection *)aConnection;

在這三個代理方法中的前兩個會不?;卣{(diào) progressBlock 來提示下載的進度.

而最后一個代理方法會在圖片下載完成之后調(diào)用 completionBlock 來完成最后 UIImageView.image 的更新.

而這里調(diào)用的 progressBlock completionBlock cancelBlock 都是在之前存儲在 URLCallbacks 字典中的.


到目前為止, 我們就基本解析了 SDWebImage

[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"url"]
                  placeholderImage:[UIImage imageNamed:@"placeholder.png"]];

這個方法執(zhí)行的全部過程了.

流程圖

我們使用一個流程圖來表示上述方法所執(zhí)行的全過程.

這段流程圖展示了上述方法調(diào)用中大部分重要方法的調(diào)用.

SDWebImage 如何為 UIImageView 添加圖片(面試回答)

SDWebImage 中為 UIView 提供了一個分類叫做 WebCache, 這個分類中有一個最常用的接口, sd_setImageWithURL:placeholderImage:, 這個分類同時提供了很多類似的方法, 這些方法最終會調(diào)用一個同時具有 option progressBlock completionBlock 的方法, 而在這個類最終被調(diào)用的方法首先會檢查是否傳入了 placeholderImage 以及對應(yīng)的參數(shù), 并設(shè)置 placeholderImage.

然后會獲取 SDWebImageManager 中的單例調(diào)用一個 downloadImageWithURL:... 的方法來獲取圖片, 而這個 manager 獲取圖片的過程有大體上分為兩部分, 它首先會在 SDWebImageCache 中尋找圖片是否有對應(yīng)的緩存, 它會以 url 作為數(shù)據(jù)的索引先在內(nèi)存中尋找是否有對應(yīng)的緩存, 如果緩存未命中就會在磁盤中利用 MD5 處理過的 key 來繼續(xù)查詢對應(yīng)的數(shù)據(jù), 如果找到了, 就會把磁盤中的緩存?zhèn)浞莸絻?nèi)存中.

然而, 假設(shè)我們在內(nèi)存和磁盤緩存中都沒有命中, 那么 manager 就會調(diào)用它持有的一個 SDWebImageDownloader 對象的方法 downloadImageWithURL:... 來下載圖片, 這個方法會在執(zhí)行的過程中調(diào)用另一個方法 addProgressCallback:andCompletedBlock:forURL:createCallback: 來存儲下載過程中和下載完成的回調(diào), 當(dāng)回調(diào)塊是第一次添加的時候, 方法會實例化一個 NSMutableURLRequestSDWebImageDownloaderOperation, 并將后者加入 downloader 持有的下載隊列開始圖片的異步下載.

而在圖片下載完成之后, 就會在主線程設(shè)置 image 屬性, 完成整個圖像的異步下載和配置.

總結(jié)

SDWebImage 的圖片加載過程其實很符合我們的直覺:

  • 查看緩存
    • 緩存命中
      • 返回圖片
      • 更新 UIImageView
    • 緩存未命中
      • 異步下載圖片
      • 加入緩存
      • 更新 UIImageView

只要有足夠的耐心, 閱讀這個開源項目的源代碼也是沒有太多困難的.

SDWebImage 的探索到目前為止差不多結(jié)束了, 對于這個框架的學(xué)習(xí)以及解析確實使我受益匪淺, 我也將在之后繼續(xù)閱讀其它的著名框架的源代碼. 在這篇博客之中難免會有錯誤, 希望各位能夠指正~

Follow: @Draveness

本文章來自:https://github.com/liyuechun/iOS-Source-Code-Analyze

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容