SDWebImage源碼解析(一)

SDWebImage是一個圖片下載的開源項目,由于它提供了簡介的接口以及異步下載與緩存的強大功能,深受“猿媛“的喜愛。截止到本篇文章開始,項目的star數已經超過1.6k了。今天我就對項目的源碼做個閱讀筆記,一方面歸納總結自己的心得,另一方面給準備閱讀源碼的童鞋做點鋪墊工作。代碼最新版本為3.8。

正如項目的第一句介紹一樣:

Asynchronous image downloader with cache support as a UIImageView category

SDWebImage是個支持異步下載與緩存的UIImageView擴展。項目主要提供了一下功能:

  • 擴展UIImageView, UIButton, MKAnnotationView,增加網絡圖片與緩存管理。
  • 一個異步的圖片加載器
  • 一個異步的 內存 + 磁盤 圖片緩存,擁有自動的緩存過期處理機制。
  • 支持后臺圖片解壓縮處理
  • 確保同一個 URL 的圖片不被多次下載
  • 確保虛假的 URL 不會被反復加載
  • 確保下載及緩存時,主線程不被阻塞
  • 使用 GCD 與 ARC

項目支持的圖片格式包括PNG,JEPG,GIF,WebP等等。

先看看SDWebImage的項目組織架構:

SDWebImage組織架構.png

SDWebImageDownloader負責維持圖片的下載隊列;
SDWebImageDownloaderOperation負責真正的圖片下載請求;
SDImageCache負責圖片的緩存;
SDWebImageManager是總的管理類,維護了一個SDWebImageDownloader實例和一個SDImageCache實例,是下載與緩存的橋梁;
SDWebImageDecoder負責圖片的解壓縮;
SDWebImagePrefetcher負責圖片的預取;
UIImageView+WebCache和其他的擴展都是與用戶直接打交道的。

其中,最重要的三個類就是SDWebImageDownloaderSDImageCacheSDWebImageManager。接下來我們就分別詳細地研究一下這些類各自具體做了哪些事,又是怎么做的。

為了便于大家從宏觀上有個把握,我這里先給出項目的框架結構:

Paste_Image.png

UIImageView+WebCacheUIButton+WebCache直接為表層的 UIKit框架提供接口, 而 SDWebImageManger負責處理和協調SDWebImageDownloaderSDWebImageCache, 并與 UIKit層進行交互。SDWebImageDownloaderOperation真正執行下載請求;最底層的兩個類為高層抽象提供支持。
我們按照從上到下執行的流程來研究各個類

UIImageView+WebCache

這里,我們只用UIImageView+WebCache來舉個例子,其他的擴展類似。
常用的場景是已知圖片的url地址,來下載圖片并設置到UIImageView上。UIImageView+WebCache提供了一系列的接口:

- (void)setImageWithURL:(NSURL *)url;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options;
- (void)setImageWithURL:(NSURL *)url completed:(SDWebImageCompletedBlock)completedBlock;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder completed:(SDWebImageCompletedBlock)completedBlock;
- (void)setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options completed:(SDWebImageCompletedBlock)completedBlock;

這些接口最終會調用

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock;

方法的第一行代碼[self sd_cancelCurrentImageLoad]是取消UIImageView上當前正在進行的異步下載,確保每個 UIImageView 對象中永遠只存在一個 operation,當前只允許一個圖片網絡請求,該 operation 負責從緩存中獲取 image 或者是重新下載 image。具體執行代碼是:

// UIView+WebCacheOperation.m
// Cancel in progress downloader from queue
NSMutableDictionary *operationDictionary = [self operationDictionary];
id operations = [operationDictionary objectForKey:key];
if (operations) {
    if ([operations isKindOfClass:[NSArray class]]) {
        for (id <SDWebImageOperation> operation in operations) {
            if (operation) {
                [operation cancel];
            }
        }
    } else if ([operations conformsToProtocol:@protocol(SDWebImageOperation)]){
        [(id<SDWebImageOperation>) operations cancel];
    }
    [operationDictionary removeObjectForKey:key];
}

實際上,所有的操作都是由一個operationDictionary字典維護的,執行新的操作之前,先cancel所有的operation。這里的cancel是SDWebImageOperation協議里面定義的。

//預覽 占位圖
    if (!(options & SDWebImageDelayPlaceholder)) {
        dispatch_main_async_safe(^{
            self.image = placeholder;
        });
    }

是一種占位圖策略,作為圖片下載完成之前的替代圖片。dispatch_main_async_safe是一個宏,保證在主線程安全執行,最后再講。
然后判斷url,url為空就直接調用完成回調,報告錯誤信息;否則,用SDWebImageManager單例的

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock

方法下載圖片。下載完成之后刷新UIImageView的圖片。

//圖像的繪制只能在主線程完成
dispatch_main_sync_safe(^{
    if (!wself) return;
        if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock)
        {//延遲設置圖片,手動處理
            completedBlock(image, error, cacheType, url);
            return;
        } else if (image) {
             //直接設置圖片
             wself.image = image;
             [wself setNeedsLayout];
        } else {
            //image== nil,設置占位圖
            if ((options & SDWebImageDelayPlaceholder)) {
                wself.image = placeholder;
                [wself setNeedsLayout];
            }
    }
    if (completedBlock && finished) {
        completedBlock(image, error, cacheType, url);
    }
});

最后,把返回的id <SDWebImageOperation> operation添加到operationDictionary中,方便后續的cancel。

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:來直接下載圖片。

SDWebImageManager.h首先定義了一些枚舉類型的SDWebImageOptions。關于這些Options的具體含義可以參考葉孤城大神的解析

然后,聲明了三個block:

//操作完成的回調,被上層的擴展調用。
typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL);
//被SDWebImageManager調用。如果使用了SDWebImageProgressiveDownload標記,這個block可能會被重復調用,直到圖片完全下載結束,finished=true,再最后調用一次這個block。
typedef void(^SDWebImageCompletionWithFinishedBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL);
//SDWebImageManager每次把URL轉換為cache key的時候調用,可以刪除一些image URL中的動態部分。
typedef NSString *(^SDWebImageCacheKeyFilterBlock)(NSURL *url);

定義了SDWebImageManagerDelegate協議:

@protocol SDWebImageManagerDelegate <NSObject>

@optional

/**
 * Controls which image should be downloaded when the image is not found in the cache.
 *
 * @param imageManager The current `SDWebImageManager`
 * @param imageURL     The url of the image to be downloaded
 *
 * @return Return NO to prevent the downloading of the image on cache misses. If not implemented, YES is implied.
 * 控制在cache中沒有找到image時 是否應該去下載。
 */
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;

/**
 * Allows to transform the image immediately after it has been downloaded and just before to cache it on disk and memory.
 * NOTE: This method is called from a global queue in order to not to block the main thread.
 *
 * @param imageManager The current `SDWebImageManager`
 * @param image        The image to transform
 * @param imageURL     The url of the image to transform
 *
 * @return The transformed image object.
 * 在下載之后,緩存之前轉換圖片。在全局隊列中操作,不阻塞主線程
 */
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

@end

SDWebImageManager是單例使用的,分別維護了一個SDImageCache實例和一個SDWebImageDownloader實例。 類方法分別是:

//初始化SDWebImageManager單例,在init方法中已經初始化了cache單例和downloader單例。
- (instancetype)initWithCache:(SDImageCache *)cache downloader:(SDWebImageDownloader *)downloader;
//下載圖片
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
//緩存給定URL的圖片
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
//取消當前所有的操作
- (void)cancelAll;
//監測當前是否有進行中的操作
- (BOOL)isRunning;
//監測圖片是否在緩存中, 先在memory cache里面找  再到disk cache里面找
- (BOOL)cachedImageExistsForURL:(NSURL *)url;
//監測圖片是否緩存在disk里
- (BOOL)diskImageExistsForURL:(NSURL *)url;
//監測圖片是否在緩存中,監測結束后調用completionBlock
- (void)cachedImageExistsForURL:(NSURL *)url
                     completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//監測圖片是否緩存在disk里,監測結束后調用completionBlock
- (void)diskImageExistsForURL:(NSURL *)url
                   completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//返回給定URL的cache key
- (NSString *)cacheKeyForURL:(NSURL *)url;

我們主要研究

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock

首先,監測url 的合法性:

if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}
// Prevents app crashing on argument type error like sending NSNull instead of NSURL
if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}

第一個判斷條件是防止很多用戶直接傳遞NSString作為NSURL導致的錯誤,第二個判斷條件防止crash。

if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        dispatch_main_sync_safe(^{
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
            completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
        });
        return operation;
    }

集合failedURLs保存之前失敗的urls,如果url為空或者url之前失敗過且不采用重試策略,直接調用completedBlock返回錯誤。

@synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }

runningOperations是一個可變數組,保存所有的operation,主要用來監測是否有operation在執行,即判斷running 狀態。

SDWebImageManager會首先在memory以及disk的cache中查找是否下載過相同的照片,即調用imageCache

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

方法。
如果在緩存中找到圖片,直接調用completedBlock,第一個參數是緩存的image。

dispatch_main_sync_safe(^{
    __strong __typeof(weakOperation) strongOperation = weakOperation;
    if (strongOperation && !strongOperation.isCancelled) {//為啥這里用strongOperation TODO
        completedBlock(image, nil, cacheType, YES, url);
    }
});

如果沒有在緩存中找到圖片,或者不管是否找到圖片,只要operation有SDWebImageRefreshCached標記,那么若SDWebImageManagerDelegateshouldDownloadImageForURL方法返回true,即允許下載時,都使用 imageDownloader

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

方法進行下載。如果下載有錯誤,直接調用completedBlock返回錯誤,并且視情況將url添加到failedURLs里面;

dispatch_main_sync_safe(^{
    if (strongOperation && !strongOperation.isCancelled) {
        completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
    }
});

if (error.code != NSURLErrorNotConnectedToInternet
 && error.code != NSURLErrorCancelled
 && error.code != NSURLErrorTimedOut
 && error.code != NSURLErrorInternationalRoamingOff
 && error.code != NSURLErrorDataNotAllowed
 && error.code != NSURLErrorCannotFindHost
 && error.code != NSURLErrorCannotConnectToHost) {
      @synchronized (self.failedURLs) {
          [self.failedURLs addObject:url];
      }
}

如果下載成功,若支持失敗重試,將url從failURLs里刪除:

if ((options & SDWebImageRetryFailed)) {
    @synchronized (self.failedURLs) {
         [self.failedURLs removeObject:url];
    }
}

如果delegate實現了,imageManager:transformDownloadedImage:withURL:方法,圖片在緩存之前,需要做轉換(在全局隊列中調用,不阻塞主線程)。轉化成功切下載全部結束,圖片存入緩存,調用completedBlock回調,第一個參數是轉換后的image。

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
    UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

    if (transformedImage && finished) {
        BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
        //將圖片緩存起來
        [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
    }
    dispatch_main_sync_safe(^{
        if (strongOperation && !strongOperation.isCancelled) {
            completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
        }
    });
});

否則,直接存入緩存,調用completedBlock回調,第一個參數是下載的原始image。

if (downloadedImage && finished) {
    [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}

dispatch_main_sync_safe(^{
    if (strongOperation && !strongOperation.isCancelled) {
        completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
    }
});

存入緩存都是調用imageCache

- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk

方法。

如果沒有在緩存找到圖片,且不允許下載,直接調用completedBlock,第一個參數為nil。

dispatch_main_sync_safe(^{
    __strong __typeof(weakOperation) strongOperation = weakOperation;
    if (strongOperation && !weakOperation.isCancelled) {//為啥這里用weakOperation TODO
        completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
    }
});

最后都要將這個operation從runningOperations里刪除。

@synchronized (self.runningOperations) {
    [self.runningOperations removeObject:operation];
 }

這里再說一下上面的operation,是一個SDWebImageCombinedOperation實例:

@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>

@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic) NSOperation *cacheOperation;

@end

是一個遵循SDWebImageOperation協議的NSObject子類。

@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

在里面封裝一個NSOperation,這么做的目的應該是為了使代碼更簡潔。因為下載操作需要查詢緩存的operation和實際下載的operation,這個類的cancel方法可以同時cancel兩個operation,同時還可以維護一個狀態cancelled。
敬請期待后續更新!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,885評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,312評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,993評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,667評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,410評論 6 411
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,778評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,775評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,955評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,521評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,266評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,468評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,998評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,696評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,095評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,385評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,193評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,431評論 2 378

推薦閱讀更多精彩內容