SDWebImage缓存机制学习

SDWebImage是很多项目中都会用到的三方库。这篇文章用来记录下学习SDWebImage图片缓存机制的心得。在SDWebImage更新到4.0后,对部分内容进行了修正。

SDWebImageCache结构

上图是SDWebImage设置图片的流程,也是我们项目中最常用到的,下面就分几个部分来具体看一下。

设置缓存

通常我们使用SDImage加载图片时,会使用SDWebImage对UIImageView的扩展方法

1
[imageView sd_setImageWithURL:[NSURL URLWithString:@"图片的URL"]];

而它最终会调用UIView+WebCache中的方法

1
2
3
4
5
6
7
8
- (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<NSString *, id> *)context

来看一下这个方法中比较重要的代码

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
- (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 {
// 取消之前的下载操作
NSString *validOperationKey = operationKey ?: NSStringFromClass([self class]);
[self sd_cancelImageLoadOperationWithKey:validOperationKey];
// 动态绑定imageURLKey,用来存储和返回下载图片的地址
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

if (url) { // 判断url是否有效
// 开启下载任务,返回下载的operation
__weak __typeof(self)wself = self;
// 构建图片下载的operation(需遵守SDWebImageOperation协议,用于取消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) {
// 图片下载结果回调
__strong __typeof (wself) sself = wself;
dispatch_main_async_safe(^{
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
// options设置了不自动 调用Set图片的策略时直接回调结果
completedBlock(image, error, cacheType, url);
return;
} else if (image) {
// 自动设置图片。(内部会做判断设置UIImage的图片、UIButton不同状态的图片和背景图等)
[sself sd_setImage:image imageData:data basedOnClassOrViaCustomSetImageBlock:setImageBlock];
[sself sd_setNeedsLayout];
} else {
}
// 结果的回调,包含图片、错误、缓存类型、及图片
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
// 由于异步下载图片,根据validOperationKey存储图片下载操作,以备图片下载未完成之时用来取消operation
[self sd_setImageLoadOperation:operation forKey:validOperationKey];
} else {
// 直接回调失败
}
}
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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
- (id <SDWebImageOperation>)loadImageWithURL:(nullable NSURL *)url
options:(SDWebImageOptions)options
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock {


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

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

// 如果url错误、或者下载options没有重试的选项且已经下载失败过一次的url 直接返回初始的operation抛出错误。
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;
}

@synchronized (self.runningOperations) {
[self.runningOperations addObject:operation];
}
NSString *key = [self cacheKeyForURL:url];

operation.cacheOperation = [self.imageCache queryCacheOperationForKey:key done:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
if (operation.isCancelled) {
[self safelyRemoveOperationFromRunning:operation];
return;
}
// 磁盘缓存没有命中 || 刷新磁盘缓存 || 针对当前的url是否需要下载(默认YES,开发者可以根据代理配置为NO)开启下载任务
if ((!cachedImage || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
if (cachedImage && options & SDWebImageRefreshCached) {
// If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image
// AND try to re-download it in order to let a chance to NSURLCache to refresh it from server.
[self callCompletionBlockForOperation:weakOperation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES url:url];
}

// 设置下载任务的options:如优先级、下载进度条是否显示等,参考SDWebImageOptions

// 开启下载任务,返回下载任务的token用于取消操作
SDWebImageDownloadToken *subOperationToken = [self.imageDownloader downloadImageWithURL:url options:options 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];

// 合理的错误将url加入失败的url数组。便于重试下载(合理是指:url对应的真实资源、非用户主动取消下载等)
if (error.code) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}
}
else {
// 从失败数组中移除
if ((options & SDWebImageRetryFailed)) {
@synchronized (self.failedURLs) {
[self.failedURLs removeObject:url];
}
}

BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

if (options & SDWebImageRefreshCached && cachedImage && !downloadedImage) {
// Image refresh hit the NSURLCache cache, do not call the completion block
} 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), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
// 对图片缓存(比对处理后的图片是否有所改动。改动后不一致会重新对图片encodeData)
[self.imageCache storeImage:transformedImage imageData:(imageWasTransformed ? nil : downloadedData) forKey:key toDisk:cacheOnDisk completion:nil];
}
// 回调
[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];
}
// 回调
[self callCompletionBlockForOperation:strongOperation completion:completedBlock image:downloadedImage data:downloadedData error:nil cacheType:SDImageCacheTypeNone finished:finished url:url];
}
}

if (finished) {
// 从下载数组中移除当前完成的下载operation
[self safelyRemoveOperationFromRunning:strongOperation];
}
}];


operation.cancelBlock = ^{
// 取消下载、移除下载中的记录
[self.imageDownloader cancel:subOperationToken];
__strong __typeof(weakOperation) strongOperation = weakOperation;
[self safelyRemoveOperationFromRunning:strongOperation];
};
} 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];
} else {

// 缓冲未命中、不允许下载直接回调返回 移除下载记录
__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];
}
}];

return operation;

如果下载成功了,就会调用以下方法来设置缓存

1
2
3
4
5
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* Asynchronously store an image into memory and disk cache at the given key.
*
* @param image The image to store
* @param imageData The image data as returned by the server, this representation will be used for disk storage
* instead of converting the given image object into a storable/compressed image format in order
* to save quality and CPU
* @param key The unique image cache key, usually it's image absolute URL
* @param toDisk Store the image to disk cache if YES
* @param completionBlock A block executed after the operation is finished
*/
- (void)storeImage:(nullable UIImage *)image
imageData:(nullable NSData *)imageData
forKey:(nullable NSString *)key
toDisk:(BOOL)toDisk
completion:(nullable SDWebImageNoParamsBlock)completionBlock;

方法的注释写的很清楚,一共有五个参数,分别是缓存的图片,服务端返回的图片数据,是否保存的disk的flag,缓存的key(通常是图片的完整链接)以及缓存完成后的回调。

这里有一个memCache对象,它继承于NSCache

@interface SDMemoryCache <KeyType, ObjectType> : NSCache <KeyType, ObjectType>

如果缓存配置里使用的是缓存到内存中,那么就会使用[self.memCache setObject:image forKey:key cost:cost];来进行缓存设置。而如果缓存是保存到disk中的,那么就会执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Make sure to call form io queue by caller
- (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key {
if (!imageData || !key) {
return;
}

if (![self.fileManager fileExistsAtPath:_diskCachePath]) {
[self.fileManager createDirectoryAtPath:_diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL];
}

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

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

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

以上就是sd下载缓存的大致逻辑。

读取缓存

上文中可以看到,SDWebImageManager会使用imageCache来查询缓存。首先会查询内存中是否存在图片。存在则返回图片数据,不会再往下查询。不存在则再进行磁盘查询,查询磁盘缓存时以url为key经过MD5计算后拼接得到的完整磁盘路径后异步访问图片。缓冲如果命中后会把得到的图片设置到内存缓存,便于下次使用。

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
 - (nullable NSOperation *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDCacheQueryCompletedBlock)doneBlock {
// 读取内存(NSCache)中的图片
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
NSData *diskData = nil;
if (image.images) {
// 如果是动图,读取图片data
diskData = [self diskImageDataBySearchingAllPathsForKey:key];
}
if (doneBlock) {
doneBlock(image, diskData, SDImageCacheTypeMemory);
}
return nil;
}
// 在内存中查询不到图片数据
NSOperation *operation = [NSOperation new];
// 耗时的io操作(磁盘查询)异步在ioQueue队列中
dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
// 判断读取操作是否被取消保护
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);
});
}
}
});
// 异步操作时返回operation,便于取消磁盘查找
return operation;
}

如果两者中都没有找到,就会开始下载流程。

1
2
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
}

如果下载失败,会把失败的地址写入failedURLs集合中

1
2
3
4
5
6
7
8
9
10
11
if ( error.code != NSURLErrorNotConnectedToInternet
&& error.code != NSURLErrorCancelled
&& error.code != NSURLErrorTimedOut
&& error.code != NSURLErrorInternationalRoamingOff
&& error.code != NSURLErrorDataNotAllowed
&& error.code != NSURLErrorCannotFindHost
&& error.code != NSURLErrorCannotConnectToHost) {
@synchronized (self.failedURLs) {
[self.failedURLs addObject:url];
}
}

需要这个操作是因为SDWebImage有一个对加载失败的图片拒绝再次加载的机制。

Disk缓存清理

上文有提到过SDWebImageCacheConfig,其中有maxCacheAge和maxCacheSize属性,分别是文件缓存时长和文件缓存空间

1
2
3
4
5
6
7
8
9
10

/**
* The maximum length of time to keep an image in the cache, in seconds.
*/
@property (assign, nonatomic) NSInteger maxCacheAge;

/**
* The maximum size of the cache, in bytes.
*/
@property (assign, nonatomic) NSUInteger maxCacheSize;

SDWebImage会注册两个通知

1
2
3
4
5
6
7
8
9
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];

分别在应用进入后台和结束的时候,遍历所有的缓存文件,如果缓存文件超过 maxCacheAge 中指定的时长,就会被删除掉。

如果清理完过期文件后缓存空间仍然没有达到maxCacheSize的要求,那么久会继续清理就文件。

1
static const NSInteger kDefaultCacheMaxCacheAge = 60 * 60 * 24 * 7; // 1 week

SD中maxCacheAge默认值是1周,而对于maxCacheSize没有设置默认值,也就是说对于缓存空间默认不受限制。所以理论上来说图片缓存甚至可以占据整个设备的使用空间,所以这个值最好还是要设置一下。

以上就是SDWebImage缓存机制的流程和原理。