SDWebImage探究(九) —— 深入研究图片下载流程(三)之下载之前的缓存查询操作

版本记录

版本号 时间
V1.0 2018.02.12

前言

我们做APP,文字和图片是绝对不可缺少的元素,特别是图片一般存储在图床里面,一般公司可以委托第三方保存,NB的公司也可以自己存储图片,ios有很多图片加载的第三方框架,其中最优秀的莫过于SDWebImage,它几乎可以满足你所有的需求,用了好几年这个框架,今天想总结一下。感兴趣的可以看其他几篇。
1. SDWebImage探究(一)
2. SDWebImage探究(二)
3. SDWebImage探究(三)
4. SDWebImage探究(四)
5. SDWebImage探究(五)
6. SDWebImage探究(六) —— 图片类型判断深入研究
7. SDWebImage探究(七) —— 深入研究图片下载流程(一)之有关option的位移枚举的说明
8. SDWebImage探究(八) —— 深入研究图片下载流程(二)之开始下载并返回下载结果的总的方法

回顾

如果看过上一篇你就知道,上一篇主要说的是调用接口,然后通过UIView分类中的一个方法- (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;返回给调用接口的completedBlock回调,获取到你想要的图片。

那么本篇我们就看一下上面方法中的那个下载方法。


下载方法内部

在上面的方法中,我们调用了SDWebImageManager的对象进行了下载,返回了id类型遵循协议的operation对象,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;

从上面描述我们就可以看到,该方法的作用就是如果该图像在内存或者硬盘中不存在,那么就根据指定的url进行下载,如果存在就直接给出cache中的图像,这个也是SDWebImage的整体原理架构。


下载方法分析

下面我们就一起分析一下这个比较长的方法。

1. 容错处理

首先还是对一些参数进行容错处理。

  • completedBlock不能为空,这里用的是断言NSAssert
// Invoking this method without a completedBlock is pointless

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

这里针url做了两方面的容错处理

  • 有些人总是错将NSString对象传给NSURL对象,但是xcode也不会报错,所以这里进行了判断,传入的url值如果是NSString对象就转化为NSURL对象。

  • 如果url是除了NSString和NSURL以外的对象,那么就直接让url = nil

2. 下载失败url集合

这里维护了一个集合属性,用于存放下载失败的url

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

这里首先判断,要下载的这个url是否是在下载失败的集合列表里面,这里加了锁,保证线程数据安全。

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

当url的长度为0或者options枚举值不是SDWebImageRetryFailedisFailedUrl == YES的情况下,就直接调用下面方法直接返回给UIView分类那个sd_internalSetImageWithURL方法,返回给调用接口的completionBlock回调。

- (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. 操作集合

这里实例化了类SDWebImageCombinedOperation并且进行了弱化,因为后面block要使用。

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

这里维护了一个可变数组,作为放置下载操作的容器。

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

利用锁将这个操作添加到这个容器中

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

4. 生成图片缓存的key

下面就是生成图片缓存的key,这个key有什么用呢?它有两个作用,其一是用来查询缓存中对应的图片资源;其二就是如果内存和disk中就需要从网络上下载,下载后就要进行缓存,那就是用来存储图片缓存的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;
    }
}

下面我们就一起看一下生成的规则,如果url为nil那么直接就返回空的字符串,否则就判断这个blockcacheKeyFilter是否为空,如果不为空就直接调用block,如果为空就直接返回url完整的表示形式url.absoluteString。下面我们就详细的看一下这个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;

补充说明

这里举例和大家说明一个URL的scheme、host等参数都是什么。

// 以这个地址为例  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

这里就不给大家解释了,看输出一目了然。

下面我们继续,通过上面的步骤我们就生成了图片缓存的key了。

5. 在SDImageCache中查询缓存

这里利用SDImageCache类中的下面方法进行缓存的查询。

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

这里我们可以看见,就是利用上面生成的key进行缓存的本地查询,这个查询是异步的,查询完毕会返回一个包含三个参数的block - SDCacheQueryCompletedBlock,并且如果该操作被标记为取消状态,那么就不会调用这个doneBlock了。

下面看一下该方法实现的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;
}

下面就一起分析下这个查询方法的实现。

容错处理

和别的方法一样,上来直接就是荣错处理,如果key为nil,那么直接return,返回不带任何图像信息的block:doneBlock(nil, nil, SDImageCacheTypeNone);

检查内存中图像以及disk中的NSData图像数据

这里首先检查内存中是否包含该图像,实现如下:

// 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:,根据传入的key查询是否有该图像,接着就是if判断,针对图像类型,如果图像是GIF类型的需要调用方法diskImageDataBySearchingAllPathsForKey:进行特殊处理,然后调用block回调doneBlock(image, diskData, SDImageCacheTypeMemory);

那么这里就有几个问题了,第一个是如何利用方法imageFromMemoryCacheForKey:查询到对应的image的呢,下面我们就看一下代码。

//实例化的时候初始化内存缓存
_memCache = [[AutoPurgeCache alloc] init];

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

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

这里直接给出了根据key直接取值的代码,有取那么就有存储,如下:

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

这里利用类SDImageCacheConfig中的属性判断是否要进行内存缓存

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

如果是要进行内存缓存,就直接利用memCache进行缓存。这样第1个问题就解决了。

下面就是第二个问题,利用UIImage分类判断了是否是GIF图,self.images != nil,如果是GIF图调用了方法diskImageDataBySearchingAllPathsForKey:获取了图像的NSData数据类型,看一下该方法里面的实现。

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

这里,首先要做的是,根据默认路径获取NSData类型数据。

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

可以看见,这里是以key为基础,进行了MD5加密作为了存储图像的文件名,利用NSDatadataWithContentsOfFile:方法获取了NSData数据。如果数据不为空,直接就返回了data。

接着,由于https://github.com/rs/SDWebImage/pull/976给磁盘文件名添加了扩展,所以这里要特殊处理一下。

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

下面就是维护一个可变数组作为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];
    }
}

然后对数组进行了遍历,找到path,在根据key生成文件目录,在利用NSData的方法生成该类型的数据。

    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。

实例化operation

到这里就示例话一个新的对象,并且在一个新的队列中执行异步操作。

#define SDDispatchQueueSetterSementics strong

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

这里SDDispatchQueueSetterSementics没见过,其实就是宏定义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里面根据key查找了图像,如果能查找到,并且设置self.config.shouldCacheImagesInMemory == YES,那么就会将图像缓存的内存。

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

这些都执行完毕后,在主线程里面返回doneBlock这个block,并return operation;

到此为止,有关查找缓存操作- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock的就结束了,下一篇我们就一起看一下,这个方法的回调中的处理。

后记

本篇已结束,后面更精彩~~~~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容