9

天天都在用的 SDWebImage, 你了解它的缓存策略吗?

 3 years ago
source link: http://www.swiftcafe.io/post/sdimage-cache
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

天天都在用的 SDWebImage, 你了解它的缓存策略吗?

天天都在用的 SDWebImage, 你了解它的缓存策略吗?

swift 发布于 2020年12月21日 基本结构

闲言少叙,咱们这就开始。 首先咱们来看看 SDWebImage 的整体结构:

1f9gqf7cj5160867.png

有一个专门的 Cache 分类用来处理图片的缓存。 这里面也有两个类 SDImageCache 和 SDImageCacheConfig。 大部分的缓存处理都在 SDImageCache 这个类中实现。其他几个文件夹咱们分别有个字的功能,因为咱们这次专门讨论缓存策略,所以其他内容暂时略过。

Memory 和 Disk 双缓存

首先,SDWebImage 的图片缓存采用的是 Memory 和 Disk 双重 Cache 机制, 听起来挺高大上吧。其实也不复杂。

我们先来看看 Memory Cache,贴一段 SDImageCache 的代码:

@interface SDImageCache ()

#pragma mark - Properties
@property (strong, nonatomic, nonnull) NSCache *memCache;

...

这里我们发现, 有一个叫做 memCache 的属性,它是一个 NSCache 对象,用于实现我们对图片的 Memory Cache。 SDWebImage 还专门实现了一个叫做 AutoPurgeCache 的类, 相比于普通的 NSCache, 它提供了一个在内存紧张时候释放缓存的能力:

@interface AutoPurgeCache : NSCache
@end

@implementation AutoPurgeCache

- (nonnull instancetype)init {
self = [super init];
if (self) {
#if SD_UIKIT
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(removeAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
#endif
}
return self;
}

其实就是接受系统的内存警告通知,然后清除掉自身的图片缓存。 这里大家比较少见的一个类应该是 NSCache 了。 简单来说,它是一个类似于 NSDictionary 的集合类,用于在内存中存储我们要缓存的数据。详细信息大家可以参考官方文档:https://developer.apple.com/reference/foundation/nscache

说完 Memory Cache, 我们再来说说 Disk Cache,也就是文件缓存。

SDWebImage 会将图片存放到 NSCachesDirectory 目录中:

- (nullable NSString *)makeDiskCachePath:(nonnull NSString*)fullNamespace {
NSArray<NSString *> *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
return [paths[0] stringByAppendingPathComponent:fullNamespace];
}

然后为每一个缓存文件生成一个 md5 文件名, 存放到文件中。

整体机制

为了节约篇幅,提升大家的阅读体验,这里尽量少贴大段代码。 我们前面介绍了 SDWebImage 同时使用内存和硬盘两种缓存。 那么我们来看看当使用 SDWebImage 读取图片时候的完整流程。 我们一般会使用 SDWebImage 对 UIKit 的扩展,直接加载图片:

[imageView sd_setImageWithURL:[NSURL URLWithString:@"http://swiftcafe.io/images/qrcode.jpg"]];

首先这个 Category 方法 sd_setImageWithURL 内部会调用 SDWebImageManager 的 downloadImageWithURL 方法来处理这个图片 URL:

id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
...
}];

SDWebImageManager 内部的 downloadImageWithURL 方法会先使用我们前面提到的 SDImageCache 类的 queryDiskCacheForKey 方法,查询图片缓存:

operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {

...
}];

再来看 queryDiskCacheForKey 方法内部, 先会查询 Memory Cache :

UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}

如果 Memory Cache 查找不到, 就会查询 Disk Cache:

dispatch_async(self.ioQueue, ^{
if (operation.isCancelled) {
return;
}

@autoreleasepool {
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage && self.shouldCacheImagesInMemory) {
NSUInteger cost = SDCacheCostForImage(diskImage);
[self.memCache setObject:diskImage forKey:key cost:cost];
}

dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});

查询 Disk Cache 的时候有一个小插曲,就是如果 Disk Cache 查询成功,还会把得到的图片再次设置到 Memory Cache 中。 这样做可以最大化那些高频率展现图片的效率。

如果缓存查询成功, 那么就会直接返回缓存数据。 如果不成功,接下来就开始请求网络:

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

请求网络使用的是 imageDownloader 属性,这个示例专门负责下载图片数据。 如果下载失败, 会把失败的图片地址写入 failedURLs 集合:

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

为什么要有这个 failedURLs 呢, 因为 SDWebImage 默认会有一个对上次加载失败的图片拒绝再次加载的机制。 也就是说,一张图片在本次会话加载失败了,如果再次加载就会直接拒绝。SDWebImage 这样做可能是为了提高性能吧。这个机制可能会容易被大家忽略,所以这里特意提一下,说不定哪天遇到一些奇怪问题时候,这个知识点会帮你快速定位问题~

如果下载图片成功了,接下来就会使用 [self.imageCache storeImage] 方法将它写入缓存,并且调用 completedBlock 告诉前端显示图片:

if (downloadedImage && finished) {
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}

dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});

好了,到此为止 SDWebImage 的整体图片加载流程就都走完了。 由于要控制篇幅,我这里只挑了最重点的几个节点写出来,SDWebImage 的完整机制肯定会更全面,先帮大家疏通思路。

是否要重试失败的 URL

SDWebImage 的整体图片处理流程咱们体验了一遍。 那么有哪些重要的点对咱们使用它会有帮助呢? 我帮大家整理了一些。

你可以在加载图片的时候设置 SDWebImageRetryFailed 标记,这样 SDWebImage 就会加载之前失败过的图片了。 记得我们前面提到的 failedURLs 属性了吧,这个属性是在内存中存储的,如果图片加载失败, SDWebImage 会在本次 APP 会话中都不再重试这张图片了。当然这个加载失败是有条件的,如果是超时失败,不记在内。

总之,如果你更需要图片的可用性,而不是这一点点的性能优化,那么你就可以带上 SDWebImageRetryFailed 标记:

[_image sd_setImageWithURL:[NSURL URLWithString:@"http://swiftcafe.io/images/qrcodexx.jpg"] placeholderImage:nil options:SDWebImageRetryFailed];Disk 缓存清理策略

SDWebImage 会在每次 APP 结束的时候执行清理任务。 清理缓存的规则分两步进行。 第一步先清除掉过期的缓存文件。 如果清除掉过期的缓存之后,空间还不够。 那么就继续按文件时间从早到晚排序,先清除最早的缓存文件,直到剩余空间达到要求。

具体点,SDWebImage 是怎么控制哪些缓存过期,以及剩余空间多少才够呢? 通过两个属性:

@interface SDImageCache : NSObject

@property (assign, nonatomic) NSInteger maxCacheAge;
@property (assign, nonatomic) NSUInteger maxCacheSize;

maxCacheAge 是文件缓存的时长, SDWebImage 会注册两个通知:

[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];

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

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

同样的, maxCacheSize 控制 SDImageCache 所允许的最大缓存空间。 如果清理完过期文件后缓存空间依然没达到 maxCacheSize 的要求, 那么就会继续清理旧文件,直到缓存空间达到要求为止。

了解了这个机制对我们有什么帮助呢? 我们来继续讲解,我们平时在使用 SDWebImage 的时候是没接触过它们的。那么以此推理,它们一定有默认值,也确实有:

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

上面是 maxCacheAge 的默认值,注释上写的很清楚,缓存一周。 再来看看 maxCacheSize。 翻了一遍 SDWebImage 的代码,并没有对 maxCacheSize 设置默认值。 这就意味着 SDWebImage 在默认情况下不会对缓存空间设限制。

这一点可以在 SDWebImage 清理缓存的代码中求证:

if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
//清理缓存代码
}

说明一下, 上面代码中的 currentCacheSize 变量代表当前图片缓存占用的空间。 从这里可以看出, 只有在 maxCacheSize 大于 0 并且当前缓存空间大于 maxCacheSize 的时候才进行第二步的缓存清理。

这意味着什么呢? 其实就是 SDWebImage 在默认情况下是不对我们的缓存大小设限制的,理论上,APP 中的图片缓存可以占满整个设备。

SDWebImage 的这个特性还是比较容易被大家忽略的,如果你开发的类似信息流的 APP,应该会加载大量的图片,如果这时候按照默认机制,缓存尺寸是没有限制的,并且默认的缓存周期是一周。 就很容易造成应用存储空间占用偏大的问题。

那么可能有人会说了,现在 iPhone 的存储空间都很大,多缓存一点也不是问题吧? 但你是否知道 iOS 上还有一个用量查询的功能呢。在设置项中用户可以查看每个 APP 的空间使用情况, 如果你的 APP 占用空间比较大的话,就很容易成为用户的卸载目标,这应该是需要关注的一个细节。

另外,过多的占用缓存空间其实并不一定有用。大部分情况是一些图片被缓存下来后,很少再被重复展现。所以合理的规划缓存空间尺寸还是很有必要的。可以这样设置:

[SDImageCache sharedImageCache].maxCacheSize = 1024 * 1024 * 50; // 50M

maxCacheSize 是以字节来表示的,我们上面的计算代表 50M 的最大缓存空间。 把这行代码写在你的 APP 启动的时候,这样 SDWebImage 在清理缓存的时候,就会清理多余的缓存文件了。

结语

这次跟大家聊了聊 SDWebImage 整体流程,以及它的缓存策略。SDWebImage 是一个历时很久的开源库,并且它不断的保持着更新。 虽然是一个并不算很复杂的开源库,但仔细研读一下它的代码, 会发现它的内部很多机制设计的还是很巧妙的。 为了保证大家的阅读体验,尽量控制文章的篇幅,这里尽量选出对大家最有帮助的内容和大家分享。这篇文章结构整理花了几天时间,仔细筛选最重要的内容。想达到的效果就是,让他读起来不累,但却能很快抓住重点,让大家得到有用的信息。也希望大家对阅读体验能够提出反馈,帮助我给大家创造更好的内容。

如果想了解 SDWebImage 更详细的内容,那么它的 Github 主页就是最好的地方了,在这里也贴出来,供大家参考: https://github.com/rs/SDWebImage

如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。
qrcode.jpg 关注微信公众号
发现更多精彩
swift-cafe


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK