SDWebImage源碼解讀

SDWebImage是一個開源的第三方庫,它提供了UIImageView的分類來實現從網絡端下載數據並緩存到內存和磁盤。git

SDWebImage有以下特色:github

  • 提供了UIImageView和UIButton的分類。以支持加載網絡圖片並緩存。
  • 一個異步的圖片下載器
  • 提供異步的內存和磁盤緩存,並自動處理緩存過時。
  • 後臺圖片解壓縮處理。
  • 確保同一個URL不會被下載屢次。
  • 確保主線程永遠不會阻塞。

一.儲備知識

SDWebImage中每個下載任務都是一個SDWebImageDownloaderOperation,而SDWebImageDownloaderOperation又是繼承自NSOperation,因此每個下載任務對應一個NSOperation。在SDWebImage中使用SDWebImageDownloader來管理 多個下載任務,在SDWebImageDownloader中有一個downloadedQueue這個屬性,這個屬性是NSOperationQueue類型的,也就是用NSOperationQueue來管理NSOperation。 下面咱們就一塊兒學習一下NSOperationNSOperationQueueweb

NSOperation和NSOperationQueue

NSOperation是一個抽象類,用來表示與單個任務相關聯的代碼和數據。緩存

NSOperation類是一個抽象類,咱們不能直接去使用它而是應該建立一個子類來繼承它。雖然它只是一個抽象類,可是它的基本實現仍是提供了頗有價值的邏輯來確保任務的安全執行。這種原生的邏輯可讓咱們專一於任務的真正的實現,而不須要用一些膠水代碼去確保這個任務能正常工做在其餘地方。安全

咱們能夠把一個NSOperation對象加入到一個operation queue中,讓這個operation queue去決定何時執行這個operation。**當使用operation queue去管理operation時,輪到某個operation執行時實際是去執行這個operation的start方法,因此咱們一個operation對象實際要執行的任務應該放在start方法裏面。**若是咱們不想使用operation queue,也能夠經過手動調用NSOperation的start方法來執行任務。bash

咱們能夠經過添加依賴來肯定operation queue中operation的具體執行順序。添加依賴和移除依賴可使用下列的API:網絡

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

只要當一個operation對象的全部依賴都執行完成的時候,其才能夠變成熟ready狀態,而後才能夠被執行。若是一個operation沒有添加依賴,直接加入了operation queue中,那麼就會按照加入隊列的前後順序,當這個operation的前一個operation執行完成之後,其狀態纔會變成ready,才能被執行。session

NSOperation對象有下列四個比較重要的狀態:app

  • isCancelled
  • isExecuting
  • isFinished
  • isReady 其中isExecutingisFinishedisReady這三種狀態至關因而operation對象的生命週期:
    operation生命週期.png
    isCancelled這種狀態則比較特殊,當咱們對operation對象調用- (void)cancel方法時,其isCancelled屬性會被置爲YES。這個時候要分兩種狀況:
  • operation正在執行 也就是說其狀態如今是isExecuting,調用- (void)cancel方法後會立刻中止執行當前任務,而且狀態變爲isFinishedisCancelled = Yes
  • operation還沒開始執行 這個時候operation的狀態實際上是isReady這個狀態以前,operation還沒開始執行,調用- (void)cancel方法後會去調用operation的start方法,在start方法裏咱們要去處理cancel事件,並設置isFinished = YES
    調用cancel方法.png
SDWebImageOptions

在SDWebImage中大量使用了option類型,經過判斷option類型的值來決定下一步應該怎麼作,因此若是對這些option值一點都不瞭解的話可能理解起源碼來也會很是難受。SDWebImageOptions是暴露在外的可供使用者使用的option。還有一些option好比SDImageCacheOptions, SDWebImageDownloaderOptions這些都是不暴露給用戶使用的。源碼中是根據用戶設置的SDWebImageOptions這個option來肯定另外兩個option的值。 下面咱們來具體看一下SDWebImageOptions 框架

SDWebImageOptions.png
SDImageCacheOptions

SDImageCacheOptions.png
這裏咱們也能夠看到,SDImageCacheOptions中的三個選項在SDWebImageOptions中都有對應的選項。

  • SDImageCacheQueryDataWhenInMemory對應SDWebImageQueryDataWhenInMemory
  • SDImageCacheQueryDiskSync對應SDWebImageQueryDiskSync
  • SDImageCacheScaleDownLargeImages對應SDWebImageScaleDownLargeImages

SDWebImageDownloaderOptions

SDWebImageDownloaderOptions.png
SDWebImageDownloaderOptions中全部選項在SDWebImageOptions中也有相對應的選項,這裏再也不贅述。

SDImageCacheType

//從緩存中得不到數據
    SDImageCacheTypeNone,
    
    //從磁盤緩存中獲得數據
    SDImageCacheTypeDisk,
    
    //從內存緩存中獲得數據
    SDImageCacheTypeMemory
複製代碼

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

框架的主要類

主要類之間的關係.png
從上圖也能夠看出,整個框架主要分爲三部分,即圖片的下載,圖片的緩存,和處理圖片相關的類。

一次圖片加載的主要流程

加載圖片流程圖.png
針對上圖中一次圖片加載的主要流程,每一步作介紹:

  • 1.SDWebImage爲UIImagView建立了一個分類UIImageView (WebCache),而後UIImageView對象能夠調用這個分類的方法來下載圖片:
[imageView sd_setImageWithURL:[NSURL URLWithString:@""]];
複製代碼
  • 2.UIImageView (WebCache)- (void)sd_setImageWithURL:(nullable NSURL *)url方法實際調用了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)的上述方法在實現時會建立一個SDWebImageManager的實例對象,而後調用其下列方法來加載圖片:
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                     options:(SDWebImageOptions)options
                                    progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                   completed:(nullable SDInternalCompletionBlock)completedBlock;
複製代碼
  • 4.在SDWebImageManager對象的上述方法裏,首先會查詢在緩存中有沒有這個圖片,而後根據各類option的判斷決定是否要從網絡端下載。查詢緩存中有沒有是經過調用SDImageCache對象的實例方法來實現的:
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key 
                                            options:(SDImageCacheOptions)options 
                                               done:(nullable SDCacheQueryCompletedBlock)doneBlock;
複製代碼
  • 5.返回緩存查詢的結果
  • 6.若是須要下載圖片,那麼調用SDWebImageDownloader對象的下列方法進行下載:
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
                                                   options:(SDWebImageDownloaderOptions)options
                                                  progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                                 completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;
複製代碼
  • 7.獲取從網絡端下載的圖片。
  • 8.判斷是否要將下載的圖片進行緩存,若是須要,則緩存。
  • 9.把經過SDWebImageManager對象獲取的圖片顯示在UIImageView上。

源碼分析

這一部分咱們進行詳細的源碼分析。 首先從SDWebImageManager類的loadImageWithURL:方法看起: 因爲代碼比較長,我就採用註釋的方式

- (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");
    
    //若是傳進來的是一個NSString,則把NSString轉化爲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在這個黑名單裏,那麼就不會嘗試加載這個圖片,直接返回
    BOOL isFailedUrl = NO;
    if (url) {
        LOCK(self.failedURLsLock);
        isFailedUrl = [self.failedURLs containsObject:url];
        UNLOCK(self.failedURLsLock);
    }
    
    //SDWebImageRetryFailed即即便URL被加入了黑名單,也要嘗試加載這個URL對應的圖片
    //若是URL長度爲0,或者URL被加入了黑名單而且沒有設置SDWebImageRetryFailed,那麼就直接回調完成的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];
    
    //因爲咱們在使用API的時候只設置SDWebImageOptions,因此這裏就是根據用戶設置的SDWebImageOptions去設置SDImageCacheOptions
    SDImageCacheOptions cacheOptions = 0;
    if (options & SDWebImageQueryDataWhenInMemory) cacheOptions |= SDImageCacheQueryDataWhenInMemory;
    if (options & SDWebImageQueryDiskSync) cacheOptions |= SDImageCacheQueryDiskSync;
    if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
    
    __weak SDWebImageCombinedOperation *weakOperation = operation;
    //這裏開始調用SDImageCache對象的queryCacheOperationForKey:方法去緩存中查找有沒有這個URL對應的圖片
    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;
        }
        
        // 判斷咱們是否須要從網絡端下載圖片
        //首先檢查沒有設置只能從緩存中獲取,而後檢查cachedImage = nil或者設置了要刷新緩存,則須要從網絡端下載圖片
        BOOL shouldDownload = (!(options & SDWebImageFromCacheOnly))
        && (!cachedImage || options & SDWebImageRefreshCached)
        && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]);
        if (shouldDownload) {
            //從緩存中獲取了圖片而且設置了要刷新緩存這個option,則要進行兩次完成的回調,這是第一次回調
            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
            //這裏是根據用戶設置的SDWebImageOptions來手動設置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;
            
            //若是已經從緩存中獲取了圖片而且設置了要刷新緩存
            if (cachedImage && options & SDWebImageRefreshCached) {
                
                //這裏其實就是把SDWebImageDownloaderProgressiveDownload這個option去掉
                downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
                // ignore image read from NSURLCache if image if cached but force refreshing
                //加上SDWebImageDownloaderIgnoreCachedResponse這個option,忽略NSURLCache中緩存的response
                downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
            }
            

            __weak typeof(strongOperation) weakSubOperation = strongOperation;
            
            //l開始進行圖片的下載
            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;
                    // 後面都是判斷在請求失敗的狀況下是否應該把
                    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 {
                    //若是設置了SDWebImageRetryFailed那麼就要把URL從黑名單中移除
                    if ((options & SDWebImageRetryFailed)) {
                        LOCK(self.failedURLsLock);
                        [self.failedURLs removeObject:url];
                        UNLOCK(self.failedURLsLock);
                    }
                    
                    //判斷是否應該把下載的圖片緩存到磁盤,SDWebImageCacheMemoryOnly這個option表示只把圖片緩存到內存
                    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 { //對圖片進行緩存 [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil]; } } //第二次調用完成的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) { //執行完成的回調 [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url]; [self safelyRemoveOperationFromRunning:strongOperation]; } else { // 緩存中沒有獲取圖片,也不用下載 [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url]; [self safelyRemoveOperationFromRunning:strongOperation]; } }]; return operation; } 複製代碼

總結一下SDWebImageManagerloadImageWithURL:所作的事情:

其實在loadImageWithURL:裏面作了加載圖片的完整流程。首先檢查傳入的NSURL的有效性。而後開始從緩存中查找是否有這個圖片,獲得查詢結果以後再根據查詢結果和設置的option判斷是否須要進行下載操做,若是不須要下載操做那麼就直接使用cache image進行下載回調。若是須要進行下載操做那麼就開始下載,下載完成後按照設置的option將圖片緩存到內存和磁盤,最後進行完成的回調。

而後咱們看一下查詢緩存的具體過程,也就是SDImageCache這個類的queryCacheOperationForKey:方法: 這裏也是採用註釋的方式

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }
    
    // 首先檢查內存緩存中有沒有這個圖片,注意內存緩存使用的是NSCache,它是一個類字典結構,使用圖片對應的nsurl做爲key,在查詢的時候就用這個key去查詢
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    
    //是否只查詢內存緩存(若是從內存緩存中獲取了圖片而且沒有設置SDImageCacheQueryDataWhenInMemory,那麼就只查詢內存緩存)
    BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryDataWhenInMemory));
    if (shouldQueryMemoryOnly) {
        if (doneBlock) {
            //執行回調
            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 {
            //從磁盤中查詢
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage;
            //j緩存獲取的類型,有三種m類型,none,memory,disk
            SDImageCacheType cacheType = SDImageCacheTypeNone;
            if (image) {
                // 圖片是從內存緩存中獲取的
                diskImage = image;
                cacheType = SDImageCacheTypeMemory;
            } else if (diskData) {
                //圖片是從磁盤緩存中獲取的
                cacheType = SDImageCacheTypeDisk;
                // 解壓圖片
                diskImage = [self diskImageForKey:key data:diskData options:options];
                //判斷是否須要把圖片緩存到內存
                if (diskImage && self.config.shouldCacheImagesInMemory) {
                    NSUInteger cost = SDCacheCostForImage(diskImage);
                    //將圖片緩存到內存
                    [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;
}
複製代碼

總結一下queryCacheOperationForKey:方法所作的事情:

SDImageCache這個類是專門負責緩存相關的問題的,包括查詢緩存和將圖片進行緩存。SDImageCache使用了一個NSCache對象來進行內存緩存,磁盤緩存則是把圖片數據存放在應用沙盒的Caches這個文件夾下。

首先查詢內存緩存,內存緩存查詢完了之後再判斷是否須要查詢磁盤緩存。若是查詢內存緩存已經有告終果而且沒有設置必定要查詢磁盤緩存,那麼就不查詢磁盤緩存,不然就要查詢磁盤緩存。內存緩存沒有查詢到圖片,而且磁盤緩存查詢到了圖片,那麼就要把這個內容緩存到內存緩存中。

圖片的緩存查詢完成後咱們再來看一下下載操做,即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) {
        
        //建立一下下載的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的時候使用 id downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock]; SDWebImageDownloadToken *token = [SDWebImageDownloadToken new]; token.downloadOperation = operation; token.url = url; token.downloadOperationCancelToken = downloadOperationCancelToken; return token; } 複製代碼

SDWebImageDownloader這個類是專門管理下載的,它有一個屬性是downloadQueue,這是一個NSOperationQueue,每建立一個新的下載任務都把它加入到這個downloadQueue中,讓downloadQueue去管理任務的開始,取消,結束。

上面的方法其實作的事情很簡單,就是建立了一個下載圖片的operation,而後把它加入到了downloadQueue中去。

下面咱們來具體看一下建立下載圖片的operation的過程,即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];
    }
    
    //前面都是爲了建立一個request,而後使用request和session對象去建立下載的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];
    }
    
    //設置operation的隊列優先級
    if (options & SDWebImageDownloaderHighPriority) {
        operation.queuePriority = NSOperationQueuePriorityHigh;
    } else if (options & SDWebImageDownloaderLowPriority) {
        operation.queuePriority = NSOperationQueuePriorityLow;
    }
    
    //若是設置的執行順序是xLIFI,即後進先出,則要把queue中的最後一個加入的operation的依賴設置爲該operation,這樣來保證這個operation最早執行
    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; } 複製代碼

這個方法就是建立了一個request對象,而後使用這個request對象和session對象去建立下載的operation對象。

咱們看一下負責單個下載任務的operation對象究竟是怎麼建立的,即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;
}
複製代碼

這個初始化方法其實也很簡單,就是給本身的成員變量賦值

咱們知道,NSOperation類的真正執行任務是在其start方法裏面,那麼咱們看一下SDWebImageDownloaderOperationstart方法的具體實現: 代碼比較長,我在關鍵部分加了註釋

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

#if SD_UIKIT
        //這一部分就是解決在後臺仍然進行下載的問題
        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;
        //建立一個session對象,由於後面要建立NSURLSessionTask,須要session對象
        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; } } //建立dataTask,這個纔是真正執行下載任務的 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 } 複製代碼

這裏就是經過一個session對象和一個request對象建立了一個dataTask對象,這個dataTask對象纔是真正用來下載的,而後調用[self.dataTask resume]執行下載。

到這裏SDWebImage的源碼分析就結束啦。

參考: SDWebImage實現分析

這篇文章在簡書的地址:SDWebImage源碼解讀

相關文章
相關標籤/搜索