天天看點

SDWebImage 源碼閱讀(二)

我們在上一篇文章介紹了 SDWebImage 架構的 UIImageView+WebCache 擴充類,主要功能就是設計了嚴密的判斷,保證運用該架構的其他開發者能夠在各種環境下擷取圖檔。這篇文章主要是介紹 SDWebImageManager 中從緩存擷取圖檔或者從網絡下載下傳圖檔。

第一部分:擷取圖檔對應的 key

- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
    // Invoking this method without a completedBlock is pointless
    // 調用這個方法必須實作 completedblock
    NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");


    // 下面兩段代碼都是為了保護 url 邏輯性


    // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, XCode won't
    // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString.
    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;
    }


    // 建立一個 operation 
    __block SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new];
    __weak SDWebImageCombinedOperation *weakOperation = operation;

    // 判斷目前的url,是否包含在下載下傳失敗的url集合中,在通路failedURLs集合時要加鎖,防止其他線程對其進行操作
    BOOL isFailedUrl = NO;

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

    // 如果url為空 || url存在于下載下傳失敗url集合中并且不是下載下傳失敗後重新下載下傳,那麼抛出錯誤
    if (url.absoluteString.length ==  || (!(options & SDWebImageRetryFailed) && isFailedUrl)) {

        dispatch_main_sync_safe(^{
            NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorFileDoesNotExist userInfo:nil];
            completedBlock(nil, error, SDImageCacheTypeNone, YES, url);
        });
        return operation;
    }

    // 将 operation 加到運作中的url集合中
    @synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
    // 從緩存中根據url取出key(其實key就是url的string)
    NSString *key = [self cacheKeyForURL:url];

    // 根據key去查找對應的圖檔 ???
    operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {  
    ...  // 詳情見 第二部分
}];
    return operation;
}
           

第二部分:根據 image 判斷是否需要從網絡擷取

operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {  
    ...          
}];
           

上面的代碼調用了 SDImageCache 中的 queryDiskCacheForKey: done: 方法,我們後續章節再研究這個類中的東西,我們先看看對這個方法中的 block 的處理,也就是結果的處理方法:

// 判斷該任務是否已經cancel了
if (operation.isCancelled) {
    @synchronized (self.runningOperations) {
        [self.runningOperations removeObject:operation];
    }

    return;
}


// 先看 && 的後半段
// 注意後面的 A || B,如果 A 為真,那就不用判斷 B 了
// 也就是說,如果 imageManager:shouldDownloadImageForURL: 方法沒有實作,直接傳回 YES
// 目前我在源碼中并沒有看到函數的實作,是以就當 if 的後半段恒為 YES
// 我們主要看 && 前面的 || 表達式
// 如果緩存中沒找到 image,或者需要重新整理記憶體,就執行 if 語句
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
    ...  // 詳情見第三部分
}

// 從緩存中找到 image
else if (image) {
    dispatch_main_sync_safe(^{
        __strong __typeof(weakOperation) strongOperation = weakOperation;
        if (strongOperation && !strongOperation.isCancelled) {
            completedBlock(image, nil, cacheType, YES, url);
        }
    });

    // 将該 operation 從數組中移除
    @synchronized (self.runningOperations) {
        [self.runningOperations removeObject:operation];
    }
}
else {
    // Image not in cache and download disallowed by delegate
    // 又沒有從緩存中擷取到圖檔,shouldDownloadImageForURL 又傳回 NO,不允許下載下傳,悲催!
    // 是以 image 和 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];
    }
}
           

第三部分:初始化下載下傳圖檔

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

上面判斷 if 中實作的内容就是 image 需要網絡擷取部分,詳情如下:

if (image && options & SDWebImageRefreshCached) {
   dispatch_main_sync_safe(^{
        // 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.
        // 如果圖檔在緩存中找到,但是options中有SDWebImageRefreshCached
        // 那麼就嘗試重新下載下傳該圖檔,這樣是NSURLCache有機會從伺服器端重新整理自身緩存
        completedBlock(image, nil, cacheType, YES, url);
    });
}

// download if no image or requested to refresh anyway, and download allowed by delegate

// 首先定義枚舉值 downloaderOptions,并根據 options 來設定 downloaderOptions
// 基本上 options 和 downloaderOptions 是一一對應的,隻需要注意最後一個選項 SDWebImageRefreshCached
SDWebImageDownloaderOptions downloaderOptions = ;
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) {
    // force progressive off if image already cached but forced refreshing

   // 相當于downloaderOptions = downloaderOption & ~SDWebImageDownloaderProgressiveDownload);
    // ~ 位運算,取反
    downloaderOptions &= ~SDWebImageDownloaderProgressiveDownload;
    // ignore image read from NSURLCache if image if cached but force refreshing

    // 相當于 downloaderOptions = (downloaderOptions | SDWebImageDownloaderIgnoreCachedResponse);
    // 因為SDWebImage有兩種緩存方式,一個是SDImageCache,一個就是NSURLCache
    // 是以已經從SDImageCache擷取了image,就忽略NSURLCache了。
    downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse;
}

// 下載下傳圖檔 (圖檔下載下傳部分将在後續章節解讀,這裡先不介紹)
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
    ...  // 詳情見 第四部分       
}];
operation.cancelBlock = ^{
    [subOperation cancel];

    @synchronized (self.runningOperations) {
        __strong __typeof(weakOperation) strongOperation = weakOperation;
        if (strongOperation) {
            [self.runningOperations removeObject:strongOperation];
        }
    }
};
           

第四部分:下載下傳圖檔完成後 block 中的回調

圖檔下載下傳部分将在後續章節講解,這裡先介紹一下圖檔下載下傳完成後 block 中的内容:

__strong __typeof(weakOperation) strongOperation = weakOperation;
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) {
    dispatch_main_sync_safe(^{
        if (strongOperation && !strongOperation.isCancelled) {
            completedBlock(nil, error, SDImageCacheTypeNone, finished, 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];
        }
    }
}
else {

    // 如果是失敗重試,那麼将 url 從 failedURLs 中移除
    if ((options & SDWebImageRetryFailed)) {
        @synchronized (self.failedURLs) {
            [self.failedURLs removeObject:url];
        }
    }

    // 是否隻能從記憶體中取出
    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

    // 如果需要重新整理緩存,并且存在 image,并且沒有新下載下傳的 image
    if (options & SDWebImageRefreshCached && image && !downloadedImage) {
        // Image refresh hit the NSURLCache cache, do not call the completion block
    }

    // 如果有新下載下傳的 image,并且需要對 image 進行處理,并且實作了圖檔處理的代理方法
    else if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, ), ^{

            // 擷取處理後的圖檔
            UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

            if (transformedImage && finished) {

                BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];

                // 将圖檔緩存到記憶體中(這裡後續講解)
                [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);
        }
    });
}
}

if (finished) {
    @synchronized (self.runningOperations) {
       if (strongOperation) {
            [self.runningOperations removeObject:strongOperation];
        }
    }
}
           

總結

SDWebImageManager 類中主要是對圖檔下載下傳和緩存方式的管理,以及對下載下傳完成的圖檔,url 等資訊的回調,承上啟下的作用,承接了 UIImageView+WebCache 類,下接了 SDImageCache 類和 SDWebImageDownloader 類。

SDWebImage 源碼解析 github 位址:https://github.com/Mayan29/SDWebImageAnalysis

遺留問題:

  • 根據 key 去緩存中查找對應的圖檔
    • SDImageCache 類
    • - (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock

  • 下載下傳圖檔
    • SDWebImageDownloader 類
    • - (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock

  • 将圖檔存入緩存
    • SDImageCache 類
    • - (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk