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 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 )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 *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 *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 *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 *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的就结束了,下一篇我们就一起看一下,这个方法的回调中的处理。

后记

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

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

你可能感兴趣的:(SDWebImage探究(九) —— 深入研究图片下载流程(三)之下载之前的缓存查询操作)