SDWebImage5.11源碼分析(一)

SDWebImage5.0進(jìn)行了一次架構(gòu)上的改進(jìn),因?yàn)檗D(zhuǎn)Swift之后,一直沒用到SDWebImage,所以也沒怎么關(guān)注。最近有空剛好學(xué)習(xí)一下SDWebImage5.11的源碼。

一、流程架構(gòu)圖
流程架構(gòu)圖

  • SDWebImage對(duì)UIButton,UIImageView,NSButton,UIView進(jìn)行了拓展,并對(duì)外提供了接口。無論對(duì)UIButton,UIImageView還是NSButton調(diào)用sd_setImageWithURL的時(shí)候,最終都會(huì)調(diào)用到UIView拓展類的sd_internalSetImageWithURL方法。

  • 前面的拓展都只是對(duì)外的接口,主要邏輯處理放在SDWebImageManager里面。他相當(dāng)于一個(gè)調(diào)度中心,如果需要緩存(讀跟?。?,他就會(huì)調(diào)用SDImageCache,如果需要下載,就會(huì)調(diào)用SDWebImageDownloader。類似我們MVP模式下的Presenter,收到View拓展接口相關(guān)的參數(shù)后,根據(jù)不同業(yè)務(wù)傳遞給cache跟downloader處理,最后將處理完的數(shù)據(jù)通過block回調(diào)給接口。

最后還有一些工具,沒有在流程圖中畫出來,這里說明一下:

  • Decoder:做一些編解碼操作,針對(duì)不同類型的圖片進(jìn)行不同的操作。

  • Transform:從緩存或下載轉(zhuǎn)換圖像加載的轉(zhuǎn)換器協(xié)議。

  • AnimatedImage:可以替代UIImageView,支持gif

  • Utils:存放一些枚舉,Define,還有菊花器

  • Categories:對(duì)需要的類進(jìn)行拓展,大部分是UIImage

  • Private:一些私人方法

二、代碼部分

1. UIView+WebCache

直接找到sd_internalSetImageWithURL方法,這是入口進(jìn)來后第一個(gè)處理的方法,處理內(nèi)容如下:

a. 拿到舊的operation(任務(wù)),取消其操作,并從SDOperationsDictionary移除。然后創(chuàng)建新的加載任務(wù),并加入到SDOperationsDictionary中。

b. 處理進(jìn)度條,重置進(jìn)度條

c. 處理菊花器

d. 創(chuàng)建SDWebImageManager,并調(diào)用loadImageWithURL加載圖片

我們先看sd_internalSetImageWithURL方法里面的代碼

a、取消之前的任務(wù)
/*
     *通過SDWebImageContextSetImageOperationKey拿到SDOperationsDictionary的key:validOperationKey(說白了這里就是二維字典)
     *在通過validOperationKey拿到對(duì)應(yīng)的Operation(任務(wù)),對(duì)任務(wù)進(jìn)行取消之類的相關(guān)操作
     */
    NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey];
    if (!validOperationKey) {
        // pass through the operation key to downstream, which can used for tracing operation or image view class
        validOperationKey = NSStringFromClass([self class]);
        // 對(duì)context進(jìn)行深拷貝,轉(zhuǎn)為可變字典
        SDWebImageMutableContext *mutableContext = [context mutableCopy];
        // 將當(dāng)前類對(duì)象名稱裝載進(jìn)mutableContext
        mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey;
        // 將mutableContext轉(zhuǎn)回不可變字典context
        context = [mutableContext copy];
    }
    // 將validOperationKey存儲(chǔ)起來
    self.sd_latestOperationKey = validOperationKey;
    // 如果這個(gè)key存在任務(wù),則取消任務(wù),且從SDOperationsDictionary移除
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    // 將url存儲(chǔ)起來
    self.sd_imageURL = url;

這段代碼主要是拿到validOperationKey,并傳給sd_cancelImage方法,sd_cancelImage的邏輯也很簡單,通過validOperationKey,在SDOperationsDictionary里面拿到對(duì)應(yīng)的任務(wù),并取消。下面是sd_cancelImage的代碼:

- (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key {
    if (key) {
        // Cancel in progress downloader from queue
        SDOperationsDictionary *operationDictionary = [self sd_operationDictionary];
        id<SDWebImageOperation> operation;
        // 拿到對(duì)應(yīng)的任務(wù)operation
        @synchronized (self) {
            operation = [operationDictionary objectForKey:key];
        }
        if (operation) {
            if ([operation conformsToProtocol:@protocol(SDWebImageOperation)]) {
                // 如果存在并遵循SDWebImageOperation代理,則取消任務(wù)
                [operation cancel];
            }
            // 最后將任務(wù)移除
            @synchronized (self) {
                [operationDictionary removeObjectForKey:key];
            }
        }
    }
}
b、占位圖顯示
// 是否需要延遲加載占位圖
    if (!(options & SDWebImageDelayPlaceholder)) {
        // 主線程顯示占位圖
        dispatch_main_async_safe(^{
            [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url];
        });
    }
c、進(jìn)度條,菊花器處理邏輯
if (url) {
        // reset the progress
        // 重置進(jìn)度條
        NSProgress *imageProgress = objc_getAssociatedObject(self, @selector(sd_imageProgress));
        if (imageProgress) {
            imageProgress.totalUnitCount = 0;
            imageProgress.completedUnitCount = 0;
        }
        
#if SD_UIKIT || SD_MAC
        // check and start image indicator
        // 有菊花器就轉(zhuǎn)菊花
        [self sd_startImageIndicator];
        id<SDWebImageIndicator> imageIndicator = self.sd_imageIndicator;
#endif
        // 拿到當(dāng)前manager,沒有就創(chuàng)建,有的話就將context的移除,防止循環(huán)引用
        SDWebImageManager *manager = context[SDWebImageContextCustomManager];
        if (!manager) {
            manager = [SDWebImageManager sharedManager];
        } else {
            // remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager)
            SDWebImageMutableContext *mutableContext = [context mutableCopy];
            mutableContext[SDWebImageContextCustomManager] = nil;
            context = [mutableContext copy];
        }

        // 對(duì)進(jìn)度條進(jìn)行處理,如果菊花器是進(jìn)度條類型的,那就讓進(jìn)度條跑起來,回調(diào)進(jìn)度block
        SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
            if (imageProgress) {
                imageProgress.totalUnitCount = expectedSize;
                imageProgress.completedUnitCount = receivedSize;
            }
#if SD_UIKIT || SD_MAC
            if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) {
                double progress = 0;
                if (expectedSize != 0) {
                    progress = (double)receivedSize / expectedSize;
                }
                progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0
                dispatch_async(dispatch_get_main_queue(), ^{
                    [imageIndicator updateIndicatorProgress:progress];
                });
            }
#endif
            if (progressBlock) {
                progressBlock(receivedSize, expectedSize, targetURL);
            }
        };

注意這里并沒有直接調(diào)用combinedProgressBlock處理進(jìn)度條,而是在下面加載圖片的時(shí)候?qū)?code>combinedProgressBlock扔過去處理。

d、通過SDWebImageManager調(diào)用加載圖片的方法

這里調(diào)用了SDWebImageManager的圖片加載方法,將一些必要參數(shù)傳遞過去,接下來就是SDWebImageManager的事情了。

[manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL)
2. SDWebImageManager

直接找到loadImageWithURL方法,這個(gè)方法主要是對(duì)url的一些判斷,contextoptions的預(yù)處理,內(nèi)容如下:

a. 先判斷url的可行性

b. 對(duì)context,options進(jìn)行預(yù)處理,并放到result里面

c. 調(diào)用callCacheProcessForOperation 判斷是否有緩存,如果有則進(jìn)入ImageCache 拿到緩存數(shù)據(jù),如果沒有則進(jìn)入callDownloadProcessForOperation 方法進(jìn)一步判斷如何下載

先看看這些步驟的源碼,看完再看callCacheProcessForOperation做了些什么

a、判斷url的可行性
// Invoking this method without a completedBlock is pointless
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

    // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't
    // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
    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;
    }

    // 可以把operation當(dāng)做是一個(gè)任務(wù),一個(gè)執(zhí)行著讀取圖片(緩存跟加載器的組合)操作的任務(wù)
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    operation.manager = self;

    BOOL isFailedUrl = NO;
    // 檢測當(dāng)前url是否在failedURLs列表中
    if (url) {
        //os_unfair_lock的宏定義
        // 加鎖,防止多個(gè)線程對(duì)failedURLs操作,引起的數(shù)據(jù)問題
        SD_LOCK(_failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        SD_UNLOCK(_failedURLsLock);
    }

    // 如果url為nil,且未設(shè)置SDWebImageRetryFailed,url在failedURLs列表中,執(zhí)行失敗回調(diào)
    // SDWebImageRetryFailed為失敗鏈接重試,默認(rèn)是不會(huì)重試
    if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
        NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil";
        NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL;
        [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] url:url];
        return operation;
    }

這里注釋應(yīng)該很清楚了,就是判斷url的可行性,跟url是否在失敗列表里面,如果在的,且options沒有SDWebImageRetryFailed的話,就直接失敗回調(diào)。值得注意的是SD的鎖在iOS10以上用的是os_unfair_lock,iOS10以下用的是OSSpinLockLock(這個(gè)鎖存在任務(wù)優(yōu)先級(jí)問題,已經(jīng)被淘汰了)

b、對(duì)context,options進(jìn)行預(yù)處理,并放到result里面
// 將當(dāng)前operation加入到runningOperations(正在運(yùn)行的operation)
    // 加鎖,防止多個(gè)線程對(duì)runningOperations進(jìn)行操作
    SD_LOCK(_runningOperationsLock);
    [self.runningOperations addObject:operation];
    SD_UNLOCK(_runningOperationsLock);
    
    // Preprocess the options and context arg to decide the final the result for manager
    // 對(duì)context進(jìn)行預(yù)處理,然后將處理的context跟options包裝到result里面。
    /* 里面對(duì)context的處理包括,SDWebImageContextImageTransformer、SDWebImageContextCacheKeyFilter、SDWebImageContextCacheSerializer。分別查看外面是否自定義這3個(gè)key的context,如果有就使用,沒有就使用SD默認(rèn)的。除了SDWebImageContextCacheKeyFilter(緩存url的key)默認(rèn)是本身的url,其他2個(gè)都是nil
     */
    SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context];

這里先將任務(wù)加入到正在執(zhí)行的列表里面,然后再對(duì)context進(jìn)行預(yù)處理,源代碼是沒有對(duì)options進(jìn)行說明處理的,然后將contextoptions放入result里面。context的處理源代碼就不貼出來了,大概就是對(duì)SDWebImageContextImageTransformer、SDWebImageContextCacheKeyFilterSDWebImageContextCacheSerializer這3個(gè)進(jìn)行一個(gè)判斷,看是否有自定義的傳過來,沒有就用默認(rèn)的。

c、callCacheProcessForOperation的調(diào)用

這里主要是判斷要到哪里去取數(shù)據(jù),ImageCache,還是去下載,接下來就進(jìn)入這個(gè)方法看一下。

這里主要是判斷任務(wù)是否該走緩存查詢,或者直接下載。如果是緩存查詢,就進(jìn)入SDImageCache里面進(jìn)行緩存查詢,且在此處理緩存結(jié)果的回調(diào)。否則就調(diào)用callDownloadProcessForOperation進(jìn)入下一步判斷。

①. 拿到imageCache,拿到緩存類型queryCacheType

②. 通過 options判斷,走緩存還是下載。如果走緩存,則調(diào)用SDImageCache里面的queryImageForKey(開始進(jìn)入SDImageCache的邏輯);如果走下載,則調(diào)用callDownloadProcessForOperation開始下載前的一些處理。

①、拿到imageCache,拿到緩存類型queryCacheType
// Grab the image cache to use
    // 查看是否有傳進(jìn)來的自定義緩存對(duì)象,沒有就用默認(rèn)的imageCache
    id<SDImageCache> imageCache;
    if ([context[SDWebImageContextImageCache] conformsToProtocol:@protocol(SDImageCache)]) {
        imageCache = context[SDWebImageContextImageCache];
    } else {
        imageCache = self.imageCache;
    }
    // Get the query cache type
    // 查看緩存類型,默認(rèn)是all,如果有傳進(jìn)來的就用傳進(jìn)來的
    SDImageCacheType queryCacheType = SDImageCacheTypeAll;
    if (context[SDWebImageContextQueryCacheType]) {
        queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue];
    }
②、通過options,判斷緩存查找,還是下載
// Check whether we should query cache
    // SD_OPTIONS_CONTAINS為與運(yùn)算,當(dāng)options為SDWebImageFromLoaderOnly時(shí)為true(或者全是1也可以)
    // 注意這里是取反,也就是設(shè)置了SDWebImageFromLoaderOnly后是不走緩存,直接下載
    BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly);
    if (shouldQueryCache) {
        // 拿到緩存的key
        NSString *key = [self cacheKeyForURL:url context:context];
        @weakify(operation);
        // 緩存查詢,并返回緩存任務(wù)
        operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
            @strongify(operation);
            // 如果沒有執(zhí)行的任務(wù),或者任務(wù)被取消了
            if (!operation || operation.isCancelled) {
                // Image combined operation cancelled by user
                // 拋出錯(cuò)誤
                [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] url:url];
                // 安全的移除任務(wù)
                [self safelyRemoveOperationFromRunning:operation];
                return;
            } else if (context[SDWebImageContextImageTransformer] && !cachedImage) {
                // 沒拿到緩存圖片,且圖片有經(jīng)過Transformer轉(zhuǎn)化,那就去查詢?cè)紙D片緩存
                //有機(jī)會(huì)去查詢?cè)季彺?                // Have a chance to query original cache instead of downloading
                [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];
                return;
            }
            
            // Continue download process
            // 走下載流程
            [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
        }];
    } else {// 走下載流程
        // Continue download process
        [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
    }

這里解釋一下key是怎么拿(SDWebImage的緩存key是怎么樣的),邏輯在這個(gè)方法里面cacheKeyForURL,代碼就不貼出來了,說一下大概邏輯。

a、SDWebImagecontext里面有個(gè)SDWebImageContextCacheKeyFilter,里面存儲(chǔ)的是用來存放自定義key邏輯的協(xié)議,通過重寫cacheKeyForURL自定義key,如果沒有傳SDWebImageContextCacheKeyFilter進(jìn)來則使用url的string值。
b、然后通過context里面的SDWebImageContextImageThumbnailPixelSize、SDWebImageContextImagePreserveAspectRatioSDWebImageContextImageTransformer這3個(gè)里面是否有值,如果有值就加上上面的key進(jìn)行拼接,沒值就直接用上面的key

查到緩存后就是回調(diào)了,回調(diào)看代碼注釋,問題應(yīng)該不大,要注意的是它也走了callDownloadProcessForOperation這個(gè)方法,因?yàn)?code>options為SDWebImageRefreshCached的情況下,也是要走下載的,所以索性將找到的緩存,放到callDownloadProcessForOperation處理,而不是直接回調(diào)。
接下來看一下SDImageCache模塊,看看SDWebImage是如何查詢緩存的。

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

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

  • 技術(shù)無極限,從菜鳥開始,從源碼開始。 由于公司目前項(xiàng)目還是用OC寫的項(xiàng)目,沒有升級(jí)swift 所以暫時(shí)SDWebI...
    充滿活力的早晨閱讀 12,850評(píng)論 0 2
  • 1. 前言 大名鼎鼎SDWebImage不用多說,相信每一個(gè)iOS程序員或多或少都有了解。比如我,之前就大概只知道...
    wilsonhan閱讀 1,021評(píng)論 0 0
  • 本文基于 SDWebImage 5.6。重讀的原因也是由于發(fā)現(xiàn)它的 API 在不斷迭代,許多結(jié)構(gòu)已經(jīng)不同與早期版本...
    土土Edmond木閱讀 888評(píng)論 0 2
  • 1.首先使用SDWebImage才能進(jìn)行剖析(以UIImageView+WebCache.h為例) 附:由于有些操...
    一川煙草i蓑衣閱讀 567評(píng)論 0 0
  • SDWebImage是一個(gè)圖片下載的開源項(xiàng)目,由于它提供了簡介的接口以及異步下載與緩存的強(qiáng)大功能,深受“猿媛“的喜...
    foolishBoy閱讀 4,097評(píng)論 1 23

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