YYCache 的基本使用
- (void)synchronizingStorageForCache {
NSString *value1 = @"朽木自雕";
NSString *key1 = @"key1";
NSArray *value2 = @[@"1", @"2", @"3", @"4", @"5"];
NSString *key2 = @"key2";
// 创建缓存对象
YYCache *cache = [[YYCache alloc]initWithName:@"cacheTest"];
[cache setObject:value1 forKey:key1];
[cache setObject:value2 forKey:key2];
// 判断对象是否存在
if ([cache containsObjectForKey:key1]) {
// 取出缓存
id value = [cache objectForKey:key1];
NSLog(@"%@", value);
}
// 判断对象是否存在
if ([cache containsObjectForKey:key2]) {
// 取出缓存
id value = [cache objectForKey:key2];
NSLog(@"%@", value);
}
}
其他的 API 的使用很简单,不在这里逼逼叨逼逼叨的
类图
YYCache 类图类说明
YYCache
YYCache 是提供用用户使用的对象,内部对 YYMemoryCache 和 YYDiskCache 功能的整合封装。为 YYMemoryCache 提供了多线程功能,而 YYDiskCache 对象本身内部封装了异步读写功能。封装的功能包括:
初始化缓存对象
- (nullable instancetype)initWithName:(NSString *)name;
- (nullable instancetype)initWithPath:(NSString *)path;
+ (nullable instancetype)cacheWithName:(NSString *)name;
+ (nullable instancetype)cacheWithPath:(NSString *)path;
这几个方法功能是一直的,最终方法都是会掉“initWithPath:”
判断是否存在某条缓存
- (BOOL)containsObjectForKey:(NSString *)key;
- (void)containsObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, BOOL contains))block;
- 方法一 实现:先去内存缓存中查找,如果没有查找就去磁盘缓存中找,找到就返回 YES
- 方法二 实现:同步在在内存中查找缓存,如果找到了,使用异步返回,如果没有找到,就调用磁盘缓存的异步查找 API,并把查找结果异步回调
通过 key 取出缓存
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
- (void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id<NSCoding> object))block;
- 方法一 实现:先去内存缓存中查找,如果没有查找就去磁盘缓存中找,找到就返回结果。如果是在磁盘缓存中找到的缓存,先把缓存存入内存缓存中,然后再返回结果
- 方法二 实现:同步在在内存中查找缓存,如果找到了,使用异步返回,如果没有找到,就调用磁盘缓存的异步查找 API,如果是在磁盘缓存中找到的缓存,先把缓存存入内存缓存中,然后再返回结果
增、改--缓存对象
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(nullable void(^)(void))block;
- 方法一 实现逻辑:首先调用内存缓存设置缓存API(setObject:forKey:),然后调用磁盘缓存设置API(setObject:forKey:)
- 方法二 实现逻辑:首先调用内存缓存设置缓存API(setObject:forKey:),然后调用磁盘缓存异步设置API(setObject:forKey:)
删除 key 对应的缓存记录
- (void)removeObjectForKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key))block;
- 方法一 实现逻辑:首先调用内存缓存删除 API(removeObjectForKey:),然后调用磁盘缓存删除API(removeObjectForKey:)
- 方法二 实现逻辑:首先调用内存缓存删除 API(removeObjectForKey:),然后调用磁盘缓存异步删除API(removeObjectForKey:withBlock:)
清空所有缓存记录
- (void)removeAllObjects;
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
- (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
endBlock:(nullable void(^)(BOOL error))end;
- 方法一 实现:首先调用内存缓存清空 API(removeAllObjects),然后调用磁盘缓存清空 API(removeAllObjects)
- 方法二 实现:首先调用内存缓存清空 API(removeAllObjects),然后调用磁盘缓存异步清空 API(removeAllObjects)
- 方法三 实现:首先调用内存缓存清空 API(removeAllObjects),然后调用磁盘缓存带删除进度的清空缓存 API (removeAllObjectsWithProgressBlock:endBlock:)
YYMemoryCache
YYMemoryCache 内部有一个储存对象,实现分为两部分:
- 第一部分,淘汰算法,这里使用一个双向链表,每个节点为 _YYLinkedMapNode 类对象,通过访问最后访问时间来对链表进行排列,最新访问的缓存节点放在链表的头部,淘汰算法只需要将链表未尾节点移除即可
- 第二部分,查找算法,这里使用的是 CFMutableDictionaryRef 散列表进行存储
YYMemoryCache 的多线程安全是 使用 pthread_mutex_t(互斥锁) 来完成
YYDiskCache
- YYDiskCache 是对 YYKVStorage 封装了异步访问 API,多线程安全使用 dispatch_semaphore_t(二元信号量) 来完成
- YYDiskCache 中同一功能方法,同步和异步的区别:异步方法的实现其实质上就是只是异步的调用了同步方法,所以我在下面方法的介绍的时候只介绍同步方法的实现
- YYDiskCache 自动清理缓存机制,内部有一个这样的 方法“_trimRecursively” 在类对象实例化的时候会被调用,仔细看一下源码的实现:
// 自动检查缓存
- (void)_trimRecursively {
__weak typeof(self) _self = self;
// 延迟执行
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)_trimInBackground {
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
if (!self) return;
// 没什么好解释,磁盘数据的读写必须加锁,保证多线程读写安全
Lock();
[self _trimToCost:self.costLimit];
[self _trimToCount:self.countLimit];
[self _trimToAge:self.ageLimit];
[self _trimToFreeDiskSpace:self.freeDiskSpaceLimit];
Unlock();
});
}
注释都写好了,不用再解释吧。
YYDiskCache 判断是否存在某条缓存
- (BOOL)containsObjectForKey:(NSString *)key {
// 判断参数的合法性
if (!key) return NO;
//上锁,没什么好说的,为了安全起见
Lock();
// 从数据库中查询
BOOL contains = [_kv itemExistsForKey:key];
Unlock();
//返回结果
return contains;
}
同时还存在一个功能相同,但是异步的方法(containsObjectForKey:withBlock:),这个异步方法的实现就是异步调用了方法(containsObjectForKey:),不妨看看源码
- (void)containsObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key, BOOL contains))block {
// 判断参数的合法性
if (!block) return;
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
BOOL contains = [self containsObjectForKey:key];
block(key, contains);
});
}
YYDiskCache 获取某条缓存(objectForKey:)
这个方法的返回的是一个 遵守 NSCoding 协议的类对象,对象的解码可以 customUnarchiveBlock 属性,自定义解码。默认内部使用 NSCoding 归档工具解码。
- (id<NSCoding>)objectForKey:(NSString *)key {
// 判断参数的合法性
if (!key) return nil;
//上锁,没什么好说的,为了安全起见
Lock();
YYKVStorageItem *item = [_kv getItemForKey:key];
Unlock();
if (!item.value) return nil;
id object = nil;
if (_customUnarchiveBlock) {
// 外部解压
object = _customUnarchiveBlock(item.value);
// 自己解压
} else {
@try {
object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
}
@catch (NSException *exception) {
// nothing to do...
}
}
// 扩展参数。为了做到速度的极致,这里也是拼了
if (object && item.extendedData) {
[YYDiskCache setExtendedData:item.extendedData toObject:object];
}
// 返回结果
return object;
}
YYDiskCache 写入缓存
同步写入方法的实现(setObject: forKey:)
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
// 判断 key 的合法性
if (!key) return;
// 当 object 为 nil 时,改存入为删除
if (!object) {
[self removeObjectForKey:key];
return;
}
// 获取到扩展数据
NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];
NSData *value = nil;
// 外部压缩
if (_customArchiveBlock) {
value = _customArchiveBlock(object);
} else {
@try {
// 内部压缩
value = [NSKeyedArchiver archivedDataWithRootObject:object];
}
@catch (NSException *exception) {
// nothing to do...
}
}
// 如果压缩的值为空,就不用存数据了
if (!value) return;
NSString *filename = nil;
// 判断缓存存储方式,如果不是 数据库存储,则进入条件
if (_kv.type != YYKVStorageTypeSQLite) {
// 如果值的长度大于临界值,则以文件的形式进行存储
if (value.length > _inlineThreshold) {
// 获取文件名
filename = [self _filenameForKey:key];
}
}
// 数据写入本地磁盘
// 上锁,没什么好说的,为了安全起见
Lock();
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}
对应的异步写入方法(setObject: forKey: withBlock:),实现的套路异步调用同步写入方法(setObject: forKey:)
YYDiskCache 删除记录的逻辑
- 删除一条记录(removeObjectForKey:),内部实现是调用 YYKVStorage 的 removeObjectForKey:方法,并在调用的前后加上了锁。而对应的异步方法(removeObjectForKey: withBlock:),使用异步线程调用了同步方法(removeObjectForKey:)
- 删除所有记录
- 不带进度删除所有记录(removeAllObjects),内部实现是调用 YYKVStorage 的 removeAllItems方法,并在调用的前后加上了锁。而对应的异步方法(removeAllObjectsWithBlock:),实质上使用异步线程调用了同步方法(removeAllObjects)
- 带删除进度地删除所有记录(removeAllObjectsWithProgressBlock:endBlock:),内部实现是异步调用 YYKVStorage 的 removeAllItemsWithProgressBlock:endBlock: 方法,并在这个方法的调用前后加了锁。
YYDiskCache 磁盘清理逻辑
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;
在几种磁盘清理实现方式基本一致,都是调用 YYKVStorage 对应的清理方法
YYKVStorage
YYKVStorage 为磁盘缓存的核心类,提供给了外部数据库(sqlite3)存储以及系统文件(系统文件管理类 NSFileManager)存储方式,在使用文件存储时是配合数据库存储的,文件的描述信息存在数据库中。
- 磁盘的淘汰算法实现,淘汰算法使用的是最后访问时间来进行淘汰的,每当数据库中某条记录被访问到后,这条记录就会更新最后访问时间为当前时间,而在删除过期过期缓存时,只需要根据确定过期时间即可。
- 磁盘的查找算法实现,前面说过,磁盘存储分为两种,一种是数据库,每条缓存对应数据库表中的一条记录,另一种是系统文件,每条缓存对应系统文件中的一个文件。在通过 key 查找某个缓存时,首先去数据库中找到 key 对应的记录,把去出来的数据转换成 YYKVStorageItem 类对象,判断 filename 字段是否为空,如果为空,说明数据存在数据空,则直接使用这个对象的 value 字段,如果不为空,则说明缓存实体数据存在系统文件中,通过 filename 去系统文件中找到对应文件,并赋值给这个对象的 value 字段。
总结
阅读 YYCache 源码,从开始到读完一共花了三天的样子,真心感叹 大神写得代码 跟我这个凡人的差距。在阅读源码的过程中有很多只是点是自己没有了解过的,得去问度娘查资料,所以阅读得比较慢,比如“pthread_mutex_t 特性”、“sqlite3 特性”、“sql 语句”、“数据库的 wal 模式”、“数据库 synchronous 的模式选择与区别”、“CFMutableDictionaryRef”等等。在源码很多的实现简直不要太精妙,希望下次能抽出时间去看看 YYText 的源码,如果我能有这两把刷子,此生足以😂😄。
网友评论