美文网首页
YYCache 阅读总结

YYCache 阅读总结

作者: 朽木自雕也 | 来源:发表于2019-02-22 12:04 被阅读3次

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;
  1. 方法一 实现:先去内存缓存中查找,如果没有查找就去磁盘缓存中找,找到就返回 YES
  2. 方法二 实现:同步在在内存中查找缓存,如果找到了,使用异步返回,如果没有找到,就调用磁盘缓存的异步查找 API,并把查找结果异步回调
通过 key 取出缓存
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
- (void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id<NSCoding> object))block;
  1. 方法一 实现:先去内存缓存中查找,如果没有查找就去磁盘缓存中找,找到就返回结果。如果是在磁盘缓存中找到的缓存,先把缓存存入内存缓存中,然后再返回结果
  2. 方法二 实现:同步在在内存中查找缓存,如果找到了,使用异步返回,如果没有找到,就调用磁盘缓存的异步查找 API,如果是在磁盘缓存中找到的缓存,先把缓存存入内存缓存中,然后再返回结果
增、改--缓存对象
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(nullable void(^)(void))block;
  1. 方法一 实现逻辑:首先调用内存缓存设置缓存API(setObject:forKey:),然后调用磁盘缓存设置API(setObject:forKey:)
  2. 方法二 实现逻辑:首先调用内存缓存设置缓存API(setObject:forKey:),然后调用磁盘缓存异步设置API(setObject:forKey:)
删除 key 对应的缓存记录
- (void)removeObjectForKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key))block;
  1. 方法一 实现逻辑:首先调用内存缓存删除 API(removeObjectForKey:),然后调用磁盘缓存删除API(removeObjectForKey:)
  2. 方法二 实现逻辑:首先调用内存缓存删除 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;
  1. 方法一 实现:首先调用内存缓存清空 API(removeAllObjects),然后调用磁盘缓存清空 API(removeAllObjects)
  2. 方法二 实现:首先调用内存缓存清空 API(removeAllObjects),然后调用磁盘缓存异步清空 API(removeAllObjects)
  3. 方法三 实现:首先调用内存缓存清空 API(removeAllObjects),然后调用磁盘缓存带删除进度的清空缓存 API (removeAllObjectsWithProgressBlock:endBlock:)

YYMemoryCache

YYMemoryCache 内部有一个储存对象,实现分为两部分:

  1. 第一部分,淘汰算法,这里使用一个双向链表,每个节点为 _YYLinkedMapNode 类对象,通过访问最后访问时间来对链表进行排列,最新访问的缓存节点放在链表的头部,淘汰算法只需要将链表未尾节点移除即可
  2. 第二部分,查找算法,这里使用的是 CFMutableDictionaryRef 散列表进行存储

YYMemoryCache 的多线程安全是 使用 pthread_mutex_t(互斥锁) 来完成

YYDiskCache

  1. YYDiskCache 是对 YYKVStorage 封装了异步访问 API,多线程安全使用 dispatch_semaphore_t(二元信号量) 来完成
  2. YYDiskCache 中同一功能方法,同步和异步的区别:异步方法的实现其实质上就是只是异步的调用了同步方法,所以我在下面方法的介绍的时候只介绍同步方法的实现
  3. 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 删除记录的逻辑
  1. 删除一条记录(removeObjectForKey:),内部实现是调用 YYKVStorage 的 removeObjectForKey:方法,并在调用的前后加上了锁。而对应的异步方法(removeObjectForKey: withBlock:),使用异步线程调用了同步方法(removeObjectForKey:)
  2. 删除所有记录
    1. 不带进度删除所有记录(removeAllObjects),内部实现是调用 YYKVStorage 的 removeAllItems方法,并在调用的前后加上了锁。而对应的异步方法(removeAllObjectsWithBlock:),实质上使用异步线程调用了同步方法(removeAllObjects)
    2. 带删除进度地删除所有记录(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 的源码,如果我能有这两把刷子,此生足以😂😄。

源码笔记

相关文章

  • YYCache 阅读总结

    YYCache 的基本使用 其他的 API 的使用很简单,不在这里逼逼叨逼逼叨的 类图 类说明 YYCache Y...

  • YYCache源码阅读一YYMemoryCache

    YYCache源码阅读一(YYMemoryCache) YYCache:高性能 iOS 缓存框架。 YYCache...

  • YYCache源码阅读总结

    为什么要有缓存?   使用缓存的2个主要原因: 降低延迟:缓存离客户端更近,因此,从缓存请求内容比从源服务器所用时...

  • iOS开发小贴:缓存

    YYCache学习篇 首先,YYCache GitHub地址 YYCache源码分析(一)YYCache源码分析(...

  • YYCache源码总结

    YYCache简介 YYCache由YYMemoryCache(高速内存缓存)和YYDiskCache(低速磁盘缓...

  • 学习YYCache总结

    仅仅是自己的理解,如有错误的地方,请指正 高山仰止,景行行止 YYMemoryCache 该类主要处理将数据存放在...

  • 源码阅读:YYCache

    前言 因项目需要加入了大量的数据缓存功能,在优化项目本地缓存组件的之前。研究阅读了一下YYCache这个国内最优秀...

  • YYCache源码阅读

    模块 YYCache 这个类是底层YYMemoryCache和YYDiskCa...

  • 源码阅读——YYCache

    前言 缓存在iOS开发中很常用,大到网络请求的缓存,小到各种属性的缓存。比如用户发送朋友圈时,写了很多内容,因为某...

  • YYKIT-YYCache

    一、YYCache的组成 YYCache由YYCache、YYDiskCache、YYMemoryCache和YY...

网友评论

      本文标题:YYCache 阅读总结

      本文链接:https://www.haomeiwen.com/subject/cyzdyqtx.html