SDWebImage 源碼學習筆記 ? SDWebImageManager

SDWebImage-源碼學習筆記.png

前言

這是本系列的第 3 篇,在前一篇中,我們了解了 SDWebImage 執行的基本流程,本篇就來介紹第一個核心類 SDWebImageMananger

正文

SDWebImageMananger.h 文件基本可以分為 3 各部分:

①定義了一個枚舉 SDWebImageOptions,列舉了可能會用到的一些場景。

typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) {
    // 重試已經失敗的 url
    SDWebImageRetryFailed = 1 << 0,
    // 低優先級,比如,在有 UI 交互的情況下,會延遲下載操作
    SDWebImageLowPriority = 1 << 1,
    // 下載完成后,僅做內存緩存,不做磁盤緩存
    SDWebImageCacheMemoryOnly = 1 << 2,
    // 下載過程中逐步加載圖片,而不是完全下載完之后才展示
    SDWebImageProgressiveDownload = 1 << 3,
    // 刷新緩存
    SDWebImageRefreshCached = 1 << 4,
    // 當 App 進入后臺時,繼續下載任務,如果后臺任務超時,操作將被自動取消
    SDWebImageContinueInBackground = 1 << 5,
    // 允許處理 Cookie
    SDWebImageHandleCookies = 1 << 6,
    // 允許不受信任的 SSL 證書,生產環境慎用
    SDWebImageAllowInvalidSSLCertificates = 1 << 7,
    // 高優先級,即會把相應的圖片放到最前邊加載,而不是按照加入隊列時的順序執行
    SDWebImageHighPriority = 1 << 8,
    // 延遲 placeholder 的加載,即在下載完成時才加載
    SDWebImageDelayPlaceholder = 1 << 9,
    // 對動圖也執行 transform 操作
    SDWebImageTransformAnimatedImage = 1 << 10,
    // 圖片下載完成后,不直接自動給 imageView 賦值,給用戶調整圖片的機會
    SDWebImageAvoidAutoSetImage = 1 << 11,
    // 依據設備內存縮放圖片,如果設置了 `SDWebImageProgressiveDownload` ,此設置無效
    SDWebImageScaleDownLargeImages = 1 << 12,
    // 在有內存緩存的情況下,依然需要查詢磁盤緩存,建議與 SDWebImageQueryDiskSync 配合使用
    SDWebImageQueryDataWhenInMemory = 1 << 13,
    // 同步查詢磁盤緩存
    SDWebImageQueryDiskSync = 1 << 14,
    // 僅加載緩存圖片
    SDWebImageFromCacheOnly = 1 << 15,
    // 對內存和磁盤中的 image 也執行 transition 的操作
    SDWebImageForceTransition = 1 << 16
};

②定義了一個協議 SDWebImageManagerDelegate,這里提供了以下 3 個協議方法:

// 緩存中沒有指定圖片時,是否需要下載
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nullable NSURL *)imageURL;

// 是否需要將制定 URL 標記為失敗的 URL
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldBlockFailedURL:(nonnull NSURL *)imageURL withError:(nonnull NSError *)error;

// 允許在剛剛下載到 image 并且未做緩存之前,對圖片執行 transform,返回處理后的 image
- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;

③SDWebImageManager 的頭文件,有幾個重要屬性,他們的作用見下邊的注釋。

// 代理對象
@property (weak, nonatomic, nullable) id <SDWebImageManagerDelegate> delegate;
// 處理緩存的對象
@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;
// 處理下載工作的對象
@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;
// 一個用戶定義的 block,用于生成 cacheKey
@property (nonatomic, copy, nullable) SDWebImageCacheKeyFilterBlock cacheKeyFilter;
// 一個用戶定義的 block,用于序列化下載到的數據
@property (nonatomic, copy, nullable) SDWebImageCacheSerializerBlock cacheSerializer;

下面是 2 個常用的創建方法:+ (nonnull instancetype)sharedManager;- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader;,其實最終都是調用了后者 。

// 單例
+ (nonnull instancetype)sharedManager {
    static dispatch_once_t once;
    static id instance;
    dispatch_once(&once, ^{
        instance = [self new];
    });
    return instance;
}

// 初始化,創建處理緩存和下載任務的對象 cache 和 downloader
- (nonnull instancetype)init {
    SDImageCache *cache = [SDImageCache sharedImageCache];
    SDWebImageDownloader *downloader = [SDWebImageDownloader sharedDownloader];
    return [self initWithCache:cache downloader:downloader];
}

// 核心的初始化方法,為各屬性賦初值
- (nonnull instancetype)initWithCache:(nonnull SDImageCache *)cache downloader:(nonnull SDWebImageDownloader *)downloader {
    if ((self = [super init])) {
        // 處理緩存和下載任務的對象
        _imageCache = cache;
        _imageDownloader = downloader;
        // 用于存儲請求失敗的 URL 的集合及操作時用的鎖 (信號量)
        _failedURLs = [NSMutableSet new];
        _failedURLsLock = dispatch_semaphore_create(1);
        // 存儲運行中 operation 的集合,通過判斷他的 count 是否為 0,判斷操作是否在進行中:BOOL isRunning = (self.runningOperations.count > 0);
        _runningOperations = [NSMutableSet new];
        // 操作時用的鎖 (信號量)
        _runningOperationsLock = dispatch_semaphore_create(1);
    }
    return self;
}

另外幾個方法,就不單獨介紹了,用到的時候再繼續討論。此處,我們只看一個核心方法 - (nullable id <SDWebImageOperation>)loadImageWithURL:url options:options progress:progressBlock completed:completedBlock;,下面我們來一步步討論這個方法的具體實現。

1.校驗參數

依次做如下處理:如果傳入的 completedBlock 為空,就直接報錯;如果傳入的參數是 NSString * 類型的,需要將其轉換成 NSURL;最后,如果 url 還不是 NSURL 類型,那就只能將其置為 nil,以免造成后邊 Crash。

    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

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

    if (![url isKindOfClass:NSURL.class]) {
        url = nil;
    }
2.生成總的 operation

他是 SDWebImageCombinedOperation 實例對象,也是當前方法要返回的結果,并將當前類賦值給 operation 的一個 weak 屬性(避免循環引用)。

SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
operation.manager = self; // 肯定是 weak 屬性

SDWebImageCombinedOperation 的聲明與實現文件均在當前類 SDWebImageManager 的實現文件里邊,簡單看一下他的 .h/.m 文件吧。

// SDWebImageCombinedOperation.h
@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
// 標識是否已取消
@property (assign, nonatomic, getter = isCanceled) BOOL cancelled;
// downloadToken 這是一個繼承自 NSObject 的類,他有一個繼承自 NSOperation 的屬性,也就是真正執行下載操作時的 operation,cancel 時會用到
@property (strong, nonatomic, nullable) SDWebImageDownloadToken *downloadToken;
// 查詢緩存時的 operation,用于標識當前 operation 是否已經被取消。其實查詢緩存時,首先查看 operation.isCanceled,如果沒被取消了,就會再去查詢了。cancel 時會將其 isCanceled 屬性置為 YES。
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
// manager
@property (weak, nonatomic, nullable) SDWebImageManager *manager;

@end

// SDWebImageCombinedOperation.m
#pragma mark - 代理方法實現

@implementation SDWebImageCombinedOperation

- (void)cancel {
    @synchronized(self) {
        self.cancelled = YES;
        // 取消查詢緩存的 Operation,此時 isCanceled 會被置為 YES。
        if (self.cacheOperation) {
            [self.cacheOperation cancel];
            self.cacheOperation = nil;
        }
        // 取消下載操作
        if (self.downloadToken) {
            [self.manager.imageDownloader cancel:self.downloadToken];
        }
        // 將當前 operation 從 manager 中運行著的 operation 數組中移除。
        [self.manager safelyRemoveOperationFromRunning:self];
    }
}

@end

//  SDWebImageOperation 協議的定義
@protocol SDWebImageOperation <NSObject>
- (void)cancel;
@end

可以看到,SDWebImageCombinedOperation 這個類的主要作用就是 cancel 操作,包括 cancel 查詢緩存 和 cancel 下載數據。

3.再次檢測一下 url

如果是曾經失敗的 url,而且不允許重試,或者 url 為空時,執行 completionBlock,并返回當前 operation。

// self.failedURLs 是一個保存曾經失敗過的 URL 的數組,用于檢測當前 URL 是不是曾經請求失敗過的URL.另外,搜索一個個元素的時候,NSSet 比 NSArray 查詢更快。
    BOOL isFailedUrl = NO;
    if (url) {
        LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        UNLOCK(self.failedURLsLock);
    }

    // 若出現以下兩種情況就不再往下走了,直接執行 CompletionBlock:① URL 是空的;② 此 URL 是曾經請求失敗的 URL,并且規定不允許重新請求曾經失敗的 URL。
    if (url.absoluteString.length == 0
        || (!(options & SDWebImageRetryFailed) && isFailedUrl))
    {
        [self callCompletionBlockForOperation:operation
                                   completion:completedBlock
                                        error:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil]
                                          url:url];
        return operation;
    }
4.保存 operationself.runningOperations

后者是一個數組,這里使用了信號量來確保線程安裝。

    LOCK(self.runningOperationsLock);
    [self.runningOperations addObject:operation];
    UNLOCK(self.runningOperationsLock);
5.查詢緩存。
    NSString *key = [self cacheKeyForURL:url];
    
    SDImageCacheOptions cacheOptions = 0;
    if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
    if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
    if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
    
    __weak SDWebImageCombinedOperation *weakOperation = operation;
    
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key
                                                                  options:cacheOptions
                                                                     done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType)
    {
        // 查詢完成后的操作在這里,可能查到了,也可能沒查到...
    }

這里先準備了 2 個參數,查詢的依據 key 和一些條件 cacheOptions。key 的獲取是通過一個私有方法 (如下),如果自定義了 key 的生成規則 self.cacheKeyFilter,就用自定義的,如果沒有,就直接取 url.absoluteString。cacheOptions 是一個用 NS_OPTIONS 定義的枚舉類型 (前邊已介紹過),可組合多種情況,在這里綜合了 2 個查詢的要求和 1 個縮放圖片的要求。

- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
    if (!url) {
        return @"";
    }
    
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    } else {
        return url.absoluteString;
    }
}
6.查詢的具體過程

詳情將會在 SDImageCache 中介紹,下面討論一下查詢緩存結束后的操作。

7.移除當前 operation

從 self.runningOperations 這個數組中移除當前 operation。

if (!strongOperation || strongOperation.isCancelled) {
    [self safelyRemoveOperationFromRunning:strongOperation];
    return;
}
8.判斷是否需要下載

當同時滿足 3 個要求時,就需要下載新數據了:
①沒要求只能從緩存獲取數據,即當緩存找不到時,可以去下載;
②找不到緩存 或 要求必須更新緩存;
③當 self.delegate 沒有遵守協議, 或者 協議方法返回 YES。

BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly)) 
        && (!cachedImage || options & SDWebImageRefreshCached) 
        && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
9.若需要下載
  • 首先依然要做一個判斷,即 如果有緩存數據并且要求刷新緩存數據時,需要先調用一次 CompletionBlock,將緩存數據返回去,然后再開始下載新數據,代碼如下:
if (cachedImage && options & SDWebImageRefreshCached) {
    [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}

然后,準備下載數據時所需的一些基本選項,可以參考篇頭介紹的枚舉 SDWebImageOptions

  • 開始下載,調用了 SDWebImageDownloader 的下載方法,留待 SDWebImageDownloader 介紹,這里只討論下載完成之后的操作。
__weak typeof(strongOperation) weakSubOperation = strongOperation;
            strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url
                                                                               options:downloaderOptions
                                                                              progress:progressBlock
                                                                             completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished)
{
        // 下載完成后的操作...
}

下載完成后,可以分這么幾種情況:
a.當前 operation 已經被取消,這種情況下不作任何操作,包括回調。

b.下載出錯,先將失敗的 error 信息返回,然后決定是否需要將當前 URL 存入失敗的 URL 數組。

c.下載成功,此時要做的工作還有許多:

①如果設置了失敗重發,則將當前 URL 從失敗的 URL 數組中移除。

if ((options & SDWebImageRetryFailed)) {
    LOCK(self.failedURLsLock);
    [self.failedURLs removeObject:url];
    UNLOCK(self.failedURLsLock);
}

②對于自定義的 manager,需要執行另外一套縮放標準。

if (self != [SDWebImageManager sharedManager]
                        && self.cacheKeyFilter
                        && downloadedImage)
{
    downloadedImage = [self scaledImageForKey:key image:downloadedImage];
}

③若需要更新緩存,但是未下載到圖片,且緩存中本來有值的情況下,什么也不做,因為下載之前早已經緩存數據返回了。

if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// 需要更新緩存,但是未下載到圖片,且緩存中本來有值的情況下,什么也不做,因為下載之前早已經緩存數據返回了
}

④如果下載到了圖片,并且要求 transform 圖片的情況下,異步執行 transform 和緩存圖片的工作,然后回到主線程執行 completionBlock。

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];
            NSData *cacheData;
            // pass nil if the image was transformed, so we can recalculate the data from the image
            if (self.cacheSerializer) {
                cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
            } else {
                cacheData = (imageWasTransformed ? nil : downloadedData);
            }
            
            // *** 存盤:注意是存的 imageData
            [self.imageCache storeImage:transformedImage
                              imageData:cacheData
                                 forKey:key
                                 toDisk:cacheOnDisk
                             completion:nil];
        }
        
        [self callCompletionBlockForOperation:strongSubOperation
                                   completion:completedBlock
                                        image:transformedImage
                                         data:downloadedData
                                        error:nil
                                    cacheType:SDImageCacheTypeNone
                                     finished:finished
                                          url:url];
    });

⑤如果下載到了圖片,并且下載完成的話,則存盤并執行 completionBlock。存盤調用了 SDImageCache 的方法,隨后介紹。

最后將當前 operation 移除。

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];
            NSData *cacheData;
            // pass nil if the image was transformed, so we can recalculate the data from the image
            if (self.cacheSerializer) {
                cacheData = self.cacheSerializer(transformedImage, (imageWasTransformed ? nil : downloadedData), url);
            } else {
                cacheData = (imageWasTransformed ? nil : downloadedData);
            }
            
            // *** 存盤:注意是存的 imageData
            [self.imageCache storeImage:transformedImage
                              imageData:cacheData
                                 forKey:key
                                 toDisk:cacheOnDisk
                             completion:nil];
        }
        
        [self callCompletionBlockForOperation:strongSubOperation
                                   completion:completedBlock
                                        image:transformedImage
                                         data:downloadedData
                                        error:nil
                                    cacheType:SDImageCacheTypeNone
                                     finished:finished
                                          url:url];
    });

// ...

if (finished) {
    [self safelyRemoveOperationFromRunning:strongSubOperation];
}
10.若不需要下載,并且有緩存

此時,執行 completionBlock 將緩存數據返回,然后移除當前 operation。

[self callCompletionBlockForOperation:strongOperation
                           completion:completedBlock
                                image:cachedImage
                                 data:cachedData
                                error:nil
                            cacheType:cacheType
                             finished:YES
                                  url:url];
            
[self safelyRemoveOperationFromRunning:strongOperation];
11.其它,即沒有緩存,且不需要下載

和上邊的操作類似,只不過傳回的 image 和 data 均為 nil。

    [self callCompletionBlockForOperation:strongOperation
                               completion:completedBlock
                                    image:nil
                                     data:nil
                                    error:nil
                                cacheType:SDImageCacheTypeNone
                                 finished:YES
                                      url:url];
    
    [self safelyRemoveOperationFromRunning:strongOperation];

最后將 operation 返回。

小結

以上就是 SDWebImageManager 這個類的主要功能,其中關于緩存和下載的內容,詳見后邊幾篇的討論。

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

推薦閱讀更多精彩內容