每日一問27——SDWebImage(調(diào)度)

前言

通過前兩篇關于SDWebImage的學習,我們已經(jīng)知道了它的下載策略和緩存策略。本次要學習的內(nèi)容是SDWebImage中是通過怎樣的調(diào)度方法來使用下載和緩存的。

方法的調(diào)用

在使用的時候,我們通常會直接調(diào)用類別中提供的方法來加載圖片,查看它們的實現(xiàn)我們可以發(fā)現(xiàn)最終我們會調(diào)用到這樣一個函數(shù)

- (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
                           context:(nullable NSDictionary *)context;

外部提供的接口都是對這個函數(shù)的一個封裝,方便我們使用時候調(diào)用。來看看具體的實現(xiàn):

里面首先對operationKey進行了檢驗,生成可用的key,并將這個key對應的operation添加到了一個SDOperationsDictionary類的字典中,并綁定到對應的view中。后續(xù)在管理任務都會用到這個字典。

NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
    [self sd_cancelImageLoadOperationWithKey:validOperationKey];
    objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

省略一些檢測代碼,當URL正確時,就會調(diào)用SDWebImageManager類的單利對象進行圖片下載。這個方法就會返回一個operation回來,通過[self sd_setImageLoadOperation:operation forKey:validOperationKey];將這個operation保存到之前說到的字典中。

id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager loadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
        /**
        省略若干代碼,這些代碼的作用是根據(jù)用戶的設定將下載好的數(shù)據(jù)返回給上層。
        *//
        [self sd_setImageLoadOperation:operation forKey:validOperationKey];
}

可以發(fā)現(xiàn),這個函數(shù)中并沒有下載和緩存相關的代碼,于是我們可以猜測下載和緩存相關的邏輯是放在SDWebImageManager類中進行處理的。下面我們就對調(diào)度類SDWebImageManager進行進一步的查看分析。

SDWebImageManager

查看SDWebImageManager.h文件,可以發(fā)現(xiàn)SDWebImageManager中提供了2個我們之前講到過的屬性,分別是SDWebImageDownloaderimageCache。這兩個實例中不就是提供下載和緩存的操作嗎?

@interface SDWebImageManager : NSObject

@property (weak, nonatomic, nullable) id <SDWebImageManagerDelegate> delegate;

@property (strong, nonatomic, readonly, nullable) SDImageCache *imageCache;
@property (strong, nonatomic, readonly, nullable) SDWebImageDownloader *imageDownloader;

進一步查看,還提供了一個遵循<SDWebImageManagerDelegate>協(xié)議的代理。<SDWebImageManagerDelegate>協(xié)議主要提供了2個接口:

//控制在cache中沒有找到image時 是否應該去下載。默認是YES。
- (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nullable NSURL *)imageURL;

//在下載之后,緩存之前轉換圖片。在全局隊列中操作,不阻塞主線程
- (nullable UIImage *)imageManager:(nonnull SDWebImageManager *)imageManager transformDownloadedImage:(nullable UIImage *)image withURL:(nullable NSURL *)imageURL;

此外,SDWebImageManager還提供了這些操作,包括圖片的下載,取消,緩存,檢測等。我們需要重點學習的方法是downloadImageWithURL下載這個函數(shù)。

- (instancetype)initWithCache:(SDImageCache *)cache downloader:(SDWebImageDownloader *)downloader;
//下載圖片
- (id )downloadImageWithURL:(NSURL *)url
                                         options:(SDWebImageOptions)options
                                        progress:(SDWebImageDownloaderProgressBlock)progressBlock
                                       completed:(SDWebImageCompletionWithFinishedBlock)completedBlock;
//緩存給定URL的圖片
- (void)saveImageToCache:(UIImage *)image forURL:(NSURL *)url;
//取消當前所有的操作
- (void)cancelAll;
//監(jiān)測當前是否有進行中的操作
- (BOOL)isRunning;
//監(jiān)測圖片是否在緩存中,監(jiān)測結束后調(diào)用completionBlock
- (void)cachedImageExistsForURL:(NSURL *)url
                     completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//監(jiān)測圖片是否緩存在disk里,監(jiān)測結束后調(diào)用completionBlock
- (void)diskImageExistsForURL:(NSURL *)url
                   completion:(SDWebImageCheckCacheCompletionBlock)completionBlock;
//返回給定URL的cache key,默認是圖片的url
- (NSString *)cacheKeyForURL:(NSURL *)url;

對于downloadImageWithURL下載函數(shù)來說,它需要傳入4個參數(shù),和返回SDWebImageOperation類的實例。

  • @param url 網(wǎng)絡圖片的 url 地址
  • @param options 一些定制化選項
  • @param progressBlock 下載時的 Block,其定義為:typedef void(^SDWebImageDownloaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize);
  • @param completedBlock 下載完成時的 Block,其定義為:typedef void(^SDWebImageDownloaderCompletedBlock)(UIImage *image, NSData *data, NSError *error, BOOL finished);
  • @return 返回 SDWebImageOperation 的實例

具體的流程是

1.驗證URL正確性

如果不正確,則回調(diào)error

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;
    }

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

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

    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;
    }
2.檢測該URL是否被下載過,去緩存中查找圖片

私有成員變量是一個NSMutableArray<SDWebImageCombinedOperation *>的數(shù)組,用來保存所有正在進行的operation,方便統(tǒng)一控制。

@synchronized (self.runningOperations) {
        [self.runningOperations addObject:operation];
    }
//根據(jù)url生成緩存對應的key
    NSString *key = [self cacheKeyForURL:url];

//將查找結果保存到operation中的chcheOperation中。
    operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
        if (operation.isCancelled) {
            [self safelyRemoveOperationFromRunning:operation];
            return;
        }
3.根據(jù)不同的情況進行不同的操作
3.1>如果在緩存中沒有找到圖片,或者采用的 SDWebImageRefreshCached 選項,則從網(wǎng)絡下載

這個操作會先調(diào)用imageDownloader嘗試下載圖片,如果下載失敗則拋出異常,如果下載成功則會調(diào)用imageCache將圖片進行緩存。

//如果沒有緩存或者用戶允許刷新緩存
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {

//如果在緩存中找到了圖片,直接回調(diào)。
            if (cachedImage && options & SDWebImageRefreshCached) {
                [self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            }

           /**
            省略一部分用戶定制策略的判斷
            **/

            //使用imageDownloader進行下載。
            SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) {
                __strong __typeof(weakOperation) strongOperation = weakOperation;
                if (!strongOperation || strongOperation.isCancelled) {
                    
                    //如果這個任務被取消了,則什么都不做

                } else if (error) {
                    [self callCompletionBlockForOperation:strongOperation completion:completedBlock error:error url:url];
                    如果這個任務失敗了,則組裝error對象,拋出
                    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) {
                            //將這個URL添加到失敗URL數(shù)組中
                            [self.failedURLs addObject:url];
                        }
                    }
                }
                else {
                    //下載成功,如果url曾經(jīng)失敗過,則將這個url從失敗數(shù)組中移除
                    if ((options & SDWebImageRetryFailed)) {
                        @synchronized (self.failedURLs) {
                            [self.failedURLs removeObject:url];
                        }
                    }
                    
                    BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

                    if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
                       //只是刷新緩存,則不回調(diào)

                    } 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), ^{
                            //如果下載成功,并且不是GIF圖片,并且代理實現(xiàn)了圖片轉換
                            //則先使用代理進行圖片轉換
                            //將裝換后的結果進行緩存
                            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
                                [self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
                            }
                            
                            //將結果回調(diào)
                            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:transformedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                        });
                    } else {
                        if (downloadedImage && finished) {
                            //緩存
                            [self.imageCache storeImage:downloadedImage imageData:downloadedData forKey:key toDisk:cacheOnDisk completion:nil];
                        }
                            //回調(diào)
                        [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
                    }
                }

                if (finished) {
                    //完畢后,將operation從進行中數(shù)組中移除
                    [self safelyRemoveOperationFromRunning:strongOperation];
                }
            }];
            @synchronized(operation) {
                // Need same lock to ensure cancelBlock called because cancel method can be called in different queue
                operation.cancelBlock = ^{
                    [self.imageDownloader cancel:subOperationToken];
                    __strong __typeof(weakOperation) strongOperation = weakOperation;
                    [self safelyRemoveOperationFromRunning:strongOperation];
                };
            }
3.2>有緩存則直接返回
else if (cachedImage) {
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }
3.3>沒有緩存,用戶又不允許下載,則返回nil并講operation移除。
else {
            // Image not in cache and download disallowed by delegate
            __strong __typeof(weakOperation) strongOperation = weakOperation;
            [self callCompletionBlockForOperation:strongOperation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES url:url];
            [self safelyRemoveOperationFromRunning:operation];
        }
其他接口

取消所有任務,從正在執(zhí)行的任務數(shù)組中獲取所有任務對象,并調(diào)用它們的cancel方法

- (void)cancelAll {
    @synchronized (self.runningOperations) {
        NSArray<SDWebImageCombinedOperation *> *copiedOperations = [self.runningOperations copy];
        [copiedOperations makeObjectsPerformSelector:@selector(cancel)];
        [self.runningOperations removeObjectsInArray:copiedOperations];
    }
}

//檢測sd當前是否正在執(zhí)行任務

- (BOOL)isRunning {
    BOOL isRunning = NO;
    @synchronized (self.runningOperations) {
        isRunning = (self.runningOperations.count > 0);
    }
    return isRunning;
}

//安全移除任務,由于runningOperations可能被多個線程同時訪問,所以需要進行加鎖

- (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation {
    @synchronized (self.runningOperations) {
        if (operation) {
            [self.runningOperations removeObject:operation];
        }
    }
}
小結:
  • 調(diào)度層下載的主要流程是什么

查找緩存,若緩存中沒有 image 則通過 SDWebImageDownloader 來進行下載,下載完成后通過 SDImageCache 進行緩存,會同時緩存到 memCache 和 diskCache 中

  • 為什么下載成功要將url從失敗數(shù)組中移除。

因為[self.failedURLs addObject:url]是只在下載失敗時添加的,而下載成功和下載失敗是互斥的,也就是說,下載成功時failedURLs數(shù)組里就不應該有這個url,為什么要這么寫呢,這是為了解決競態(tài)條件下的問題,若兩個線程下載同一個url的圖片,若第一個線程下載失敗,第二個下載成功。如果不從failedURLs移除這個url的話,以后下載此url的圖片都會失敗。

  • 為什么要返回返回的SDWebImageCombinedOperation類型

這個類型包含NSOperation *cacheOperation的一個子類型,其中cacheOperation中又存在id <SDWebImageOperation>的下載圖片的subOperation。在cancel的時候也應該把這兩個操作都cancle。

相關文章

SDWebImage源碼閱讀筆記
SDWebImage 源碼閱讀筆記(一)

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

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

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