SDWebImage源碼分析 2

學(xué)習(xí)源碼時可能太過枯燥,一個不錯背景音樂能讓心情平靜提升專注力(??????)??
推薦歌單:http://music.163.com/#/m/playlist?id=6683129

接著上回,我們還留著downloadImageWithURL沒說,現(xiàn)在就到SDWebImageManager.h里來看看:

/**
 * 如果圖片的url不在緩存中則下載,否則使用緩存中的圖片.
 *
 * @param url            圖片的url
 * @param options        選項
 * @param progressBlock  當(dāng)圖片正在下載中調(diào)用該block
 * @param completedBlock 當(dāng)任務(wù)完成后調(diào)用該block.
 *
 *   completedBlock必須提供,不允許為nil.
 *   typedef void(^SDWebImageCompletionWithFinishedBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL);
 *   這個block沒有返回值 并且 將請求的UIImage作為第一個參數(shù).
 *   如果發(fā)生錯誤,image參數(shù)將會為nil,第二個參數(shù)會包含NSError
 *
 *   第三個參數(shù)是`SDImageCacheType`枚舉用于表示圖片是從本地緩存(磁盤)還是內(nèi)存或是網(wǎng)絡(luò)中獲取的
 *   
 *   當(dāng)開啟了SDWebImageProgressiveDownload選項,并且正在下載圖片時,finished會被設(shè)置成NO。當(dāng)圖片被完整的下載好后會執(zhí)行block傳入完整的圖片以及將之設(shè)置為YES
 *
 * @return 返回一個遵循SDWebImageOperation協(xié)議的NSObject. 應(yīng)該是一個SDWebImageDownloaderOperation的實(shí)例
 *
 */
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;

從描述中可以了解到該方法會幫我們下載圖片以及從緩存中取出以前下載過的圖片。

同時我們注意到返回參數(shù)是id <SDWebImageOperation>,跟到SDWebImageOperation.h中看到里面僅僅只有一個方法:

@protocol SDWebImageOperation <NSObject>

- (void)cancel;

@end

這個是面向協(xié)議的做法,想使用該接口的話要遵循接口中提供的方法來用。同時繼承了該接口的子類需要對接口的方法實(shí)現(xiàn)。

SDWebImageManager.m中,我們將downloadImageWithURL 代碼拆成幾個片段來分析:

// completedBlock 必須要有
NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
//有個常見的錯誤,那就是將NSString傳進(jìn)url中,然而因為奇怪的原因xcode又不會發(fā)出警告。所以這里遇到這種情況會做一次轉(zhuǎn)換
if ([url isKindOfClass:NSString.class]) {
    url = [NSURL URLWithString:(NSString *)url];
}

// 防止傳入錯誤的非NSURL類型造成閃退,這里處理了一下
if (![url isKindOfClass:NSURL.class]) {
    url = nil;
}

這里主要是針對常見的錯誤進(jìn)行處理,曾經(jīng)我也將NSString誤傳進(jìn)行調(diào)用,Xcode還沒報錯代碼也能用,直到我翻開代碼一看原來SDWebImage幫我們做了一層轉(zhuǎn)換啊。

繼續(xù)往下看發(fā)現(xiàn)代碼中使用到了__block__weak修飾符:

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

__block的作用是在block內(nèi)想要修改外部一個變量就需要在外頭這個變量前加上該修飾符
__weak的作用是避免在block內(nèi)出現(xiàn)循環(huán)引用

BOOL isFailedUrl = NO;
@synchronized (self.failedURLs) {
    isFailedUrl = [self.failedURLs containsObject:url];
}

//如果url長度為0或者是未設(shè)置SDWebImageRetryFailed以及加載失敗的url(進(jìn)了黑名單) 直接返回錯誤
if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {
    dispatch_main_sync_safe(^{
        NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
        completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
    });
    return operation;
}

這段代碼大意是:如果未設(shè)置SDWebImageRetryFailed進(jìn)行下載失敗重試的話,遇到進(jìn)了黑名單的url就直接返回錯誤,因為不需要重試了嘛。

@synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation]; //<往runningOperations加入當(dāng)前的operation
}

這段代碼中使用到了@synchronized用來保證線程安全,即僅只允許一個線程對runningOperations進(jìn)行操作,其他線程阻塞。

NSString *key = [self cacheKeyForURL:url]; //將url換算成緩存的key

從名稱上可以直接猜到[self cacheKeyForURL:url]方法的用意是為url創(chuàng)建一個緩存的key,跟進(jìn)查看我們的猜測對不對:

- (NSString *)cacheKeyForURL:(NSURL *)url {
    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    }
    else {
        return [url absoluteString];
    }
}

這個方法簡單明了將url轉(zhuǎn)換成key,在用戶未設(shè)置自定義規(guī)則(self.cacheKeyFilter)的時候直接返回url的absoluteString。

接下來輪到queryDiskCacheForKey這個方法了:

// 從緩存中查詢image,先從內(nèi)存找,找不到再到磁盤中找
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {
      ...

我們跟到SDImageCache.m里查看:

- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
    if (!doneBlock) { //< 沒有doneBlock直接返回nil
        return nil;
    }

    if (!key) { //< 沒有key則調(diào)用doneBlock
        doneBlock(nil, SDImageCacheTypeNone);
        return nil;
    }

    // 先檢查內(nèi)存中是否有緩存
    UIImage *image = [self imageFromMemoryCacheForKey:key];
    if (image) {
        doneBlock(image, SDImageCacheTypeMemory);
        return nil;
    }

    NSOperation *operation = [NSOperation new];
    dispatch_async(self.ioQueue, ^{
        if (operation.isCancelled) {
            return;
        }

        @autoreleasepool {
            //從磁盤中查找是否命中緩存
            UIImage *diskImage = [self diskImageForKey:key];
            if (diskImage && self.shouldCacheImagesInMemory) { //< 找到圖片以及允許將圖片緩存在內(nèi)存中
                NSUInteger cost = SDCacheCostForImage(diskImage);
                [self.memCache setObject:diskImage forKey:key cost:cost];
            }

            dispatch_async(dispatch_get_main_queue(), ^{
                doneBlock(diskImage, SDImageCacheTypeDisk);
            });
        }
    });

    return operation;
}

大致可以從代碼中看出些端倪,先使用key從內(nèi)存中查找圖片,找不到的話再去磁盤中查找,然后放到內(nèi)存中并返回圖片以及SDImageCacheType:

typedef NS_ENUM(NSInteger, SDImageCacheType) {
    /**
     * 該圖片還未存入SDWebImage caches(SDWebImage 緩存)中,來自web上下載的。
     * The image wasn't available the SDWebImage caches, but was downloaded from the web.
     */
    SDImageCacheTypeNone,
    /**
     * 該圖片是從磁盤中獲取的
     */
    SDImageCacheTypeDisk,
    /**
     * 該圖片是從緩存中獲取的
     */
    SDImageCacheTypeMemory
};

目前我們只需要了解該方法的作用是用來從緩存中取出圖片用的就夠了,具體的分析等到后面的章節(jié)再說。

我們的重點(diǎn)是接下來的代碼,按照慣例將它們拆開來分析:

if (operation.isCancelled) { //< 當(dāng)前的任務(wù)被取消
  @synchronized (self.runningOperations) {
      [self.runningOperations removeObject:operation]; //< 從runningOperations中刪除掉
  }
  return;
}

這段代碼意圖明顯,運(yùn)行至此發(fā)現(xiàn)當(dāng)前的operation已經(jīng)被取消,那也不用繼續(xù)執(zhí)行了,將operation從運(yùn)行任務(wù)列表中刪除即可。

接下來進(jìn)入分支條件,一條是需要從網(wǎng)絡(luò)下載圖片,一條是直接使用緩存,最后一條是無法找到圖片且又設(shè)置了不允許下載。

if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) { //< 如果緩存中找不到圖片或者開啟了SDWebImageRefreshCached(2選1) 并且 沒實(shí)現(xiàn)shouldDownloadImageForURL

我們從第一條分支看起:

if (image && options & SDWebImageRefreshCached) {
    dispatch_main_sync_safe(^{
        //如果圖片在緩存中被找到,但啟用了SDWebImageRefreshCached就需要重新從服務(wù)器上下載圖片使NSURLCache刷新緩存
        completedBlock(image, nil, cacheType, YES, url);
    });
}
// download if no image or requested to refresh anyway, and download allowed by delegate
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 (image && options & SDWebImageRefreshCached) { //< 開啟了SDWebImageRefreshCached
    //強(qiáng)制關(guān)閉ProgressiveDownload
    downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
    //忽略從NSURLCache緩存中讀取
    downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}
// 將下載的事情交給SDWebImageDownloader來搞定,SDWebImageManager負(fù)責(zé)管理和調(diào)度,實(shí)現(xiàn)職責(zé)單一
            id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {

這里主要完成了SDWebImageManager的options到SDWebImageDownloader的options轉(zhuǎn)換,將下載的事情交給SDWebImageDownloader來搞定。

接下來就是對異常的處理:

__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) { //< strongOperation被釋放或者已經(jīng)被取消了
    // 什么都不做
    // 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) { //< 有錯誤
  dispatch_main_sync_safe(^{
      if (strongOperation && !strongOperation.isCancelled) {
          completedBlock(nil, error, SDImageCacheTypeNone, finished, url);
      }
  });

  //如果不是以下這些錯誤,將url加入黑名單
  if (   error.code != NSURLErrorNotConnectedToInternet
      && error.code != NSURLErrorCancelled
      && error.code != NSURLErrorTimedOut
      && error.code != NSURLErrorInternationalRoamingOff
      && error.code != NSURLErrorDataNotAllowed
      && error.code != NSURLErrorCannotFindHost
      && error.code != NSURLErrorCannotConnectToHost) {
      @synchronized (self.failedURLs) {
          [self.failedURLs addObject:url];
      }
  }
}

這里將url加入黑名單,如未設(shè)置下載重試的話,下次請求該圖片地址將直接略過

最后是當(dāng)strongOperation存在且沒有錯誤的情況:

//如果開啟了SDWebImageRetryFailed將url從failedURLs中刪除
if ((options & SDWebImageRetryFailed)) {
    @synchronized (self.failedURLs) {
        [self.failedURLs removeObject:url];
    }
}
//如果開啟SDWebImageCacheMemoryOnly,則cacheOnDisk為NO,即不緩存在磁盤上
BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

if (options & SDWebImageRefreshCached && image && !downloadedImage) {
    //圖片命中NSURLCache的緩存,則不調(diào)用completion block
}
// 如果圖片(downloadedImage)下載成功并且想transform圖片的情況
// 默認(rèn)情況下,animated image是不允許transform的,得先使用downloadedImage.images判斷這個圖片是不是animated image,為nil即為靜態(tài)圖
// 但如果打開了SDWebImageTransformAnimatedImage,允許強(qiáng)制transform
// 要transform還需實(shí)現(xiàn)transformDownloadedImage這個delegate
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), ^{
        UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

        if (transformedImage && finished) {
            BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage]; //< 對比圖片是否被轉(zhuǎn)換過
            [self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
        }

        dispatch_main_sync_safe(^{
            if (strongOperation && !strongOperation.isCancelled) {
                completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
            }
        });
    });
}
else {
    if (downloadedImage && finished) {
        [self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk]; //< 緩存圖片
    }

    dispatch_main_sync_safe(^{
        if (strongOperation && !strongOperation.isCancelled) {
            completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
        }
    });
}

這段代碼大意是圖片下載完成,用戶可以對圖片進(jìn)行處理,然后SDWebImage將它存入緩存中。

第二個分支是從緩存中找到了圖片,非常簡潔:

dispatch_main_sync_safe(^{
    __strong __typeof(weakOperation) strongOperation = weakOperation;
    if (strongOperation && !strongOperation.isCancelled) {
        completedBlock(image, nil, cacheType, YES, url);
    }
});
@synchronized (self.runningOperations) {
    [self.runningOperations removeObject:operation];
}

直接調(diào)用completedBlock返回,然后再從runningOperations刪除

第三條分支為圖片不在緩存中并且在delegate中又不允許下載,所以image為nil,error也為nil:

dispatch_main_sync_safe(^{
    __strong __typeof(weakOperation) strongOperation = weakOperation;
    if (strongOperation && !weakOperation.isCancelled) {
        completedBlock(nil, nil, SDImageCacheTypeNone, YES, url);
    }
});
@synchronized (self.runningOperations) {
    [self.runningOperations removeObject:operation];
}

到此為止SDWebImageManager中關(guān)鍵的downloadImageWithURL代碼的主流程我們已經(jīng)大致理解了。

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

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

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