SDWebImage (5.0.6) 图片缓存读写原理

SDWebImage (5.0.6)图片加载奇淫巧技 这篇文章介绍了SDWebImage加载图片的流程是怎样的,本文我们一起讨论一下,SDWebImage框架的缓存机制是怎么样的。
我们先来看加载过程中,SDWebImage是如何从缓存中读取我们所需的图片的,我们先找到读取缓存的入口:

1
2
// Start the entry to load image from cache  
[self callCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock];

这个方法就是开始从缓存中读取,方法内部实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// Query cache process
- (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation
url:(nonnull NSURL *)url
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {
// Check whether we should query cache
BOOL shouldQueryCache = (options & SDWebImageFromLoaderOnly) == 0;
if (shouldQueryCache) {
id<SDWebImageCacheKeyFilter> cacheKeyFilter = context[SDWebImageContextCacheKeyFilter];
NSString *key = [self cacheKeyForURL:url cacheKeyFilter:cacheKeyFilter];
@weakify(operation);
operation.cacheOperation = [self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation);
if (!operation || operation.isCancelled) {
[self safelyRemoveOperationFromRunning:operation];
return;
}
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];
} else {
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock];
}
}

从上面的源码可以看出,首先会判断一下是否需要从缓存中读取,如果不需要就直接去下载了。如果需要的话,就会使用self.imageCache实例去查询缓存,核心代码:

1
2
3
4
5
6
7
8
9
[self.imageCache queryImageForKey:key options:options context:context completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) {
@strongify(operation);
if (!operation || operation.isCancelled) {
[self safelyRemoveOperationFromRunning:operation];
return;
}
// Continue download process
[self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock];
}];

我们可以看到,查询结果回调之后,又调用了下载方法,这时因为缓存查询的结果也在下载方法里面处理了。
我们接着看查询缓存的方法:queryImageForKey: 看下它的内部实现:

1
2
3
4
5
6
7
8
9
10
11
- (id<SDWebImageOperation>)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock {
SDImageCacheOptions cacheOptions = 0;
if (options & SDWebImageQueryMemoryData) cacheOptions |= SDImageCacheQueryMemoryData;
if (options & SDWebImageQueryMemoryDataSync) cacheOptions |= SDImageCacheQueryMemoryDataSync;
if (options & SDWebImageQueryDiskDataSync) cacheOptions |= SDImageCacheQueryDiskDataSync;
if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages;
if (options & SDWebImageAvoidDecodeImage) cacheOptions |= SDImageCacheAvoidDecodeImage;
if (options & SDWebImageDecodeFirstFrameOnly) cacheOptions |= SDImageCacheDecodeFirstFrameOnly;
if (options & SDWebImagePreloadAllFrames) cacheOptions |= SDImageCachePreloadAllFrames;
return [self queryCacheOperationForKey:key options:cacheOptions context:context done:completionBlock];
}

这个方法里面做了一堆跟缓存相关的条件判断,然后调用了queryCacheOperationForKey:这个方法,正式进入缓存查询。我们接着看这个方法的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
- (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock {
if (!key) {
if (doneBlock) {
doneBlock(nil, nil, SDImageCacheTypeNone);
}
return nil;
}

id<SDImageTransformer> transformer = context[SDWebImageContextImageTransformer];
if (transformer) {
// grab the transformed disk image if transformer provided
NSString *transformerKey = [transformer transformerKey];
key = SDTransformedKeyForKey(key, transformerKey);
}

// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];

if ((options & SDImageCacheDecodeFirstFrameOnly) && image.sd_isAnimated) {
#if SD_MAC
image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp];
#else
image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation];
#endif
}

BOOL shouldQueryMemoryOnly = (image && !(options & SDImageCacheQueryMemoryData));
if (shouldQueryMemoryOnly) {
if (doneBlock) {
doneBlock(image, nil, SDImageCacheTypeMemory);
}
return nil;
}

// Second check the disk cache...
NSOperation *operation = [NSOperation new];
// Check whether we need to synchronously query disk
// 1. in-memory cache hit & memoryDataSync
// 2. in-memory cache miss & diskDataSync
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));
void(^queryDiskBlock)(void) = ^{
if (operation.isCancelled) {
// do not call the completion if cancelled
return;
}

@autoreleasepool {
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];
UIImage *diskImage;
SDImageCacheType cacheType = SDImageCacheTypeNone;
if (image) {
// the image is from in-memory cache, but need image data
diskImage = image;
cacheType = SDImageCacheTypeMemory;
} else if (diskData) {
cacheType = SDImageCacheTypeDisk;
// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:key data:diskData options:options context:context];
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memCache setObject:diskImage forKey:key cost:cost];
}
}

if (doneBlock) {
if (shouldQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}
}
};

// Query in ioQueue to keep IO-safe
if (shouldQueryDiskSync) {
dispatch_sync(self.ioQueue, queryDiskBlock);
} else {
dispatch_async(self.ioQueue, queryDiskBlock);
}

return operation;
}

接下来我们解析一下这个方法:
首先做了参数合法性判断,不满足条件直接返回空。
紧接着就是从内存缓存中查询我们需要的图片:

1
2
// First check the in-memory cache...
UIImage *image = [self imageFromMemoryCacheForKey:key];

这个方法比较简单,就是直接从memCache中按照key来读取: [self.memCache objectForKey:key];
接下来判断图片是否是动图,做相应的处理。
然后,判断是否是只需在内存中查找,如果是的话直接返回刚才查到的图片,并结束方法调用。如果不是的话,就去硬盘中查找。

1
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];

我们先把这个流程看完再来看究竟是如何从磁盘中读取Data的。
接下来通过一系列条件得出是否需要同步查询缓存:

1
2
BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) ||
(!image && options & SDImageCacheQueryDiskDataSync));

然后生命一个磁盘查询的block: queryDiskBlock开始查询。
接下来判断是否从内存中读取到了图片,如果有的话,不做其他处理,最后回调的时候把diskData一起回调出去就可以了。如果没有从内存中读取到图片,则就要看diskData是否为空,如果不为空,就对diskData进行解码处理,得到我们需要的diskImage。

1
2
// decode image data only if in-memory cache missed
diskImage = [self diskImageForKey:key data:diskData options:options context:context];

然后把得到的diskImage放到内存缓存中一份。

1
2
3
4
if (diskImage && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = diskImage.sd_memoryCost;
[self.memCache setObject:diskImage forKey:key cost:cost];
}

到这里,从内存以及磁盘中读取缓存的流程完毕,接下来就是把我们读取出来的图片数据返回给调用方了。

1
2
3
4
5
6
7
8
9
if (doneBlock) {
if (shouldQueryDiskSync) {
doneBlock(diskImage, diskData, cacheType);
} else {
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, diskData, cacheType);
});
}
}

最后就是根据是否需要同步查询来调用queryDiskBlock了。这就是图片缓存查询的流程。
接下来,我们回头看一下磁盘缓存查询究竟是如何实现的。

1
NSData *diskData = [self diskImageDataBySearchingAllPathsForKey:key];

我们看一下这个方法的内部实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key {
if (!key) {
return nil;
}

NSData *data = [self.diskCache dataForKey:key];
if (data) {
return data;
}

// Addtional cache path for custom pre-load cache
if (self.additionalCachePathBlock) {
NSString *filePath = self.additionalCachePathBlock(key);
if (filePath) {
data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil];
}
}

return data;
}

首先是key参数判断,其次从diskCache中通过key来读取,如果磁盘缓存,内存中已经读取过的话,直接返回。否则的话,就会通过key得出磁盘缓存的路径,然后读取出来,返回给调用方。

接下来,我们一起看一下,从Server端下载完图片后,SDWebImage是如何处理的他两级(内存缓存、磁盘缓存)缓存的。同样的我们先找到保存缓存的入口方法:

1
[self callStoreCacheProcessForOperation:operation url:url options:options context:context downloadedImage:downloadedImage downloadedData:downloadedData finished:finished progress:progressBlock completed:completedBlock];

这个方法的调用时机,是在从网络下载完图片的回调中。
这个方法的内部实现中有一句很重要的代码,就是我们存储缓存的代码:

1
[self.imageCache storeImage:downloadedImage imageData:cacheData forKey:key cacheType:storeCacheType completion:nil];

接下来我们来看一下,这个方法的内部实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock {
switch (cacheType) {
case SDImageCacheTypeNone: {
[self storeImage:image imageData:imageData forKey:key toMemory:NO toDisk:NO completion:completionBlock];
}
break;
case SDImageCacheTypeMemory: {
[self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:NO completion:completionBlock];
}
break;
case SDImageCacheTypeDisk: {
[self storeImage:image imageData:imageData forKey:key toMemory:NO toDisk:YES completion:completionBlock];
}
break;
case SDImageCacheTypeAll: {
[self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:YES completion:completionBlock];
}
break;
default: {
if (completionBlock) {
completionBlock();
}
}
break;
}
}

内部实现其实比较简单,就是根据缓存类型做相应的存储。这一句才是重点:

1
[self storeImage:image imageData:imageData forKey:key toMemory:YES toDisk:YES completion:completionBlock];

我们接着往下面看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toMemory:(BOOL)toMemory
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock {
if (!image || !key) {
if (completionBlock) {
completionBlock();
}
return;
}
// if memory cache is enabled
if (toMemory && self.config.shouldCacheImagesInMemory) {
NSUInteger cost = image.sd_memoryCost;
[self.memCache setObject:image forKey:key cost:cost];
}

if (toDisk) {
dispatch_async(self.ioQueue, ^{
@autoreleasepool {
NSData *data = imageData;
if (!data && image) {
// If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format
SDImageFormat format;
if ([SDImageCoderHelper CGImageContainsAlpha:image.CGImage]) {
format = SDImageFormatPNG;
} else {
format = SDImageFormatJPEG;
}
data = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:nil];
}
[self _storeImageDataToDisk:data forKey:key];
}

if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
} else {
if (completionBlock) {
completionBlock();
}
}
}

接着我们分心一下存储缓存的最关键的方法,没错,就是上面的这段代码。
首先,对参数进行合法性判断,如果不合法,直接返回。否则继续向下执行:
第一步:判断是否允许内存缓存,如果允许就把下载好的图片存储在内存中一份。
第二步:判断是否允许存储到磁盘,如果允许就进入磁盘存储逻辑:
这里的存储任务是提交到了一个异步的串行队列中。
任务的具体处理逻辑是:如果imageData 不存在,但是image 有值,则对image进行归档,得到一份data,这也是我们要写入到磁盘的data。
最后,调用_storeImageDataToDisk:方法,将二进制图片数据写入到磁盘。
第三步:将结果回调出去。

我们看一下_storeImageDataToDisk:方法的内部实现:

1
2
3
4
5
6
7
8
// Make sure to call form io queue by caller
- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
if (!imageData || !key) {
return;
}

[self.diskCache setData:imageData forKey:key];
}

比较简单,就是通过diskCache 实例来写缓存。接着我们进入diskCache类里面看下是如何写入的。
我们看下SDDiskCache里面的setData: forKey:方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)setData:(NSData *)data forKey:(NSString *)key {
NSParameterAssert(data);
NSParameterAssert(key);
if (![self.fileManager fileExistsAtPath:self.diskCachePath]) {
[self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}

// get cache Path for image key
NSString *cachePathForKey = [self cachePathForKey:key];
// transform to NSUrl
NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey];

[data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil];

// disable iCloud backup
if (self.config.shouldDisableiCloud) {
// ignore iCloud backup resource value error
[fileURL setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil];
}
}

其实这个也很简单,就是简单的文件写入。

到这里,SDWebImage缓存的读写机制就介绍完了,欢迎大家勘误,SDWebImage这个库比较强大,里面还有很多细节,文中都没有提到,这里只是做了主流程的介绍,读者要是想深入的理解,还得是去阅读源码,建议的阅读的时候,和本文一起读,有助于理解源码。
下一篇文章,会介绍SDWebImage是如何对图片进行编码解码的,欢迎大家阅读。

谢谢您的支持!