SDWebImage原始碼解讀

雪山飛狐1發表於2019-01-18

SDWebImage是一個開源的第三方庫,它提供了UIImageView的分類來實現從網路端下載資料並快取到記憶體和磁碟。

SDWebImage有如下特點:

  • 提供了UIImageView和UIButton的分類。以支援載入網路圖片並快取。
  • 一個非同步的圖片下載器
  • 提供非同步的記憶體和磁碟快取,並自動處理快取過期。
  • 後臺圖片解壓縮處理。
  • 確保同一個URL不會被下載多次。
  • 確保主執行緒永遠不會阻塞。

一.儲備知識

SDWebImage中每一個下載任務都是一個SDWebImageDownloaderOperation,而SDWebImageDownloaderOperation又是繼承自NSOperation,所以每一個下載任務對應一個NSOperation。在SDWebImage中使用SDWebImageDownloader來管理 多個下載任務,在SDWebImageDownloader中有一個downloadedQueue這個屬性,這個屬性是NSOperationQueue型別的,也就是用NSOperationQueue來管理NSOperation。 下面我們就一起學習一下NSOperationNSOperationQueue

NSOperation和NSOperationQueue

NSOperation是一個抽象類,用來表示與單個任務相關聯的程式碼和資料。

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

我們可以把一個NSOperation物件加入到一個operation queue中,讓這個operation queue去決定什麼時候執行這個operation。**當使用operation queue去管理operation時,輪到某個operation執行時實際是去執行這個operation的start方法,所以我們一個operation物件實際要執行的任務應該放在start方法裡面。**如果我們不想使用operation queue,也可以通過手動呼叫NSOperation的start方法來執行任務。

我們可以通過新增依賴來確定operation queue中operation的具體執行順序。新增依賴和移除依賴可以使用下列的API:

//新增依賴
- (void)addDependency:(NSOperation *)op;
//移除依賴
- (void)removeDependency:(NSOperation *)op;
複製程式碼

只要當一個operation物件的所有依賴都執行完成的時候,其才可以變成熟ready狀態,然後才可以被執行。如果一個operation沒有新增依賴,直接加入了operation queue中,那麼就會按照加入佇列的先後順序,當這個operation的前一個operation執行完成以後,其狀態才會變成ready,才能被執行。

NSOperation物件有下列四個比較重要的狀態:

  • 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原始碼解讀

相關文章