SDWebImage源碼解讀

SDWebImage是一個(gè)開(kāi)源的第三方庫(kù),它提供了UIImageView的分類來(lái)實(shí)現(xiàn)從網(wǎng)絡(luò)端下載數(shù)據(jù)并緩存到內(nèi)存和磁盤(pán)。

SDWebImage有如下特點(diǎn):

  • 提供了UIImageView和UIButton的分類。以支持加載網(wǎng)絡(luò)圖片并緩存。
  • 一個(gè)異步的圖片下載器
  • 提供異步的內(nèi)存和磁盤(pán)緩存,并自動(dòng)處理緩存過(guò)期。
  • 后臺(tái)圖片解壓縮處理。
  • 確保同一個(gè)URL不會(huì)被下載多次。
  • 確保主線程永遠(yuǎn)不會(huì)阻塞。

一.儲(chǔ)備知識(shí)

SDWebImage中每一個(gè)下載任務(wù)都是一個(gè)SDWebImageDownloaderOperation,而SDWebImageDownloaderOperation又是繼承自NSOperation,所以每一個(gè)下載任務(wù)對(duì)應(yīng)一個(gè)NSOperation。在SDWebImage中使用SDWebImageDownloader來(lái)管理
多個(gè)下載任務(wù),在SDWebImageDownloader中有一個(gè)downloadedQueue這個(gè)屬性,這個(gè)屬性是NSOperationQueue類型的,也就是用NSOperationQueue來(lái)管理NSOperation
下面我們就一起學(xué)習(xí)一下NSOperationNSOperationQueue

NSOperation和NSOperationQueue

NSOperation是一個(gè)抽象類,用來(lái)表示與單個(gè)任務(wù)相關(guān)聯(lián)的代碼和數(shù)據(jù)。

NSOperation類是一個(gè)抽象類,我們不能直接去使用它而是應(yīng)該創(chuàng)建一個(gè)子類來(lái)繼承它。雖然它只是一個(gè)抽象類,但是它的基本實(shí)現(xiàn)還是提供了很有價(jià)值的邏輯來(lái)確保任務(wù)的安全執(zhí)行。這種原生的邏輯可以讓我們專注于任務(wù)的真正的實(shí)現(xiàn),而不需要用一些膠水代碼去確保這個(gè)任務(wù)能正常工作在其他地方。

我們可以把一個(gè)NSOperation對(duì)象加入到一個(gè)operation queue中,讓這個(gè)operation queue去決定什么時(shí)候執(zhí)行這個(gè)operation。當(dāng)使用operation queue去管理operation時(shí),輪到某個(gè)operation執(zhí)行時(shí)實(shí)際是去執(zhí)行這個(gè)operation的start方法,所以我們一個(gè)operation對(duì)象實(shí)際要執(zhí)行的任務(wù)應(yīng)該放在start方法里面。如果我們不想使用operation queue,也可以通過(guò)手動(dòng)調(diào)用NSOperation的start方法來(lái)執(zhí)行任務(wù)。

我們可以通過(guò)添加依賴來(lái)確定operation queue中operation的具體執(zhí)行順序。添加依賴和移除依賴可以使用下列的API:

//添加依賴
- (void)addDependency:(NSOperation *)op;
//移除依賴
- (void)removeDependency:(NSOperation *)op;

只要當(dāng)一個(gè)operation對(duì)象的所有依賴都執(zhí)行完成的時(shí)候,其才可以變成熟ready狀態(tài),然后才可以被執(zhí)行。如果一個(gè)operation沒(méi)有添加依賴,直接加入了operation queue中,那么就會(huì)按照加入隊(duì)列的先后順序,當(dāng)這個(gè)operation的前一個(gè)operation執(zhí)行完成以后,其狀態(tài)才會(huì)變成ready,才能被執(zhí)行。

NSOperation對(duì)象有下列四個(gè)比較重要的狀態(tài):

  • isCancelled
  • isExecuting
  • isFinished
  • isReady
    其中isExecutingisFinishedisReady這三種狀態(tài)相當(dāng)于是operation對(duì)象的生命周期:
    operation生命周期.png

    isCancelled這種狀態(tài)則比較特殊,當(dāng)我們對(duì)operation對(duì)象調(diào)用- (void)cancel方法時(shí),其isCancelled屬性會(huì)被置為YES。這個(gè)時(shí)候要分兩種情況:
  • operation正在執(zhí)行
    也就是說(shuō)其狀態(tài)現(xiàn)在是isExecuting,調(diào)用- (void)cancel方法后會(huì)馬上停止執(zhí)行當(dāng)前任務(wù),并且狀態(tài)變?yōu)?code>isFinished,isCancelled = Yes
  • operation還沒(méi)開(kāi)始執(zhí)行
    這個(gè)時(shí)候operation的狀態(tài)其實(shí)是isReady這個(gè)狀態(tài)之前,operation還沒(méi)開(kāi)始執(zhí)行,調(diào)用- (void)cancel方法后會(huì)去調(diào)用operation的start方法,在start方法里我們要去處理cancel事件,并設(shè)置isFinished = YES
    調(diào)用cancel方法.png
SDWebImageOptions

在SDWebImage中大量使用了option類型,通過(guò)判斷option類型的值來(lái)決定下一步應(yīng)該怎么做,所以如果對(duì)這些option值一點(diǎn)都不了解的話可能理解起源碼來(lái)也會(huì)非常難受。SDWebImageOptions是暴露在外的可供使用者使用的option。還有一些option比如SDImageCacheOptions, SDWebImageDownloaderOptions這些都是不暴露給用戶使用的。源碼中是根據(jù)用戶設(shè)置的SDWebImageOptions這個(gè)option來(lái)確定另外兩個(gè)option的值。
下面我們來(lái)具體看一下SDWebImageOptions

SDWebImageOptions.png
SDImageCacheOptions

SDImageCacheOptions.png

這里我們也可以看到,SDImageCacheOptions中的三個(gè)選項(xiàng)在SDWebImageOptions中都有對(duì)應(yīng)的選項(xiàng)。

  • SDImageCacheQueryDataWhenInMemory對(duì)應(yīng)SDWebImageQueryDataWhenInMemory
  • SDImageCacheQueryDiskSync對(duì)應(yīng)SDWebImageQueryDiskSync
  • SDImageCacheScaleDownLargeImages對(duì)應(yīng)SDWebImageScaleDownLargeImages

SDWebImageDownloaderOptions

SDWebImageDownloaderOptions.png

SDWebImageDownloaderOptions中所有選項(xiàng)在SDWebImageOptions中也有相對(duì)應(yīng)的選項(xiàng),這里不再贅述。

SDImageCacheType

    //從緩存中得不到數(shù)據(jù)
    SDImageCacheTypeNone,
    
    //從磁盤(pán)緩存中得到數(shù)據(jù)
    SDImageCacheTypeDisk,
    
    //從內(nèi)存緩存中得到數(shù)據(jù)
    SDImageCacheTypeMemory

框架的主要類和一次圖片加載的主要流程

框架的主要類

主要類之間的關(guān)系.png
從上圖也可以看出,整個(gè)框架主要分為三部分,即圖片的下載,圖片的緩存,和處理圖片相關(guān)的類。

一次圖片加載的主要流程

加載圖片流程圖.png

針對(duì)上圖中一次圖片加載的主要流程,每一步做介紹:

  • 1.SDWebImage為UIImagView創(chuàng)建了一個(gè)分類UIImageView (WebCache),然后UIImageView對(duì)象可以調(diào)用這個(gè)分類的方法來(lái)下載圖片:
    [imageView sd_setImageWithURL:[NSURL URLWithString:@""]];
  • 2.UIImageView (WebCache)- (void)sd_setImageWithURL:(nullable NSURL *)url方法實(shí)際調(diào)用了UIView (WebCache)的下列方法:
- (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;
  • 3.UIView (WebCache)的上述方法在實(shí)現(xiàn)時(shí)會(huì)創(chuàng)建一個(gè)SDWebImageManager的實(shí)例對(duì)象,然后調(diào)用其下列方法來(lái)加載圖片:
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock;
  • 4.在SDWebImageManager對(duì)象的上述方法里,首先會(huì)查詢?cè)诰彺嬷杏袥](méi)有這個(gè)圖片,然后根據(jù)各種option的判斷決定是否要從網(wǎng)絡(luò)端下載。查詢緩存中有沒(méi)有是通過(guò)調(diào)用SDImageCache對(duì)象的實(shí)例方法來(lái)實(shí)現(xiàn)的:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key 
                                            options:(SDImageCacheOptions)options 
                                               done:(nullable SDCacheQueryCompletedBlock)doneBlock;
  • 5.返回緩存查詢的結(jié)果
  • 6.如果需要下載圖片,那么調(diào)用SDWebImageDownloader對(duì)象的下列方法進(jìn)行下載:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
  • 7.獲取從網(wǎng)絡(luò)端下載的圖片。
  • 8.判斷是否要將下載的圖片進(jìn)行緩存,如果需要,則緩存。
  • 9.把通過(guò)SDWebImageManager對(duì)象獲取的圖片顯示在UIImageView上。

源碼分析

這一部分我們進(jìn)行詳細(xì)的源碼分析。
首先從SDWebImageManager類的loadImageWithURL:方法看起:
由于代碼比較長(zhǎng),我就采用注釋的方式

- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock {
    // Invoking this method without a completedBlock is pointless
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
    
    //如果傳進(jìn)來(lái)的是一個(gè)NSString,則把NSString轉(zhuǎn)化為NSURL
    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;
    }
    
    SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    operation.manager = self;
    
    //self.failedURLs是nsurl的黑名單,一般情況下,如果URL在這個(gè)黑名單里,那么就不會(huì)嘗試加載這個(gè)圖片,直接返回
    BOOL isFailedUrl = NO;
    if (url) {
        LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        UNLOCK(self.failedURLsLock);
    }
    
    //SDWebImageRetryFailed即即使URL被加入了黑名單,也要嘗試加載這個(gè)URL對(duì)應(yīng)的圖片
    //如果URL長(zhǎng)度為0,或者URL被加入了黑名單并且沒(méi)有設(shè)置SDWebImageRetryFailed,那么就直接回調(diào)完成的block
    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;
    }
    
    LOCK(self.runningOperationsLock);
    [self.runningOperations addObject:operation];
    UNLOCK(self.runningOperationsLock);
    NSString *key = [self cacheKeyForURL:url];
    
    //由于我們?cè)谑褂肁PI的時(shí)候只設(shè)置SDWebImageOptions,所以這里就是根據(jù)用戶設(shè)置的SDWebImageOptions去設(shè)置SDImageCacheOptions
    SDImageCacheOptions cacheOptions = 0;
    if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
    if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
    if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
    
    __weak SDWebImageCombinedOperation *weakOperation = operation;
    //這里開(kāi)始調(diào)用SDImageCache對(duì)象的queryCacheOperationForKey:方法去緩存中查找有沒(méi)有這個(gè)URL對(duì)應(yīng)的圖片
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key options:cacheOptions done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        __strong __typeof(weakOperation) strongOperation = weakOperation;
        if (!strongOperation || strongOperation.isCancelled) {
            [self safelyRemoveOperationFromRunning:strongOperation];
            return;
        }
        
        // 判斷我們是否需要從網(wǎng)絡(luò)端下載圖片
        //首先檢查沒(méi)有設(shè)置只能從緩存中獲取,然后檢查cachedImage = nil或者設(shè)置了要刷新緩存,則需要從網(wǎng)絡(luò)端下載圖片
        BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
        && (!cachedImage || options & SDWebImageRefreshCached)
        && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
        if (shouldDownload) {
            //從緩存中獲取了圖片并且設(shè)置了要刷新緩存這個(gè)option,則要進(jìn)行兩次完成的回調(diào),這是第一次回調(diào)
            if (cachedImage && options & SDWebImageRefreshCached) {
                // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
                // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
                [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }
            
            // download if no image or requested to refresh anyway, and download allowed by delegate
            //這里是根據(jù)用戶設(shè)置的SDWebImageOptions來(lái)手動(dòng)設(shè)置SDWebImageDownloaderOptions
            SDWebImageDownloaderOptions downloaderOptions = 0;
            if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority;
            if (options & SDWebImageProgressiveDownload) downloaderOptions |= SDWebImageDownloaderProgressiveDownload;
            if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache;
            if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground;
            if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies;
            if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates;
            if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority;
            if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages;
            
            //如果已經(jīng)從緩存中獲取了圖片并且設(shè)置了要刷新緩存
            if (cachedImage && options & SDWebImageRefreshCached) {
                
                //這里其實(shí)就是把SDWebImageDownloaderProgressiveDownload這個(gè)option去掉
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // ignore image read from NSURLCache if image if cached but force refreshing
                //加上SDWebImageDownloaderIgnoreCachedResponse這個(gè)option,忽略NSURLCache中緩存的response
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            

            __weak typeof(strongOperation) weakSubOperation = strongOperation;
            
            //l開(kāi)始進(jìn)行圖片的下載
            strongOperation.downloadToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong typeof(weakSubOperation) strongSubOperation = weakSubOperation;
                if (!strongSubOperation || strongSubOperation.isCancelled) {
                    // Do nothing if the operation was cancelled
                    // See #699 for more details
                    // if we would call the completedBlock, there could be a race condition between this block and another completedBlock for the same object, so if this one is called second, we will overwrite the new data
                } else if (error) {
                    [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock error:error url:url];
                    BOOL shouldBlockFailedURL;
                    // 后面都是判斷在請(qǐng)求失敗的情況下是否應(yīng)該把
                    if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) {
                        shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error];
                    } else {
                        shouldBlockFailedURL = (   error.code != NSURLErrorNotConnectedToInternet
                                                && error.code != NSURLErrorCancelled
                                                && error.code != NSURLErrorTimedOut
                                                && error.code != NSURLErrorInternationalRoamingOff
                                                && error.code != NSURLErrorDataNotAllowed
                                                && error.code != NSURLErrorCannotFindHost
                                                && error.code != NSURLErrorCannotConnectToHost
                                                && error.code != NSURLErrorNetworkConnectionLost);
                    }
                    
                    if (shouldBlockFailedURL) {
                        LOCK(self.failedURLsLock);
                        [self.failedURLs addObject:url];
                        UNLOCK(self.failedURLsLock);
                    }
                }
                else {
                    //如果設(shè)置了SDWebImageRetryFailed那么就要把URL從黑名單中移除
                    if ((options & SDWebImageRetryFailed)) {
                        LOCK(self.failedURLsLock);
                        [self.failedURLs removeObject:url];
                        UNLOCK(self.failedURLsLock);
                    }
                    
                    //判斷是否應(yīng)該把下載的圖片緩存到磁盤(pán),SDWebImageCacheMemoryOnly這個(gè)option表示只把圖片緩存到內(nèi)存
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
                    
                    // We've done the scale process in SDWebImageDownloader with the shared manager, this is used for custom manager and avoid extra scale.
                    if (self != [SDWebImageManager sharedManager] && self.cacheKeyFilter && downloadedImage) {
                        downloadedImage = [self scaledImageForKey:key image:downloadedImage];
                    }
                    
                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                        // Image refresh hit the NSURLCache cache, do not call the completion block
                    } else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
                        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                            @autoreleasepool {
                                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);
                                    }
                                    [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];
                            }
                        });
                    } else {
                        //可以直接看到這一部分
                        if (downloadedImage && finished) {
                            if (self.cacheSerializer) {
                                dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                                    @autoreleasepool {
                                        NSData *cacheData = self.cacheSerializer(downloadedImage, downloadedData, url);
                                        [self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key toDisk:cacheOnDisk completion:nil];
                                    }
                                });
                            } else {
                                //對(duì)圖片進(jìn)行緩存
                                [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                        }
                        //第二次調(diào)用完成的block
                        [self callCompletionBlockForOperation:strongSubOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }
                
                if (finished) {
                    [self safelyRemoveOperationFromRunning:strongSubOperation];
                }
            }];
            //如果從從緩存中獲取了圖片并且不需要下載
        } else if (cachedImage) {
            //執(zhí)行完成的回調(diào)
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:strongOperation];
        } else {
            // 緩存中沒(méi)有獲取圖片,也不用下載
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:strongOperation];
        }
    }];
    
    return operation;
}

總結(jié)一下SDWebImageManagerloadImageWithURL:所做的事情:

其實(shí)在loadImageWithURL:里面做了加載圖片的完整流程。首先檢查傳入的NSURL的有效性。然后開(kāi)始從緩存中查找是否有這個(gè)圖片,得到查詢結(jié)果之后再根據(jù)查詢結(jié)果和設(shè)置的option判斷是否需要進(jìn)行下載操作,如果不需要下載操作那么就直接使用cache image進(jìn)行下載回調(diào)。如果需要進(jìn)行下載操作那么就開(kāi)始下載,下載完成后按照設(shè)置的option將圖片緩存到內(nèi)存和磁盤(pán),最后進(jìn)行完成的回調(diào)。

然后我們看一下查詢緩存的具體過(guò)程,也就是SDImageCache這個(gè)類的queryCacheOperationForKey:方法:
這里也是采用注釋的方式

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    
    // 首先檢查內(nèi)存緩存中有沒(méi)有這個(gè)圖片,注意內(nèi)存緩存使用的是NSCache,它是一個(gè)類字典結(jié)構(gòu),使用圖片對(duì)應(yīng)的nsurl作為key,在查詢的時(shí)候就用這個(gè)key去查詢
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    
    //是否只查詢內(nèi)存緩存(如果從內(nèi)存緩存中獲取了圖片并且沒(méi)有設(shè)置SDImageCacheQueryDataWhenInMemory,那么就只查詢內(nèi)存緩存)
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            //執(zhí)行回調(diào)
            doneBlock(image, nil, SDImageCacheTypeMemory);
        }
        return nil;
    }
    
    NSOperation *operation = [NSOperation new];
    void(^queryDiskBlock)(void) =  ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }
        
        @autoreleasepool {
            //從磁盤(pán)中查詢
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            //j緩存獲取的類型,有三種m類型,none,memory,disk
            SDImageCacheType cacheType = SDImageCacheTypeNone;
            if (image) {
                // 圖片是從內(nèi)存緩存中獲取的
                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
            } else if (diskData) {
                //圖片是從磁盤(pán)緩存中獲取的
                cacheType = SDImageCacheTypeDisk;
                // 解壓圖片
                diskImage = [self diskImageForKey:key data:diskData options:options];
                //判斷是否需要把圖片緩存到內(nèi)存
                if (diskImage && self.config.shouldCacheImagesInMemory) {
                    NSUInteger cost = SDCacheCostForImage(diskImage);
                    //將圖片緩存到內(nèi)存
                    [self.memCache setObject:diskImage forKey:key cost:cost];
                }
            }
            
            if (doneBlock) {
                if (options & SDImageCacheQueryDiskSync) {
                    doneBlock(diskImage, diskData, cacheType);
                } else {
                    dispatch_async(dispatch_get_main_queue(), ^{
                        doneBlock(diskImage, diskData, cacheType);
                    });
                }
            }
        }
    };
    
    if (options & SDImageCacheQueryDiskSync) {
        queryDiskBlock();
    } else {
        dispatch_async(self.ioQueue, queryDiskBlock);
    }
    
    return operation;
}

總結(jié)一下queryCacheOperationForKey:方法所做的事情:

SDImageCache這個(gè)類是專門(mén)負(fù)責(zé)緩存相關(guān)的問(wèn)題的,包括查詢緩存和將圖片進(jìn)行緩存。SDImageCache使用了一個(gè)NSCache對(duì)象來(lái)進(jìn)行內(nèi)存緩存,磁盤(pán)緩存則是把圖片數(shù)據(jù)存放在應(yīng)用沙盒的Caches這個(gè)文件夾下。

首先查詢內(nèi)存緩存,內(nèi)存緩存查詢完了以后再判斷是否需要查詢磁盤(pán)緩存。如果查詢內(nèi)存緩存已經(jīng)有了結(jié)果并且沒(méi)有設(shè)置一定要查詢磁盤(pán)緩存,那么就不查詢磁盤(pán)緩存,否則就要查詢磁盤(pán)緩存。內(nèi)存緩存沒(méi)有查詢到圖片,并且磁盤(pán)緩存查詢到了圖片,那么就要把這個(gè)內(nèi)容緩存到內(nèi)存緩存中。

圖片的緩存查詢完成后我們?cè)賮?lái)看一下下載操作,即SDWebImageDownloaderdownloadImageWithURL:方法

- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock {
    // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data.
    if (url == nil) {
        if (completedBlock != nil) {
            completedBlock(nil, nil, nil, NO);
        }
        return nil;
    }
    
    LOCK(self.operationsLock);
    NSOperation<SDWebImageDownloaderOperationInterface> *operation = [self.URLOperations objectForKey:url];
    if (!operation || operation.isFinished) {
        
        //創(chuàng)建一下下載的operation
        operation = [self createDownloaderOperationWithUrl:url options:options];
        __weak typeof(self) wself = self;
        operation.completionBlock = ^{
            __strong typeof(wself) sself = wself;
            if (!sself) {
                return;
            }
            LOCK(sself.operationsLock);
            [sself.URLOperations removeObjectForKey:url];
            UNLOCK(sself.operationsLock);
        };
        [self.URLOperations setObject:operation forKey:url];
        // Add operation to operation queue only after all configuration done according to Apple's doc.
        // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock.
        
        //把operation加入到nNSOperationQueue中去
        [self.downloadQueue addOperation:operation];
    }
    UNLOCK(self.operationsLock);

    //這一部分代碼是在取消operation的時(shí)候使用
    id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
    
    SDWebImageDownloadToken *token = [SDWebImageDownloadToken new];
    token.downloadOperation = operation;
    token.url = url;
    token.downloadOperationCancelToken = downloadOperationCancelToken;

    return token;
}

SDWebImageDownloader這個(gè)類是專門(mén)管理下載的,它有一個(gè)屬性是downloadQueue,這是一個(gè)NSOperationQueue,每創(chuàng)建一個(gè)新的下載任務(wù)都把它加入到這個(gè)downloadQueue中,讓downloadQueue去管理任務(wù)的開(kāi)始,取消,結(jié)束。

上面的方法其實(shí)做的事情很簡(jiǎn)單,就是創(chuàng)建了一個(gè)下載圖片的operation,然后把它加入到了downloadQueue中去。

下面我們來(lái)具體看一下創(chuàng)建下載圖片的operation的過(guò)程,即SDWebImageDownloader類的createDownloaderOperationWithUrl:方法:

- (NSOperation<SDWebImageDownloaderOperationInterface> *)createDownloaderOperationWithUrl:(nullable NSURL *)url
                                                                                  options:(SDWebImageDownloaderOptions)options {
    NSTimeInterval timeoutInterval = self.downloadTimeout;
    if (timeoutInterval == 0.0) {
        timeoutInterval = 15.0;
    }

    // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise
    NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData;
    NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url
                                                                cachePolicy:cachePolicy
                                                            timeoutInterval:timeoutInterval];
    
    request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
    request.HTTPShouldUsePipelining = YES;
    if (self.headersFilter) {
        request.allHTTPHeaderFields = self.headersFilter(url, [self allHTTPHeaderFields]);
    }
    else {
        request.allHTTPHeaderFields = [self allHTTPHeaderFields];
    }
    
    //前面都是為了創(chuàng)建一個(gè)request,然后使用request和session對(duì)象去創(chuàng)建下載的operation
    NSOperation<SDWebImageDownloaderOperationInterface> *operation = [[self.operationClass alloc] initWithRequest:request inSession:self.session options:options];
    operation.shouldDecompressImages = self.shouldDecompressImages;
    
    if (self.urlCredential) {
        operation.credential = self.urlCredential;
    } else if (self.username && self.password) {
        operation.credential = [NSURLCredential credentialWithUser:self.username password:self.password persistence:NSURLCredentialPersistenceForSession];
    }
    
    //設(shè)置operation的隊(duì)列優(yōu)先級(jí)
    if (options & SDWebImageDownloaderHighPriority) {
        operation.queuePriority = NSOperationQueuePriorityHigh;
    } else if (options & SDWebImageDownloaderLowPriority) {
        operation.queuePriority = NSOperationQueuePriorityLow;
    }
    
    //如果設(shè)置的執(zhí)行順序是xLIFI,即后進(jìn)先出,則要把queue中的最后一個(gè)加入的operation的依賴設(shè)置為該operation,這樣來(lái)保證這個(gè)operation最先執(zhí)行
    if (self.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
        // Emulate LIFO execution order by systematically adding new operations as last operation's dependency
        [self.lastAddedOperation addDependency:operation];
        self.lastAddedOperation = operation;
    }

    return operation;
}

這個(gè)方法就是創(chuàng)建了一個(gè)request對(duì)象,然后使用這個(gè)request對(duì)象和session對(duì)象去創(chuàng)建下載的operation對(duì)象。

我們看一下負(fù)責(zé)單個(gè)下載任務(wù)的operation對(duì)象到底是怎么創(chuàng)建的,即SDWebImageDownloaderOperation類的- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request inSession:(nullable NSURLSession *)session options:(SDWebImageDownloaderOptions)options方法:

- (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request
                              inSession:(nullable NSURLSession *)session
                                options:(SDWebImageDownloaderOptions)options {
    if ((self = [super init])) {
        _request = [request copy];
        _shouldDecompressImages = YES;
        _options = options;
        _callbackBlocks = [NSMutableArray new];
        _executing = NO;
        _finished = NO;
        _expectedSize = 0;
        _unownedSession = session;
        _callbacksLock = dispatch_semaphore_create(1);
        _coderQueue = dispatch_queue_create("com.hackemist.SDWebImageDownloaderOperationCoderQueue", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

這個(gè)初始化方法其實(shí)也很簡(jiǎn)單,就是給自己的成員變量賦值

我們知道,NSOperation類的真正執(zhí)行任務(wù)是在其start方法里面,那么我們看一下SDWebImageDownloaderOperationstart方法的具體實(shí)現(xiàn):
代碼比較長(zhǎng),我在關(guān)鍵部分加了注釋

- (void)start {
    @synchronized (self) {
        if (self.isCancelled) {
            self.finished = YES;
            [self reset];
            return;
        }

#if SD_UIKIT
        //這一部分就是解決在后臺(tái)仍然進(jìn)行下載的問(wèn)題
        Class UIApplicationClass = NSClassFromString(@"UIApplication");
        BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)];
        if (hasApplication && [self shouldContinueWhenAppEntersBackground]) {
            __weak __typeof__ (self) wself = self;
            UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)];
            self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{
                __strong __typeof (wself) sself = wself;

                if (sself) {
                    [sself cancel];

                    [app endBackgroundTask:sself.backgroundTaskId];
                    sself.backgroundTaskId = UIBackgroundTaskInvalid;
                }
            }];
        }
#endif
        NSURLSession *session = self.unownedSession;
        //創(chuàng)建一個(gè)session對(duì)象,因?yàn)楹竺嬉獎(jiǎng)?chuàng)建NSURLSessionTask,需要session對(duì)象
        if (!session) {
            NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
            sessionConfig.timeoutIntervalForRequest = 15;
            
            /**
             *  Create the session for this task
             *  We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate
             *  method calls and completion handler calls.
             */
            session = [NSURLSession sessionWithConfiguration:sessionConfig
                                                    delegate:self
                                               delegateQueue:nil];
            self.ownedSession = session;
        }
        
        if (self.options & SDWebImageDownloaderIgnoreCachedResponse) {
            // Grab the cached data for later check
            NSURLCache *URLCache = session.configuration.URLCache;
            if (!URLCache) {
                URLCache = [NSURLCache sharedURLCache];
            }
            NSCachedURLResponse *cachedResponse;
            // NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483
            @synchronized (URLCache) {
                cachedResponse = [URLCache cachedResponseForRequest:self.request];
            }
            if (cachedResponse) {
                self.cachedData = cachedResponse.data;
            }
        }
        
        //創(chuàng)建dataTask,這個(gè)才是真正執(zhí)行下載任務(wù)的
        self.dataTask = [session dataTaskWithRequest:self.request];
        self.executing = YES;
    }

    if (self.dataTask) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
        if ([self.dataTask respondsToSelector:@selector(setPriority:)]) {
            if (self.options & SDWebImageDownloaderHighPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityHigh;
            } else if (self.options & SDWebImageDownloaderLowPriority) {
                self.dataTask.priority = NSURLSessionTaskPriorityLow;
            }
        }
#pragma clang diagnostic pop
        [self.dataTask resume];
        for (SDWebImageDownloaderProgressBlock progressBlock in [self callbacksForKey:kProgressCallbackKey]) {
            progressBlock(0, NSURLResponseUnknownLength, self.request.URL);
        }
        __weak typeof(self) weakSelf = self;
        dispatch_async(dispatch_get_main_queue(), ^{
            [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:weakSelf];
        });
    } else {
        [self callCompletionBlocksWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnknown userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]];
        [self done];
        return;
    }

#if SD_UIKIT
    Class UIApplicationClass = NSClassFromString(@"UIApplication");
    if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) {
        return;
    }
    if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
        UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)];
        [app endBackgroundTask:self.backgroundTaskId];
        self.backgroundTaskId = UIBackgroundTaskInvalid;
    }
#endif
}

這里就是通過(guò)一個(gè)session對(duì)象和一個(gè)request對(duì)象創(chuàng)建了一個(gè)dataTask對(duì)象,這個(gè)dataTask對(duì)象才是真正用來(lái)下載的,然后調(diào)用[self.dataTask resume]執(zhí)行下載。

到這里SDWebImage的源碼分析就結(jié)束啦。

參考:
SDWebImage實(shí)現(xiàn)分析

這篇文章在簡(jiǎn)書(shū)的地址:SDWebImage源碼解讀

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容