SDWebImage源碼閱讀(一)

用過SDWebImage (下文用 SD 代指)的人都知道,這是一款可以通過分類的形式來為 UIImageView 以及 UIButton 異步加載圖片,并提供緩存的一個開源庫。SD 有很多優點,可以在 Github 上看到,為了讀者方便,這里做搬運工作:

  • [x] 異步加載圖片,并支持緩存
  • [x] 兩級緩存方案,內存緩存 + 磁盤緩存
  • [x] 子線程為圖片解碼,解壓
  • [x] 同一個 URL 對應的圖片可以保證不會進行多次下載
  • [x] 保證絕對不會卡主線程
  • [x] 保證加載失敗的圖片地址不會加載一遍又一遍
  • [x] 支持 GCD 和 ARC
  • [x] 支持多種圖片格式 GIF,JPEG,PNG,以及 WebP

先來一個官方的時序圖(如果不清楚時序圖是什么意思的可以去看我的另一篇 UML使用-時序圖),大致介紹一下整體的一個工作流程

SDSequenceDiagrm

簡要整個圖片的介紹流程:

  • 1.UIImageView 調用分類 UIImageView+WebCache 的 sd_setImageWithURL 方法(當然不止這一個方法,在這個分類的.h文件下可以看到)作為入口來加載圖片。

  • 2.然后 UIImageView 的分類其實是調用的其父類UIView的分類 UIView+WebCache 的 sd_internalSetImageWithURL 方法,這里為什么要把這個調用放到父類UIView里面呢,
    因為 SDWebImage 框架也支持 UIButton 的下載圖片等方法,所以需要在它們的父類:UIView里面統一給一個下載方法。

  • 3.UIView+WebCache 調用 SDWebImageManager 的 loadImageWithURL 來真正加載圖片。加載圖片分為兩步,首先先調用 SDImageCache 來查詢緩存,如果查詢緩存命中的話,返回 image;如果查詢緩存沒有的話,此時再通過 SDImageDownloader 調用downloadImage 來從服務端下載圖片,下載成功之后先更新到緩存,再返回 image。

因此,文章將分成三個部分來介紹整個 SD 框架 ,大致按照時序圖上面的步驟分成以下三個部分,本文是第一篇

  • 1.框架的基本上手與圖片的加載,即主要對 UIImageView(WebCache) ,UIView(WebCache)

  • 2.對圖片加載過程中涉及到 SDWebImageManager 來介紹,以及進行介紹 SDImageCache及其相關的東西

  • 3.對緩存查詢失敗之后去網上下載,以及下載后的操作來介紹,即 SDWebImageDownloader及 其一些相關的東西

到目前為止,咱們對 SD 也有了一個大致一點的認識,接下來咱們開始對第一部分做介紹吧:


SD暴露給我們用的最外層的就 UIImageView(WebCache),我們可以通過調用不同的接口,將圖片地址,占位圖,加載進度回調,完成回調直接給這個類,下面是這個類的公共接口:

/* ===========================  UIImageView + WebCache.h ====================== */
- (void)sd_setImageWithURL:(nullable NSURL *)url;
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder;
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options;
- (void)sd_setImageWithURL:(nullable NSURL *)url
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                 completed:(nullable SDExternalCompletionBlock)completedBlock;
- (void)sd_setImageWithURL:(nullable NSURL *)url
          placeholderImage:(nullable UIImage *)placeholder
                   options:(SDWebImageOptions)options
                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                 completed:(nullable SDExternalCompletionBlock)completedBlock;                 
-(void)sd_setImageWithPreviousCachedImageWithURL:(nullable NSURL *)url
                                 placeholderImage:(nullable UIImage *)placeholder
                                          options:(SDWebImageOptions)options
                                         progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                        completed:(nullable SDExternalCompletionBlock)completedBlock;
         
//加載多張圖片用來輪播                                        
- (void)sd_setAnimationImagesWithURLs:(nonnull NSArray<NSURL *> *)arrayOfURLs;
- (void)sd_cancelCurrentAnimationImagesLoad;                                        
                                                                        

可以看到這個類里面各個方法最后都會跳到父類的這個方法:

/* ============================  UIView + WebCache.h ========================== */
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock;

operationKey的作用:理解這個變量,估計要往后多看一部分代碼才能明白,這里先做一點講解。UIImageView(WebCache) 這個分類會為UIImageView添加一個字典SDOperationsDictionary作為其屬性(不是說分類里面不能加嗎,這是怎么搞的?別著急,后面會講),字典的key 就是這個operationKey ,而字典的value就是一個operation, 這個operation代表了一個加載圖片這樣一個操作(將這個operation 和 多線程里面的 NSOperation 聯系起來會更加好理解)。所以這個 operationKey 其實是用來作為字典的鍵的,方便以后通過這樣一個key來找到一個ImageView的實例的擁有的那些加載圖片的operation。(其實后面找這個operation的目的也就是為了在特地的時候好用來cancle掉)

SDWebImageOptions:這是一個放置在 SDWebImageManager.h 中的枚舉,姑且可以理解為指定圖片加載的一些策略吧,下面一個一個介紹

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    //默認情況下,如果一張圖片下載失敗了,這個圖片對應的URL是會被添加到黑名單中的,這樣可以保證一個失敗的URL不會反復被加載;而這個選項代表的是這個URL即使下載失敗了也不會放到黑名單中
    SDWebImageRetryFailed = 1 << 0,

    //默認情況下,圖片的下載會在UI交互的過程中開始,這個枚舉值所代表的就是推遲下載比如在UIScrollView滾動減速之后
    SDWebImageLowPriority = 1 << 1,

    //這個枚舉值限定緩存只在內存中,不緩存到磁盤上去
    SDWebImageCacheMemoryOnly = 1 << 2,
    
    //默認情況下,一張圖片只會在數據完全下載回來之后才會顯示,這個枚舉值,可以讓圖片一點一點的隨著下載回來的數據顯示
    SDWebImageProgressiveDownload = 1 << 3,

    // 磁盤緩存將會由NSURLCache 而不是SDWebImage 來處理,因此可能帶來輕微的性能下降。
    // 使用于使用固定的圖片url 但是圖片內容可能變化的場景
    SDWebImageRefreshCached = 1 << 4,

    // 如果應用進入后臺狀態,繼續圖片下載,應用因此將會額外活躍一段時間,
    // 如果這段時間用完但是下載任務尚未完成,那么下載就會被取消
    SDWebImageContinueInBackground = 1 << 5,    

    // 可以控制存在NSHTTPCookieStore的cookies.
    SDWebImageHandleCookies = 1 << 6,

    // 允許不受信任的SSL證書。主要用于測試目的。正式環境中慎用
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,

    //默認情況下,image在加載的時候是按照他們在隊列中的順序裝載的(就是先進先出).這個flag會把當前圖片加載移動到隊列的前端,立刻加載
    SDWebImageHighPriority = 1 << 8,

    //默認情況下,占位圖是在圖片加載的時候加載的,這個標記會延遲加載,直到圖片加載完成
    SDWebImageDelayPlaceholder = 1 << 9,

    //是否transform animatedImage
    SDWebImageTransformAnimatedImage = 1 << 10,
    
    //默認情況下,下載的image是直接加到ImageView上的,但是在某些情況下,我們希望在加到ImageView上之前做一些操作,通過這是這個枚舉值,你就可以在下載成功回調里面先做你想做的,在添加到ImageView上了
    SDWebImageAvoidAutoSetImage = 1 << 11,
    
    //圖片默認會被解碼成它們的原始尺寸。這個flag 會將圖片按照設備的內存來進行縮放。如果SDWebImageProgressiveDownload 被設置了,那么這個選項就不起作用 
    SDWebImageScaleDownLargeImages = 1 << 12
};

下面咱們詳細看一下這個方法的實現:我們盡量先把整個邏輯理清楚,例如有些方法我們只介紹方法干嘛,暫時不跳進去詳細介紹。適當時候在詳細分析

/* ==========================  UIView + WebCache.h =========================== */

- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
                  placeholderImage:(nullable UIImage *)placeholder
                           options:(SDWebImageOptions)options
                      operationKey:(nullable NSString *)operationKey
                     setImageBlock:(nullable SDSetImageBlock)setImageBlock
                          progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                         completed:(nullable SDExternalCompletionBlock)completedBlock {    
    //先找到指定的key,如果key沒有的話,就用類名作為key                         
    NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);    
    //根據key找到所有的 Operation 然后 cancle掉
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    //用runtime把當前圖片的url綁定到當前的imageView上,為當前imageView添加一個url屬性
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    //如果options的選項不是延遲加載占位圖的話,就加載占位圖
    if (!(options & SDWebImageDelayPlaceholder)) {
    //`dispatch_main_async_safe`可以去看到其實是一個簡單的封裝,就是操作移到主線程來更新UI
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
        });
    }
    if (url) {
    //如果加載過程中需要顯示菊花的話就顯示菊花,默認不顯示
        if ([self sd_showActivityIndicatorView]) {
            [self sd_addActivityIndicator];
        }                
        __weak __typeof(self)wself = self;
     //這里很明顯是調用 SDWebImageManager 去加載圖片(注意咱們這里的加載是包括了去緩存找以及如果緩存沒找到去下載的過程),函數返回一個實現了 SDWebImageOperation 的協議的這樣一個對象,目的在于以后便于統一去cancle緩存查找和圖片下載
        id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
        //圖片加載成功回調
            __strong __typeof (wself) sself = wself;
            [sself sd_removeActivityIndicator];
            if (!sself) {
                return;
            }
            dispatch_main_async_safe(^{
                if (!sself) {
                    return;
                }
                
                if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
                //image存在,且下載完成后不自動將image設置到ImageView上,讓開發者在成功回調里面自己去設置
                    completedBlock(image, error, cacheType, url);
                    return;
                } else if (image) {
                //image存在,且下載完成后是自動將image設置到ImageView上
                    [sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                    [sself sd_setNeedsLayout];
                } else {
                //image不存在,如果option恰好也是延遲設置,只能把占位圖放上去了
                    if ((options & SDWebImageDelayPlaceholder)) {
                        [sself sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock];
                        [sself sd_setNeedsLayout];
                    }
                }
                //有回調辦法 && 加載完成
                if (completedBlock && finished) {
                    completedBlock(image, error, cacheType, url);
                }
            });
        }];
        //在表示正在進行的圖片加載字典(operationDictionary)里添加operation
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
    } else {
       //URL不存在的情況
        dispatch_main_async_safe(^{
            [self sd_removeActivityIndicator];
            if (completedBlock) {
                NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
       //報錯,并把錯誤也回調回去
                completedBlock(nil, error, SDImageCacheTypeNone, url);
            }
        });
    }
}

幾個重要的點:

1.屬性綁定:屬性綁定其實就是通過runtime動態的為一個對象添加屬性,通過 Associated Objects 就可以為分類添加屬性了,解決分類不能添加屬性的問題,在 UIView+WebCache 中就通過這個方式為 UIImageView 綁定了一個URL。有關于屬性綁定的更多信息可以參考這篇博客這里就不做過多介紹了。

UIView+WebCache的使用:

//綁定的key
static char imageURLKey;

//將url綁定到self,也就是ImageView上
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

//通過ImageView 獲取他綁定的URL
- (nullable NSURL *)sd_imageURL {
    return objc_getAssociatedObject(self, &imageURLKey);
}

2.dispatch_main_async_safe

關于這個宏的定義可以在 SDWebImageCompat.h中看到:

#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
    if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
        block();\
    } else {\
        dispatch_async(dispatch_get_main_queue(), block);\
    }
#endif

這段代碼的目的在前面已經提到,這段代碼會把當前的操作移到主隊列中執行;具體的操作則是判斷當前執行任務的隊列是不是主隊列,如果是的話,就直接把操作添加到當前隊列;如果不是的,就把要執行的任務加到主隊列中執行。

總結:在這一篇中,咱們主要介紹了SD中上層UIKit的一些操作,大體介紹了整個SD的工作流程,讓讀者對SD有個比較全面的認識,并且在這兒也介紹了一些重要的知識點;在下一篇中 ,將主要詳細介紹SDWebImageManager,以及SDWebImageCache這兩個非常主要的類所做的操作。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容