SDWebImage探究(九) —— 深入研究圖片下載流程(三)之下載之前的緩存查詢操作

版本記錄

版本號(hào) 時(shí)間
V1.0 2018.02.12

前言

我們做APP,文字和圖片是絕對(duì)不可缺少的元素,特別是圖片一般存儲(chǔ)在圖床里面,一般公司可以委托第三方保存,NB的公司也可以自己存儲(chǔ)圖片,ios有很多圖片加載的第三方框架,其中最優(yōu)秀的莫過(guò)于SDWebImage,它幾乎可以滿足你所有的需求,用了好幾年這個(gè)框架,今天想總結(jié)一下。感興趣的可以看其他幾篇。
1. SDWebImage探究(一)
2. SDWebImage探究(二)
3. SDWebImage探究(三)
4. SDWebImage探究(四)
5. SDWebImage探究(五)
6. SDWebImage探究(六) —— 圖片類型判斷深入研究
7. SDWebImage探究(七) —— 深入研究圖片下載流程(一)之有關(guān)option的位移枚舉的說(shuō)明
8. SDWebImage探究(八) —— 深入研究圖片下載流程(二)之開(kāi)始下載并返回下載結(jié)果的總的方法

回顧

如果看過(guò)上一篇你就知道,上一篇主要說(shuō)的是調(diào)用接口,然后通過(guò)UIView分類中的一個(gè)方法- (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;返回給調(diào)用接口的completedBlock回調(diào),獲取到你想要的圖片。

那么本篇我們就看一下上面方法中的那個(gè)下載方法。


下載方法內(nèi)部

在上面的方法中,我們調(diào)用了SDWebImageManager的對(duì)象進(jìn)行了下載,返回了id類型遵循協(xié)議的operation對(duì)象,id <SDWebImageOperation> operation

下面我們就看一下該方法的API,幫助大家理解

/**
 * Downloads the image at the given URL if not present in cache or return the cached version otherwise. 
 *
 * @param url            The URL to the image
 * @param options        A mask to specify options to use for this request
 * @param progressBlock  A block called while image is downloading
 *                       @note the progress block is executed on a background queue
 * @param completedBlock A block called when operation has been completed.
 *
 *   This parameter is required.
 * 
 *   This block has no return value and takes the requested UIImage as first parameter and the NSData representation as second parameter.
 *   In case of error the image parameter is nil and the third parameter may contain an NSError.
 *
 *   The forth parameter is an `SDImageCacheType` enum indicating if the image was retrieved from the local cache
 *   or from the memory cache or from the network.
 *
 *   The fith parameter is set to NO when the SDWebImageProgressiveDownload option is used and the image is
 *   downloading. This block is thus called repeatedly with a partial image. When image is fully downloaded, the
 *   block is called a last time with the full image and the last parameter set to YES.
 *
 *   The last parameter is the original image URL
 *
 * @return Returns an NSObject conforming to SDWebImageOperation. Should be an instance of SDWebImageDownloaderOperation
 */
- (nullable id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
                                              options:(SDWebImageOptions)options
                                             progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
                                            completed:(nullable SDInternalCompletionBlock)completedBlock;

從上面描述我們就可以看到,該方法的作用就是如果該圖像在內(nèi)存或者硬盤中不存在,那么就根據(jù)指定的url進(jìn)行下載,如果存在就直接給出cache中的圖像,這個(gè)也是SDWebImage的整體原理架構(gòu)。


下載方法分析

下面我們就一起分析一下這個(gè)比較長(zhǎng)的方法。

1. 容錯(cuò)處理

首先還是對(duì)一些參數(shù)進(jìn)行容錯(cuò)處理。

  • completedBlock不能為空,這里用的是斷言NSAssert
// Invoking this method without a completedBlock is pointless

NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead");
  • 對(duì)url參數(shù)的類型進(jìn)行了判斷容錯(cuò)處理
// 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;
}

這里針url做了兩方面的容錯(cuò)處理

  • 有些人總是錯(cuò)將NSString對(duì)象傳給NSURL對(duì)象,但是xcode也不會(huì)報(bào)錯(cuò),所以這里進(jìn)行了判斷,傳入的url值如果是NSString對(duì)象就轉(zhuǎn)化為NSURL對(duì)象。

  • 如果url是除了NSString和NSURL以外的對(duì)象,那么就直接讓url = nil。

2. 下載失敗url集合

這里維護(hù)了一個(gè)集合屬性,用于存放下載失敗的url

@property (strong, nonatomic, nonnull) NSMutableSet<NSURL *> *failedURLs;

這里首先判斷,要下載的這個(gè)url是否是在下載失敗的集合列表里面,這里加了鎖,保證線程數(shù)據(jù)安全。

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

下面接著進(jìn)行條件判斷

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

當(dāng)url的長(zhǎng)度為0或者options枚舉值不是SDWebImageRetryFailedisFailedUrl == YES的情況下,就直接調(diào)用下面方法直接返回給UIView分類那個(gè)sd_internalSetImageWithURL方法,返回給調(diào)用接口的completionBlock回調(diào)。

- (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation
                             completion:(nullable SDInternalCompletionBlock)completionBlock
                                  error:(nullable NSError *)error
                                    url:(nullable NSURL *)url {
    [self callCompletionBlockForOperation:operation completion:completionBlock image:nil data:nil error:error cacheType:SDImageCacheTypeNone finished:YES url:url];
}

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

3. 操作集合

這里實(shí)例化了類SDWebImageCombinedOperation并且進(jìn)行了弱化,因?yàn)楹竺鎎lock要使用。

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

這里維護(hù)了一個(gè)可變數(shù)組,作為放置下載操作的容器。

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

利用鎖將這個(gè)操作添加到這個(gè)容器中

@synchronized (self.runningOperations) {
    [self.runningOperations addObject:operation];
}

4. 生成圖片緩存的key

下面就是生成圖片緩存的key,這個(gè)key有什么用呢?它有兩個(gè)作用,其一是用來(lái)查詢緩存中對(duì)應(yīng)的圖片資源;其二就是如果內(nèi)存和disk中就需要從網(wǎng)絡(luò)上下載,下載后就要進(jìn)行緩存,那就是用來(lái)存儲(chǔ)圖片緩存的key。

NSString *key = [self cacheKeyForURL:url];
- (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url {
    if (!url) {
        return @"";
    }

    if (self.cacheKeyFilter) {
        return self.cacheKeyFilter(url);
    } else {
        return url.absoluteString;
    }
}

下面我們就一起看一下生成的規(guī)則,如果url為nil那么直接就返回空的字符串,否則就判斷這個(gè)blockcacheKeyFilter是否為空,如果不為空就直接調(diào)用block,如果為空就直接返回url完整的表示形式url.absoluteString。下面我們就詳細(xì)的看一下這個(gè)block,先看一下API幫助理解。

typedef NSString * _Nullable (^SDWebImageCacheKeyFilterBlock)(NSURL * _Nullable url);

/**
 * The cache filter is a block used each time SDWebImageManager need to convert an URL into a cache key. This can
 * be used to remove dynamic part of an image URL.
 *
 * The following example sets a filter in the application delegate that will remove any query-string from the
 * URL before to use it as a cache key:
 *
 * @code

[[SDWebImageManager sharedManager] setCacheKeyFilter:^(NSURL *url) {
    url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path];
    return [url absoluteString];
}];

 * @endcode
 */
@property (nonatomic, copy, nullable) SDWebImageCacheKeyFilterBlock cacheKeyFilter;

補(bǔ)充說(shuō)明

這里舉例和大家說(shuō)明一個(gè)URL的scheme、host等參數(shù)都是什么。

// 以這個(gè)地址為例  https://www.baidu.com/abcdef?a=1

NSString *str = @"https://www.baidu.com/abcdef?a=1";
NSURL *url = [NSURL URLWithString:str];
NSLog(@"url.absoluteString = %@", url.absoluteString);
NSLog(@"url.scheme = %@", url.scheme);
NSLog(@"url.host = %@", url.host);
NSLog(@"url.path = %@", url.path);

下面看一下輸出

2018-02-12 11:04:00.988966+0800 JJWebImage[4751:1325852] url.absoluteString = https://www.baidu.com/abcdef?a=1
2018-02-12 11:04:00.989054+0800 JJWebImage[4751:1325852] url.scheme = https
2018-02-12 11:04:00.989090+0800 JJWebImage[4751:1325852] url.host = www.baidu.com
2018-02-12 11:04:00.989168+0800 JJWebImage[4751:1325852] url.path = /abcdef

這里就不給大家解釋了,看輸出一目了然。

下面我們繼續(xù),通過(guò)上面的步驟我們就生成了圖片緩存的key了。

5. 在SDImageCache中查詢緩存

這里利用SDImageCache類中的下面方法進(jìn)行緩存的查詢。

// typedef void(^SDCacheQueryCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType);

/**
 * Operation that queries the cache asynchronously and call the completion when done.
 *
 * @param key       The unique key used to store the wanted image
 * @param doneBlock The completion block. Will not get called if the operation is cancelled
 *
 * @return a NSOperation instance containing the cache op
 */
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock;

這里我們可以看見(jiàn),就是利用上面生成的key進(jìn)行緩存的本地查詢,這個(gè)查詢是異步的,查詢完畢會(huì)返回一個(gè)包含三個(gè)參數(shù)的block - SDCacheQueryCompletedBlock,并且如果該操作被標(biāo)記為取消狀態(tài),那么就不會(huì)調(diào)用這個(gè)doneBlock了。

下面看一下該方法實(shí)現(xiàn)的API

- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
    if (!key) {
        if (doneBlock) {
            doneBlock(nil, nil, SDImageCacheTypeNone);
        }
        return nil;
    }

    // First check the in-memory cache...
    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];
    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];
            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;
}

下面就一起分析下這個(gè)查詢方法的實(shí)現(xiàn)。

容錯(cuò)處理

和別的方法一樣,上來(lái)直接就是榮錯(cuò)處理,如果key為nil,那么直接return,返回不帶任何圖像信息的block:doneBlock(nil, nil, SDImageCacheTypeNone);

檢查內(nèi)存中圖像以及disk中的NSData圖像數(shù)據(jù)

這里首先檢查內(nèi)存中是否包含該圖像,實(shí)現(xiàn)如下:

// First check the in-memory cache...

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

這里利用方法imageFromMemoryCacheForKey:,根據(jù)傳入的key查詢是否有該圖像,接著就是if判斷,針對(duì)圖像類型,如果圖像是GIF類型的需要調(diào)用方法diskImageDataBySearchingAllPathsForKey:進(jìn)行特殊處理,然后調(diào)用block回調(diào)doneBlock(image, diskData, SDImageCacheTypeMemory);

那么這里就有幾個(gè)問(wèn)題了,第一個(gè)是如何利用方法imageFromMemoryCacheForKey:查詢到對(duì)應(yīng)的image的呢,下面我們就看一下代碼。

//實(shí)例化的時(shí)候初始化內(nèi)存緩存
_memCache = [[AutoPurgeCache alloc] init];

@property (strong, nonatomic, nonnull) NSCache *memCache;

- (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key {
    return [self.memCache objectForKey:key];
}

這里直接給出了根據(jù)key直接取值的代碼,有取那么就有存儲(chǔ),如下:

// if memory cache is enabled
if (self.config.shouldCacheImagesInMemory) {
    NSUInteger cost = SDCacheCostForImage(image);
    [self.memCache setObject:image forKey:key cost:cost];
}

這里利用類SDImageCacheConfig中的屬性判斷是否要進(jìn)行內(nèi)存緩存

/**
 * use memory cache [defaults to YES]
 */
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;

如果是要進(jìn)行內(nèi)存緩存,就直接利用memCache進(jìn)行緩存。這樣第1個(gè)問(wèn)題就解決了。

下面就是第二個(gè)問(wèn)題,利用UIImage分類判斷了是否是GIF圖,self.images != nil,如果是GIF圖調(diào)用了方法diskImageDataBySearchingAllPathsForKey:獲取了圖像的NSData數(shù)據(jù)類型,看一下該方法里面的實(shí)現(xiàn)。

- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
    NSString *defaultPath = [self defaultCachePathForKey:key];
    NSData *data = [NSData dataWithContentsOfFile:defaultPath];
    if (data) {
        return data;
    }

    // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
    // checking the key with and without the extension
    data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension];
    if (data) {
        return data;
    }

    NSArray<NSString *> *customPaths = [self.customPaths copy];
    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath];
        if (imageData) {
            return imageData;
        }

        // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
        // checking the key with and without the extension
        imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension];
        if (imageData) {
            return imageData;
        }
    }

    return nil;
}

這里,首先要做的是,根據(jù)默認(rèn)路徑獲取NSData類型數(shù)據(jù)。

NSString *defaultPath = [self defaultCachePathForKey:key];
NSData *data = [NSData dataWithContentsOfFile:defaultPath];
- (nullable NSString *)defaultCachePathForKey:(nullable NSString *)key {
    return [self cachePathForKey:key inPath:self.diskCachePath];
}
- (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path {
    NSString *filename = [self cachedFileNameForKey:key];
    return [path stringByAppendingPathComponent:filename];
}
- (nullable NSString *)cachedFileNameForKey:(nullable NSString *)key {
    const char *str = key.UTF8String;
    if (str == NULL) {
        str = "";
    }
    unsigned char r[CC_MD5_DIGEST_LENGTH];
    CC_MD5(str, (CC_LONG)strlen(str), r);
    NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@",
                          r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10],
                          r[11], r[12], r[13], r[14], r[15], [key.pathExtension isEqualToString:@""] ? @"" : [NSString stringWithFormat:@".%@", key.pathExtension]];

    return filename;
}

可以看見(jiàn),這里是以key為基礎(chǔ),進(jìn)行了MD5加密作為了存儲(chǔ)圖像的文件名,利用NSDatadataWithContentsOfFile:方法獲取了NSData數(shù)據(jù)。如果數(shù)據(jù)不為空,直接就返回了data。

接著,由于https://github.com/rs/SDWebImage/pull/976給磁盤文件名添加了擴(kuò)展,所以這里要特殊處理一下。

data = [NSData dataWithContentsOfFile:defaultPath.stringByDeletingPathExtension];
if (data) {
    return data;
}

下面就是維護(hù)一個(gè)可變數(shù)組作為cach的路徑集合,并利用addReadOnlyCachePath:添加了路徑。

@property (strong, nonatomic, nullable) NSMutableArray<NSString *> *customPaths;

- (void)addReadOnlyCachePath:(nonnull NSString *)path {
    if (!self.customPaths) {
        self.customPaths = [NSMutableArray new];
    }

    if (![self.customPaths containsObject:path]) {
        [self.customPaths addObject:path];
    }
}

然后對(duì)數(shù)組進(jìn)行了遍歷,找到path,在根據(jù)key生成文件目錄,在利用NSData的方法生成該類型的數(shù)據(jù)。

    for (NSString *path in customPaths) {
        NSString *filePath = [self cachePathForKey:key inPath:path];
        NSData *imageData = [NSData dataWithContentsOfFile:filePath];
        if (imageData) {
            return imageData;
        }

        // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name
        // checking the key with and without the extension
        imageData = [NSData dataWithContentsOfFile:filePath.stringByDeletingPathExtension];
        if (imageData) {
            return imageData;
        }
    }

這里一樣有文件名擴(kuò)展的問(wèn)題,上面已經(jīng)進(jìn)行了特殊處理。到此為止我們都沒(méi)有進(jìn)行任何下載的操作,所以上面凡是有return的地方都是返回的nil。

實(shí)例化operation

到這里就示例話一個(gè)新的對(duì)象,并且在一個(gè)新的隊(duì)列中執(zhí)行異步操作。

#define SDDispatchQueueSetterSementics strong

@property (SDDispatchQueueSetterSementics, nonatomic, nullable) dispatch_queue_t ioQueue;

這里SDDispatchQueueSetterSementics沒(méi)見(jiàn)過(guò),其實(shí)就是宏定義strong而已。

接著就是做了下面這么處理

@autoreleasepool {
    NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
    UIImage *diskImage = [self diskImageForKey:key];
    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);
        });
    }
}

這里就是在disk里面根據(jù)key查找了圖像,如果能查找到,并且設(shè)置self.config.shouldCacheImagesInMemory == YES,那么就會(huì)將圖像緩存的內(nèi)存。

NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];

這些都執(zhí)行完畢后,在主線程里面返回doneBlock這個(gè)block,并return operation;。

到此為止,有關(guān)查找緩存操作- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock的就結(jié)束了,下一篇我們就一起看一下,這個(gè)方法的回調(diào)中的處理。

后記

本篇已結(jié)束,后面更精彩~~~~

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

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

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