通俗易懂的SDWebImage原始碼解析(二)

weixin_34007291發表於2017-04-14

這是SDWebImage原始碼解析的第二篇文章,在這篇文章中我們將接著上一篇文章,從下面的方法開始講起:

/**
 * 如果不存在於快取中,請下載給定URL的影象,否則返回快取的版本。
 * url 影象的url
 * options 指定用於此請求的選項的掩碼
 * progressBlock 當影象下載中時呼叫的block,這個程式的回撥是在一個後臺佇列執行
 * completedBlock 當操作完成的回撥,這個引數是必須的
 *   completedBlock這個回撥無返回值。
     第一個引數為請求到的影象。
     第二個引數為NSData。  
     第三個引數包含一個NSError物件,為了防止影象的引數為空導致的錯誤。 
     第四個引數是一個SDImageCacheType列舉值,影象是從本地快取還是記憶體快取或者是從網路上獲得
     第五個引數,當使用SDWebImageProgressiveDownload,且影象正在下載的時候,這個值是NO。當呈現部分影象的時候會多次呼叫這個回撥,當影象完全下載完畢以後,這個回撥會最後一次呼叫,返回全部的影象,這個引數最後被設定為YES
     最後一個引數是原始的影象url
 *   返回一個遵循SDWebImageOperation的物件,應該是一個SDWebImageDownloaderOperation物件的例項
 *   這個NSObject類是遵循一個協議,這個協議叫做SDWebImageOperation,這個協議很簡單,就是一個cancel掉operation的協議.
 */
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                             progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                            completed:(nullable SDInternalCompletionBlock)completedBlock;

可以看到這個方法是非常長的,但是不要怕,先從翻譯的註釋看起來,通過方法註釋大概可以看出,這個下載方法有四個引數,返回一個遵循SDWebImageOperation協議的id型別的物件。
這種寫法id <SDWebImageOperation>在我們寫程式碼的過程中也可以參考來使用。

現在我們一行一行的來看這個方法的實現,下面的程式碼主要做了一下容錯。

// 如果沒有設定completedBlock來呼叫這個方法是沒有意義的
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");

// 有時候xcode不會警告這個型別錯誤(將NSSTring當做NSURL),所以這裡做一下容錯
if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}

// 防止app由於型別錯誤的奔潰,比如傳入NSNull代替NSURL
if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}



下面建立了一個繼承自NSObjectSDWebImageCombinedOperation物件,至於__block__weak相信大家都瞭解,我就不多解釋了。

__block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
__weak SDWebImageCombinedOperation *weakOperation = operation;

檢視SDWebImageCombinedOperation的具體程式碼可以看出,這是一個很簡單的物件,擁有三個屬性,實現了SDWebImageOperation的協議的對應方法,其實就是這裡的取消方法。

@interface SDWebImageCombinedOperation : NSObject <SDWebImageOperation>
@property (assign, nonatomic, getter = isCancelled) BOOL cancelled;
@property (copy, nonatomic, nullable) SDWebImageNoParamsBlock cancelBlock;
@property (strong, nonatomic, nullable) NSOperation *cacheOperation;
@end

@implementation SDWebImageCombinedOperation

- (void)setCancelBlock:(nullable SDWebImageNoParamsBlock)cancelBlock {
    // 如果該operation已經取消了,我們只是呼叫回撥block
    if (self.isCancelled) {
        if (cancelBlock) {
            cancelBlock();
        }
        _cancelBlock = nil; 
        // 不要忘了設定cacelBlock為nil,否則可能會奔潰
    } else {
        _cancelBlock = [cancelBlock copy];
    }
}

// SDWebImageCombinedOperation遵循SDWebImageOperation協議
- (void)cancel {
    self.cancelled = YES;
    if (self.cacheOperation) {
        [self.cacheOperation cancel];
        self.cacheOperation = nil;
    }
    if (self.cancelBlock) {
        self.cancelBlock();
        // TODO: this is a temporary fix to #809.
        // Until we can figure the exact cause of the crash, going with the ivar instead of the setter
//        self.cancelBlock = nil;
        _cancelBlock = nil;
    }
}
@end



接下來主要是對url的一個操作,如果url的長度為0,或者url在失敗的url列表中且下載的策略不為SDWebImageRetryFailed那麼就丟擲錯誤,並return
當然如果不存在上面的這些錯誤,就將對應的SDWebImageCombinedOperation物件加入到SDWebImageManagerrunningOperations陣列中。

// 建立一個互斥鎖防止現在有別的執行緒修改failedURLs.
// 判斷這個url是否是fail過的.如果url failed過的那麼isFailedUrl就是true
BOOL isFailedUrl = NO;
if (url) {
    @synchronized (self.failedURLs) {
        isFailedUrl = [self.failedURLs containsObject:url];
    }
}

// 1.如果url的長度為0時候執行
// 2.當前url在失敗的URL列表中,且options 不為 SDWebImageRetryFailed   時候執行
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;
}

// 建立一個互斥鎖防止現在有別的執行緒修改runningOperations.
@synchronized (self.runningOperations) {
    [self.runningOperations addObject:operation];
}



做完上述的操作以後,來獲取到url對應的cacheKey,也就是說快取對應的key(如果有快取的話)

// 通過url來獲取到對應的cacheKey
NSString *key = [self cacheKeyForURL:url];

下面是具體實現的程式碼,對應的作用已經通過註釋寫在裡面了

// 利用Image的URL生成一個快取時需要的key.
// 這裡有兩種情況,第一種是如果檢測到cacheKeyFilter不為空時,利用cacheKeyFilter來處理URL生成一個key.
// 如果為空,那麼直接返回URL的string內容,當做key.
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
    if (!url) {
        return @"";
    }
    // 如果設定了快取key的過濾器,過濾一下url
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    } else {
        // 否則直接使用url
        return url.absoluteString;
    }
}



下面的方法是個非常長的方法,有100行的程式碼,其中絕大多數的程式碼都是在完成的block中,在這裡先不看block中的程式碼,先概覽一下這個方法。

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock 

在上面建立的SDWebImageCombinedOperation物件有cacheOperation屬性,這個屬性是一個NSOperation型別的,通過SDWebImageManagerSDImageCache例項呼叫上面的方法來返回所需要的這個NSOperation例項。

通過上面的方法名可以大概猜出來這個是來搜尋快取的程式碼,現在來看一下這個方法的實現。

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
   // 如果key為nil,說明url不對,因此不執行後面的操作了,直接返回Operaion為nil。
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

    // First check the in-memory cache...
   // 檢查記憶體中key對應的快取,返回影象
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        NSData *diskData = nil;
        if ([image isGIF]) {
            diskData = [self diskImageDataBySearchingAllPathsForKey:key];
        }
        // 現在已經找到記憶體對應的影象快取了,直接返回
        if (doneBlock) {
            doneBlock(image, diskData, SDImageCacheTypeMemory);
        }
        return nil;
    }

    // 如果記憶體中沒有,現在檢查磁碟的快取
    NSOperation *operation = [NSOperation new];
    // 新開一個序列佇列,在裡面執行下面的程式碼
    // 在這個檔案全域性搜尋,發現_ioQueue = dispatch_queue_create("com.hackemist.SDWebImageCache", DISPATCH_QUEUE_SERIAL); 
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            // do not call the completion if cancelled
            return;
        }

        @autoreleasepool {
            // 搜尋磁碟快取,將磁碟快取加入記憶體快取
            NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
            UIImage *diskImage = [self diskImageForKey:key];
            // 如果可以獲取到磁碟影象,且快取影象到記憶體快取為yes(預設為yes)
            if (diskImage && self.config.shouldCacheImagesInMemory) {
                // 計算出需要花費的記憶體代銷,將該影象快取到記憶體中
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }
            // 在主執行緒執行對應的回撥,這裡的快取型別是磁碟快取
            if (doneBlock) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    doneBlock(diskImage, diskData, SDImageCacheTypeDisk);
                });
            }
        }
    });
    return operation;
}

通過?上面的這個方法,如果能從記憶體或者磁碟中得到該影象,那麼我們就把對應的image,data,SDImageCacheType傳遞出去(磁碟快取對應的CacheTypeSDImageCacheTypeDisk,記憶體快取對應的CacheTypeSDImageCacheTypeMemory),並且返回新創將的NSOperation物件。如果沒有獲得影象,那麼就不給對應的doneBlock傳參,只返回新建立的NSOperation物件。

現在已經知道上面的方法有什麼作用,接下來看看那個在查詢完快取block中100行的程式碼。

在這裡需要強烈注意的一點是:這裡除了operation.cacheOperationNSOperation型別的物件以外,別的operation的都是繼承自NSObject型別的物件,遵循<SDWebImageOperation>協議。

現在我們只需要知道這個block裡的程式碼是在查詢完快取以後呼叫,無論有沒有圖片的快取相關資訊。

首先如果當前的這個operation進行了取消操作,在SDWebImageManager的runningOperations移除operation。

// 如果對當前operation進行了取消標記,在SDWebImageManager的runningOperations移除operation
if (operation.isCancelled) {
  [self safelyRemoveOperationFromRunning:operation];
  return;
}

現在我們的SDWebImageManager有一個陣列,專門用來存放這些對圖片的操作的operation物件,再多嘴一句,這個operation不是繼承自NSOperation

@property (strong, nonatomic, nonnull) NSMutableArray<SDWebImageCombinedOperation *> *runningOperations;

如果當前operation的取消標記為YES,在runningOperations中把當前這個operation移除了。

- (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
    @synchronized (self.runningOperations) {
        if (operation) {
            [self.runningOperations removeObject:operation];
        }
    }
}

下面是一個很長的if else的判斷,在這裡先從簡單的else開始看起

else {
            // Image not in cache and download disallowed by delegate
            // 不在快取中,不被代理允許下載
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            // completedBlock中image和error均傳入nil。
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }

通過上面的程式碼的傳參image:nil data:nil error:nil cacheType:SDImageCacheTypeNone可以知道,這裡肯定是沒有獲取到圖片和相關資料,所以都傳入的是nil
下面的程式碼就是在主執行緒直接呼叫完成的回撥。

- (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
                             completion:(nullable SDInternalCompletionBlock)completionBlock
                                  image:(nullable UIImage *)image
                                   data:(nullable NSData *)data
                                  error:(nullable NSError *)error
                              cacheType:(SDImageCacheType)cacheType
                               finished:(BOOL)finished
                                    url:(nullable NSURL *)url {
    dispatch_main_async_safe(^{
        if (operation && !operation.isCancelled && completionBlock) {
            completionBlock(image, data, error, cacheType, finished, url);
        }
    });
}



解釋完上面的函式,來看看這個判斷,這裡其實很簡單,如果快取圖片存在,就把圖片及對應的資料傳給回撥的方法,看清楚了,這裡傳遞的引數可不是為nil
但是需要注意的是因為這裡是從後往前看程式碼,所以並不是說有快取圖片就一定會走這個方法,肯定是先走if再走else if

else if (cachedImage) {
    // 如果有快取,返回快取
    __strong __typeof(weakOperation) strongOperation = weakOperation;
    // 直接執行completedBlock,其中error置為nil即可。
    [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
    [self safelyRemoveOperationFromRunning:operation];
} 

最後來到這個比較長的if判斷

if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) 

可以進入這個判斷裡的幾種組合在這裡我就列出來了

  1. cachedImagenilSDWebImageManager的代理物件沒有實現imageManager:shouldDownloadImageForURL:
    解釋:如果現在下載的圖片沒有快取,且沒有實現代理方法(這個代理方法是我們自己來實現的)。
  2. cachedImagenilSDWebImageManager的代理物件的代理方法返回YES(當影象沒有在記憶體中找到的時候,控制是否下載影象)。(無快取,但是實現了代理方法且返回YES)
    解釋:如果現在下載的圖片沒有快取,我們實現了代理方法,但是代理方法返回的是YES
  3. optionsSDWebImageRefreshCachedSDWebImageManager的代理物件沒有實現imageManager:shouldDownloadImageForURL:
    解釋:如果下載的方法的options沒有被手動設定為SDWebImageRefreshCached,且沒有實現代理方法(這個代理方法是我們自己來實現的)。
  4. optionsSDWebImageRefreshCachedSDWebImageManager的代理物件的代理方法返回YES(當影象沒有在記憶體中找到的時候,控制是否下載影象)。(無快取,但是實現了代理方法且返回YES)
    解釋:如果下載的方法的options沒有被手動設定為SDWebImageRefreshCached,我們實現了代理方法,但是代理方法返回的是YES

上面的這些情況會進入對應的if程式碼塊

下面的程式碼就是如果可以找到快取,且optionsSDWebImageRefreshCached,那麼就先把快取的影象資料傳遞出去

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:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}



首先downloaderOptions初始值為0,如果這裡的options設定了哪些,就給downloaderOptions加上哪些,其實下面的程式碼就是為什麼會有13種的SDWebImageOptions的原因,可以幫著做一些高定製化的工作。
關於這裡的&和|符號的詳解,可以看我的上一篇文章。

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;



這裡和上面解釋過的是同一種,這裡的作用就是如果可以找到快取,且optionsSDWebImageRefreshCached,那麼就不讓downloaderOptions包含SDWebImageDownloaderProgressiveDownload(漸進式下載),並且讓downloaderOptions裡必須包含SDWebImageDownloaderIgnoreCachedResponse(忽略快取)。

if (cachedImage && options & SDWebImageRefreshCached) {
    // force progressive off if image already cached but forced refreshing
    downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
    // ignore image read from NSURLCache if image if cached but force refreshing
    downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}

接下來又出現了一個新的類SDWebImageDownloadToken,參看它的原始檔可以發現,這恐怕是迄今為止見到的最簡單的類了吧。

// 一個與每個下載關聯的Token,可以用來取消下載
@interface SDWebImageDownloadToken : NSObject
@property (nonatomic, strong, nullable) NSURL *url;
@property (nonatomic, strong, nullable) id downloadOperationCancelToken;
@end

@implementation SDWebImageDownloadToken
@end



在上面的程式碼中已經講過找對應的快取來給UIImageView設定圖片了。
但是還沒有講如果不存在快取的話,下載影象的操作和過程,這一部分的內容留在下一篇講。
下面的程式碼主要是圖片獲取成功以後的操作。主要的講解寫在程式碼的註釋中了。

SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
    // block中的__strong 關鍵字--->防止物件提前釋放
    __strong __typeof(weakOperation) strongOperation = weakOperation;
    // 如果operation(SDWebImageCombinedOperation型別)為空,或者operation取消了
    if (!strongOperation || strongOperation.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) {
        // 如果發生了錯誤,就把錯誤傳入對應的回撥來處理error
        [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
        // 檢查錯誤型別,確認不是客戶端或者伺服器端的網路問題,就認為這個url本身問題了。並把這個url放到failedURLs中
        if (   error.code != NSURLErrorNotConnectedToInternet
            && error.code != NSURLErrorCancelled
            && error.code != NSURLErrorTimedOut
            && error.code != NSURLErrorInternationalRoamingOff
            && error.code != NSURLErrorDataNotAllowed
            && error.code != NSURLErrorCannotFindHost
            && error.code != NSURLErrorCannotConnectToHost
            && error.code != NSURLErrorNetworkConnectionLost) {
            @synchronized (self.failedURLs) {
                [self.failedURLs addObject:url];
            }
        }
    }
    else {
        // 如果使用了SDWebImageRetryFailed選項,那麼即使該url是failedURLs,也要從failedURLs移除,並繼續執行download
        if ((options & SDWebImageRetryFailed)) {
            @synchronized (self.failedURLs) {
                [self.failedURLs removeObject:url];
            }
        }
        // 如果不設定options裡包含SDWebImageCacheMemoryOnly,那麼cacheOnDisk為YES,表示會把圖片快取到磁碟
        BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);
       // 如果options包含SDWebImageRefreshCached,cachedImage有值,但是下載影象downloadedImage為nil,不呼叫完成的回撥completion block
       // 這裡的意思就是雖然現在有快取圖片,但是要強制重新整理圖片,但是沒有下載到圖片,那麼現在就什麼都不做,還是使用原來的快取圖片 
        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:)]) {
            /*
             1.圖片下載成功了,不是gif影象,且代理實現了imageManager:transformDownloadedImage:withURL:
             2.圖片下載成功了,options中包含SDWebImageTransformAnimatedImage,且代理實現了imageManager:transformDownloadedImage:withURL:
              這裡做的主要操作是在一個新開的非同步佇列中對圖片做一個轉換的操作,例如需要改變原始圖片的灰度值等情況
             */
           dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
                // 如果獲得了新的transformedImage,不管transform後是否改變了圖片.都要儲存到快取中
                UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];
                if (transformedImage && finished) {
                    BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
                    // pass nil if the image was transformed, so we can recalculate the data from the image
                    // 如果影象被轉換,則給imageData傳入nil,因此我們可以從影象重新計算資料
                    [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                }
                // 將對應轉換後的圖片通過block傳出去
                [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
            });
        } else {
            // 下載好了圖片且完成了,存到記憶體和磁碟,將對應的圖片通過block傳出去
            if (downloadedImage && finished) {
                [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
            }
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
        }
    }
    // 下載結束後移除對應operation
    if (finished) {
        [self safelyRemoveOperationFromRunning:strongOperation];
    }
}];

最後在來講一些關於SDWebImageManager的相關知識:

  1. SDWebImageManager是一個單例物件(這不是廢話?)
  2. 它同時擁有兩個子單例物件,一個為SDImageCache,另一個為SDWebImageDownloader,一個負責快取相關的操作,一個負責下載相關的操作。
  3. SDWebImageManager還擁有failedURLs用來存放下載失敗的URL地址的陣列。
  4. SDWebImageManager還擁有runningOperations用來存放SDWebImageCombinedOperation : NSObject物件。
  5. SDWebImageCombinedOperation的作用就是關聯快取和下載的物件,每當有新的圖片地址需要下載的時候,就會產生一個新的SDWebImageCombinedOperation例項。

這篇文章講的東西還是比較雜,我還是以一張圖來總結圖來結尾吧!

435391-510dc526d122d581.png
SDWebImage2-3.png

相關文章