缓存是由内存缓存和磁盘缓存组成,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储。
这里通过从YYCache入手,再到内存缓存YYMemoryCache、磁盘缓存YYDiskCache,一层一层地剥开它的心。
YYCache
管理和协调YYMemoryCache和YYDiskCache
YYCache存储、查询、删除的每个功能都提供了两种API,同步缓存和异步缓存。因磁盘大数据缓存可能会耗时,涉及到磁盘缓存的使用了GCD异步缓存,并提供了回调,可以在回调中做些自定义操作。
存储数据
同时存储到内存缓存和磁盘
//同步缓存
- (void)setObject:(id)object forKey:(NSString *)key {
[_memoryCache setObject:object forKey:key];
[_diskCache setObject:object forKey:key];
}
//磁盘部分使用异步缓存
- (void)setObject:(id)object forKey:(NSString *)key withBlock:(void (^)(void))block {
[_memoryCache setObject:object forKey:key];
[_diskCache setObject:object forKey:key withBlock:block];
}
查询缓存数据
查询缓存数据思路:优先读取内存缓存的数据;内存中读取不到再去磁盘中读取,如果能读取到,同时把数据存储到内存缓存中。这样设计主要是为了提高下次查询的速度。
//查询
- (id)objectForKey:(NSString *)key {
//优先查找内存缓存
id object = [_memoryCache objectForKey:key];
if (!object) {
//其次,查找磁盘缓存
object = [_diskCache objectForKey:key];
if (object) {
//磁盘缓存能命中的情况下,缓存到内存
[_memoryCache setObject:object forKey:key];
}
}
return object;
}
//异步查询
- (void)objectForKey:(NSString *)key withBlock:(void (^)(NSString *key, id object))block {
//处理边界条件
if (!block) return;
//优先查找内存缓存
id object = [_memoryCache objectForKey:key];
if (object) {
//
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
block(key, object);
});
} else {
[_diskCache objectForKey:key withBlock:^(NSString *key, id object) {
if (object && ![_memoryCache objectForKey:key]) {
[_memoryCache setObject:object forKey:key];
}
block(key, object);
}];
}
}
删除数据
将内存缓存和磁盘缓存都删掉。
- (void)removeObjectForKey:(NSString *)key {
[_memoryCache removeObjectForKey:key];
[_diskCache removeObjectForKey:key];
}
- (void)removeObjectForKey:(NSString *)key withBlock:(void (^)(NSString *key))block {
[_memoryCache removeObjectForKey:key];
[_diskCache removeObjectForKey:key withBlock:block];
}
YYMemoryCache
NSCache 是苹果提供的一个简单的内存缓存,它有着和 NSDictionary 类似的 API,不同点是它是线程安全的,并且不会 retain key。NSCache 底层并没有用 NSDictionary 等已有的类,而是直接调用了 libcache.dylib,其中线程安全是由 pthread_mutex 完成的。另外,它的性能和 key 的相似度有关,如果有大量相似的 key ,NSCache 的存取性能会下降得非常厉害,大量的时间被消耗在 CFStringEqual() 上。
NSCache
NSCache是一个可变的集合类型,用于临时存放键值对,当资源不足时会被移除。
NSCache的主要特点:
- 线程安全
- 内存超出阈值时,会自动清理
- Key-Value数据结构,类似字典的使用
- 可以限制缓存对象数量和总的缓存空间大小
NSCache的OC源码没开源,可以通过阅读GNU源码NSCache.h和NSCache.m
来了解它的实现。
它的实现是基于苹果封装的NS系列类的,如:NSMapTable,NSString,NSEnumerator,NSMutableArray等。
缓存淘汰策略通过LRU算法来实现,用一个可变数组保存所有的缓存对象,然后根据对象的平均访问次数 * 0.2 + 1 这个限制来淘汰所有低于这个访问次数的对象,一直释放直到有足够的所需空间。因此,它是通过释放访问次数小的对象来实现淘汰策略的。线程安全据YY大神说是使用了互斥锁pthread_mutex来保障的,但在GNU源码并没有看到。
PS:个人觉得这里可以通过LRU-K算法解决可能导致的缓存污染问题,关于LRU-K算法可以看下 你与解决“缓存污染”只差这篇文章的距离
Swift下的NSCache是通过一个双向链表来实现的,链表里是按缓存对象大小cost进行排序的,优先驱逐占用缓存cost较小的对象。线程安全是通过NSLock来保障。
NSCache.swift源码
YYMemoryCache
YYMemoryCache优化了同步访问的性能,用互斥锁pthread_mutex来保证线程安全。另外,缓存内部用双向链表和 NSDictionary 实现了 LRU 淘汰算法。
的特点主要是使用了LRU-K算法处理内存缓存,同时尽可能避免了缓存污染问题。
YYMemoryCache中使用CF系列的类,相比NSCache中直接使用NS系列的类性能上会好一些。同时也使用性能较好的互斥锁pthread_mutex。
//节点
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
//Map
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // do not set object directly
NSUInteger _totalCost;
NSUInteger _totalCount;
_YYLinkedMapNode *_head; // MRU, do not change it directly
_YYLinkedMapNode *_tail; // LRU, do not change it directly
BOOL _releaseOnMainThread;
BOOL _releaseAsynchronously;
}
通过源码对象可以大概看出,其实是用双向链表+CFDictionary实现的LRU缓存算法。
查询内存缓存
- (id)objectForKey:(id)key {
if (!key) return nil;
//加锁保证线程安全
pthread_mutex_lock(&_lock);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
if (node) {
//存在的数据,通过LRU算法将它移到头结点
node->_time = CACurrentMediaTime();
[_lru bringNodeToHead:node];
}
//解锁
pthread_mutex_unlock(&_lock);
return node ? node->_value : nil;
}
修改内存缓存数据
根据
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
//处理特殊情况
if (!key) return;
if (!object) {
[self removeObjectForKey:key];
return;
}
pthread_mutex_lock(&_lock);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
NSTimeInterval now = CACurrentMediaTime();
if (node) {
//对已有节点操作,并通过LRU算法放到头结点
} else {
//新增节点,并通过LRU算法放到头结点
}
//异步处理总的缓存大小大于缓存限制的情况
if (_lru->_totalCost > _costLimit) {
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}
//处理缓存对象个数大于限制的情况
if (_lru->_totalCount > _countLimit) {
//删除一次链表尾部节点,因为只setObject一次
_YYLinkedMapNode *node = [_lru removeTailNode];
if (_lru->_releaseAsynchronously) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[node class]; //hold and release in queue
});
} else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
[node class]; //hold and release in queue
});
}
}
pthread_mutex_unlock(&_lock);
}
缓存清理策略
- (instancetype)init {
self = super.init;
pthread_mutex_init(&_lock, NULL);
_lru = [_YYLinkedMap new];
_queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);
//初始化配置
_countLimit = NSUIntegerMax;
_costLimit = NSUIntegerMax;
_ageLimit = DBL_MAX;
_autoTrimInterval = 5.0;
//默认情况下
//收到内存警告就清除所有缓存
_shouldRemoveAllObjectsOnMemoryWarning = YES;
//收到app进入后台通知就清除所有缓存
_shouldRemoveAllObjectsWhenEnteringBackground = YES;
//监听内存警告、进入后台的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];
//初始化时自动递归清除缓存
[self _trimRecursively];
return self;
}
递归清除缓存
- (void)_trimRecursively {
__weak typeof(self) _self = self;
//延迟_autoTrimInterval=5s后并行清理缓存,不影响主线程
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
__strong typeof(_self) self = _self;
if (!self) return;
[self _trimInBackground];
[self _trimRecursively];
});
}
同时也提供了根据缓存数量限制、缓存大小、时间节点来手动清除缓存。
//缓存数量限制
- (void)trimToCount:(NSUInteger)count;
//缓存大小
- (void)trimToCost:(NSUInteger)cost;
//缓存时间节点
- (void)trimToAge:(NSTimeInterval)age;
YYMemoryCache和NSDictionary、NSCache
通过YYCache作者在单线程的 Memory Cache 性能基准测试,读写性能上,YYMemoryCache 仅次于 NSDictionary,优于NSCache,即NSDictionary>YYMemoryCache>NSCache。
YYDiskCache
YYDiskCache和YYMemoryCache的实现思路上有些类似,都是使用LRU算法来处理缓存,在初始化时延迟清理下缓存,都具有增删改查的特点,同时也支持缓存数量限制countLimit、缓存大小限制costLimit、时间限制ageLimit等。
由于对磁盘操作会是耗时的,YYDiskCache的每个功能api设计上都加了个异步处理的。
- (nullable id)objectForKey:(NSString *)key;
- (void)objectForKey:(NSString *)key withBlock:(void(^)(NSString *key, id _Nullable object))block;
和YYMemoryCache不同,这里使用的是dispatch_semaphore信号量来保证线程安全。其中原因是信号量在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。它的优势在于等待时不会消耗 CPU 资源。它比较合适磁盘缓存。
YY作者评测过SQLite读写的性能
iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。这和 SQLite 官网的描述基本一致。另外,直接从官网下载最新的 SQLite 源码编译,会比 iOS 系统自带的 sqlite3.dylib 性能要高很多。基于 SQLite 的这种表现,磁盘缓存最好是把 SQLite 和文件存储结合起来:key-value 元数据保存在 SQLite 中,而 value 数据则根据大小不同选择 SQLite 或文件存储。
因此,YYDiskCache 采用了SQLite +文件的存储方式。在存取小数据 (NSNumber) 时,YYDiskCache 的性能远远高出基于文件存储的库;而较大数据的存取性能则比较接近了。
另外,YYDiskCache还支持释放到指定的剩余空间
- (void)_trimToFreeDiskSpace:(NSUInteger)targetFreeDiskSpace {
if (targetFreeDiskSpace == 0) return;
int64_t totalBytes = [_kv getItemsSize];
if (totalBytes <= 0) return;
int64_t diskFreeBytes = _YYDiskSpaceFree();
if (diskFreeBytes < 0) return;
int64_t needTrimBytes = targetFreeDiskSpace - diskFreeBytes;
if (needTrimBytes <= 0) return;
int64_t costLimit = totalBytes - needTrimBytes;
if (costLimit < 0) costLimit = 0;
[self _trimToCost:(int)costLimit];
}
YYKVStorage
YYKVStorage是YYDiskCache的核心类,它主要封装了 SQLite 数据库的操作和文件存储操作。YYKVStorageItem是保存了磁盘缓存的基本信息。
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key; ///< key
@property (nonatomic, strong) NSData *value; ///< value
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
@property (nonatomic) int size; ///< value's size in bytes
@property (nonatomic) int modTime; ///< modification unix timestamp
@property (nonatomic) int accessTime; ///< last access unix timestamp
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end
YYKVStorage内主要是对YYKVStorageItem的操作来实现进行缓存管理。提供了三种情况的缓存处理针对文件,SQLite和混合的方式。文件操作使用系统的NSFileManager。
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
/// The `value` is stored as a file in file system.
YYKVStorageTypeFile = 0,
/// The `value` is stored in sqlite with blob type.
YYKVStorageTypeSQLite = 1,
/// The `value` is stored in file system or sqlite based on your choice.
YYKVStorageTypeMixed = 2,
};
YYDiskCache中对20KB以上的使用YYKVStorageTypeMixed。大小为NSUIntegerMax的使用YYKVStorageTypeSQLite。默认情况下YYKVStorageTypeFile。
总结:YYCache在性能上使用C语言,少用NS系列类,同时锁的使用也是使用性能最高的pthread_和信号量,保障了缓存实现的性能。同时使用LRU算法提高了缓存的命中率,大大提高了查找效率。但这里我个人觉得可以使用LRU-K算法来优化,避免偶发性的操作导致的缓存污染问题,比如一个A页面跳转到B页面,又跳回来等情况。
如有不对的地方可以指出留言哈,若对您有帮助,可以考虑点个赞哈
参考资料:
YYCache 设计思路
你与解决“缓存污染”只差这篇文章的距离