有效的本地 cache 机制,可以避免不必要的重复网络加载,不仅能提高相关应用场景的资源加载速度,也可以避免不必要的流量浪费造成用户损失。但是,由于缓存一般做法是通过 url 经过 md5 变换的值作为 key 进行存储,因此对于同样 url 的资源在第一次缓存之后如果没有合适的清理机制,就会存在不同步的问题,导致 bug 的出现。本文就是再这样的背景下,通过对比 NSURLCache、SDImageCache、YYCache、AFNetworking 等优秀开源库,探索通用的 cache 自动清理方案。
NSURLCache 中的缓存清理方案
关于 NSURLCache,可以阅读 这篇文章,其中详细介绍了 url loading 系统中最关键的缓存部分。
缓存策略 NSURLRequestCachePolicy
typedef NS_ENUM(NSUInteger, NSURLRequestCachePolicy)
{
NSURLRequestUseProtocolCachePolicy = 0,// 对特定的 URL 请求使用网络协议中实现的缓存逻辑。默认策略
NSURLRequestReloadIgnoringLocalCacheData = 1,//数据需要从原始地址加载。不使用现有缓存
NSURLRequestReloadIgnoringLocalAndRemoteCacheData = 4, // 不仅忽略本地缓存,同时也忽略代理服务器或其他中间介质目前已有的、协议允许的缓存(未实现)
NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData,
NSURLRequestReturnCacheDataElseLoad = 2,// 无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么从原始地址加载数据。
NSURLRequestReturnCacheDataDontLoad = 3,// 无论缓存是否过期,先使用本地缓存数据。如果缓存中没有请求所对应的数据,那么放弃从原始地址加载数据,请求视为失败(即:“离线”模式)
NSURLRequestReloadRevalidatingCacheData = 5, // 从原始地址确认缓存数据的合法性后,缓存数据就可以使用,否则从原始地址加载(未实现)
};
SDImageCache 中的缓存清理方案
SDImageCache 是优秀的第三方网络图片下载库 SDWebImage 中使用的自定义 cache 类,也是该库的重要组件之一。功能包括了常见的缓存存储、查询以及清除,通过阅读 SDImageCache 的源码,就可以很快找到 cache 自动清理的处理方案。
cache 组织结构
SDImageCache 由内存缓存和磁盘缓存两部分组成,内存缓存使用 NSCache 实现,磁盘缓存通过NSFileManager的单例来管理,简单易懂。日常开发中,当我们遇到 cache 相关的需求时,其实可以直接使用 SDImageCache 而不需要重新造轮子。
阅读头文件,有这样唯一一个 designated 初始化方法:
- (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns
diskCacheDirectory:(nonnull NSString *)directory NS_DESIGNATED_INITIALIZER;
由此可知,SDImageCache 的初始化需要指定一个磁盘目录作为缓存的根目录和一个文件存储的目录,通过命名空间机制就可以把不同应用场景的缓存目录给隔离开,使用该机制最大的好处就在于,每一个自定的 cache 实例,包括该类的单例,都可以自动管理自己存储路径下的缓存文件,而不会对其他人的 cache 有所影响。
cache 清理机制
先说说内存缓存,由于 SDImageCache 使用 NSCache 来充当内存缓存,而
NSCache 本身就支持内存清理机制,当系统内存很低时该类的实例会自动释放一些对象(模拟器除外),因此理论上不需要做什么额外的处理。该类有两个可供开发者设置的属性:
// 内存总消耗限制,If 0, there is no total cost limit. The default value is 0.
@property NSUInteger totalCostLimit;
// 内存总数量限制,If 0, there is no count limit. The default value is 0.
@property NSUInteger countLimit;
官方文档也对这两个属性做了清晰的解释,不过,SDImageCache 中采用的就是默认值,另外,在 NSCache 子类的初始化方法中监听了系统内存警告的通知,当系统收到内存警告时,清空内存中所有的对象。内存清理 SDImageCache 就做了这么多。
下面说一下磁盘缓存。在 designated 初始化中,发现 cache 实例监听了如下几个通知:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deleteOldFiles)
name:UIApplicationWillTerminateNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundDeleteOldFiles)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
第一个通知应该有些多余,内存部分已经做了处理。仔细观察处理后两个通知的 selector 名大概能猜到,SDImageCache 在 app 将要被终止时和切后台时都会做一次较旧文件的清除操作。
那么,什么样的文件算是 oldFiles?
在 SDImageCache 的 designated 初始化方法中,有这样一个被初始化的变量:
_config = [[SDImageCacheConfig alloc] init];
这个变量对应的是头文件中只读的属性:
@property (nonatomic, nonnull, readonly) SDImageCacheConfig *config;
翻遍 SDImageCache.m 文件可以发现,这个属性正是用于辅助处理磁盘缓存的清理操作的,如果外部没有修改这个属性,那么 SDImageCache 就会使用配置的默认值进行处理。
// 图片下载完成后,是否解压,默认 YES
@property (assign, nonatomic) BOOL shouldDecompressImages;
// 是否禁用 iclould 备份,默认 YES
@property (assign, nonatomic) BOOL shouldDisableiCloud;
// 是否使用内存缓存,默认 YES
@property (assign, nonatomic) BOOL shouldCacheImagesInMemory;
// 磁盘缓存文件的最大有效期,单位 second
@property (assign, nonatomic) NSInteger maxCacheAge;
// 最大缓存大小,单位 byte。默认0,不会基于缓存大小对磁盘缓存进行清理。
@property (assign, nonatomic) NSUInteger maxCacheSize;
具体如何清理,就要进入 deleteOldFiles 方法以及 backgroundDeleteOldFiles 方法中查看。阅读源码发现,最终都是调用这个方法:
- (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock;
根据时间有效性清理的步骤,有这样一段注释:
// Enumerate all of the files in the cache directory. This loop has two purposes:
//
// 1. Removing files that are older than the expiration date.
// 2. Storing file attributes for the size-based cleanup pass.
根据缓存大小限制清理的步骤,有这样一段注释:
// If our remaining disk cache exceeds a configured maximum size, perform a second
// size-based cleanup pass. We delete the oldest files first.
// Target half of our maximum cache size for this cleanup pass.
在遍历缓存目录时,作者用到了 NSURL 的这个方法:
- (nullable NSDictionary *)resourceValuesForKeys:(NSArray *)keys error:(NSError **)error NS_AVAILABLE(10_6, 4_0);
针对每一个文件的 URL 链接,主要关注这几个属性:URL 是不是目录的 path、URL 对应文件最近修改时间以及文件分配的存储空间,即代码中的:NSArray
这里给大家留个思考问题,作者在计算文件大小的时候,不使用 NSURLFileAllocatedSizeKey,而是用 NSURLTotalFileAllocatedSizeKey,为什么?
小结一下,SDImageCache 缓存时效性问题的处理方案是:内存缓存在遇到内存警告时全部清除,磁盘缓存根据外部配置在客户端终止前或者切到后台时进行清理,过期的缓存文件全部删除,缓存总大小超过 maxSize时,从最大的文件开始删除直到当前缓存大小在 maxSize/2 值以下。
YYCache 中的缓存清理方案
AFNetworking 中的缓存清理方案
AFNetworking 是专业的网络请求框架,支持 NSURLConnection 和 NSURLSession 的请求方式,AFN 的下载缓存部分采用 NSURLCache,NSURLCache 是系统自动管理缓存部分,就不多做介绍,这里主要介绍下其为图片请求设计的 cache:AFAutoPurgingImageCache。
cache 组织结构
AFAutoPurgingImageCache 只包括内存缓存部分,所以实现也比较简单。内存缓存使用 NSMutableDictionary 实现。
cache 清理机制
默认最大内存缓存大小为 100M,每次内存超过最大值时,清理掉 60M 的空间。清除内存时,按照 LUR 算法,首先对内存中的图片数据按照最近访问时间进行排序,优先删除最后访问时间久远的数据。
小结:网络请求加载库主要实现对 NSURLConnection 和 NSURLSession 的封装,缓存部分主要还是使用系统的 NSURLCache 实现,重点关注下内存缓存清理时,如何像 AFNetworing 这样采用 LRU 算法进行清理,提高 cache 的命中率。