YYCache是著名iOS框架YYKit的一个组件是之一, 这里有作者对这个轮子的介绍, 同时有作者对主流的几个缓存框架的性能对比. 我们以YYCache为入口, 逐个分析每个api, 学习缓存如何设计.本文的思路, 同样适用于SDWebImage的缓存策略, 只是有些细节不太一样
下面是大神对缓存策略的基本描述是这样的
缓存通常一个缓存是由内存缓存和磁盘缓存组成,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储
用来调用YYMemoryCache和YYDiskCache的外层接口, 我们直接使用的类, 两者在使用过程中对缓存开销, 缓存时长和数量都制定了一定的策略;
内存缓存负责处理容量小, 相对高速的数据;并提供了自动和手动两张删除方式且是线程安全的, 且对异步回调提供了支持
磁盘缓存负责处理容量大, 相对低速的. 并提供了自动和手动两张删除方式且是线程安全的, 且对异步回调提供了支持. 同时, 它可以根据所存储的内容自动的选择合适的数据类型(文件/sqlite)来获取最高的性能;
YYCache是线程安全的, 其中YYMemoryCache将对象负责内存缓存, YYDiskCache负责磁盘缓存; 两种存储载体有明显的区别, 内存存取速度快且容量小, 磁盘存取速度慢但容量大!
YYCache
is a thread safe key-value cache.
这三个api是逐级调用的, 是实例方法, 不论调用哪个, 都会创建三个实例对象, 分别是YYCache, YYMemoryCache, YYDiskCache.
- (instancetype) init;
// 名称不要重复, 否则缓存不稳定
- (nullable instancetype)initWithName:(NSString *)name;
- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
这两个api是构造器方法创建实例; 和上面的实例方法不同的是, 这两个方法只创建了YYCache实例
+ (nullable instancetype)cacheWithName:(NSString *)name;
+ (nullable instancetype)cacheWithPath:(NSString *)path;
// 若在内存缓存中查找到对应的缓存后就会返回YES;返回结果在当前线程
- (BOOL)containsObjectForKey:(NSString *)key {
return [_memoryCache containsObjectForKey:key] || [_diskCache containsObjectForKey:key];
}
// 使用block方式回调在后台队列
- (void)containsObjectForKey:(NSString *)key withBlock:(void (^)(NSString *key, BOOL contains))block {
if (!block) return;
if ([_memoryCache containsObjectForKey:key]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
block(key, YES);
});
} else {
[_diskCache containsObjectForKey:key withBlock:block];
}
}
1.1 检查内存
读取内存缓存的策略, 我们可以看到, 在读取数据之前, 用pthread_mutex_lock获取到了互斥锁, 当前线程被锁定, 直到获取到结果. 也就是说, 此方法调用后会阻塞线程直到文件读取完成. 并且需要注意的是, 这里作者使用了LRU 淘汰算法提升查找速度
- (BOOL)containsObjectForKey:(id)key {
if (!key) return NO;
pthread_mutex_lock(&_lock);
BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key));
pthread_mutex_unlock(&_lock);
return contains;
}
1.2 检查磁盘
同样的, 在读取磁盘缓存时, 也使用锁操作保证安全, 即此方法也会阻塞当前线程, 直至文件读取完成.与查找内存缓存不同的是, 对磁盘缓存查找时使用了信号量来设置锁! 当信号量等于1时, 可以被当作锁来保证线程安全. 与自旋锁OSSpinLock不同的是, 使用信号量做锁不会消耗CPU资源(OSSpinLock会产生忙等), 适合用于不等待的存储过程.
// 信号量
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
self->_lock: dispatch_semaphore_t _lock;
- (BOOL)containsObjectForKey:(NSString *)key {
if (!key) return NO;
Lock();
BOOL contains = [_kv itemExistsForKey:key];
Unlock();
return contains;
}
// 检查是否有指定的key
// 拿key直接去sqlite中查询
- (BOOL)itemExistsForKey:(NSString *)key {
if (key.length == 0) return NO;
return [self _dbGetItemCountWithKey:key] > 0;
}
同样的, 可以使用key取缓存值, 原理和上述1中大致相同
// 直接返回值
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
// block返回
-(void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id<NSCoding> object))block;
需要注意的是, 如果我们在内存缓存中没有查找到对应的值, 但是在磁盘缓存中查找到值了, 会同时把该值缓存到内存缓存中去
object = [_diskCache objectForKey:key];
f (object) {
// 缓存内存
[_memoryCache setObject:object forKey:key];
}
同样的, 内存缓存查找时使用了LRU 淘汰算法对双向链表进行查找(_YYLinkedMapNode是一个双向节点类)