前言

YYKit 源码分析 之 YYCache

笔记

YYCache 是一个线程安全的 key-value 的缓存框架。使用 YYMemoryCache 来将对象存放在小且快的内存中;使用 YYDiskCache 来将对象存放在较慢的磁盘上。

YYCache

先来看看 YYCache 的工作流程。

创建Cache(init)

使用下面代码可以创建一个叫做vanney的新的cache。

1
YYCache *newCache = [YYCache alloc] initWithName:@"vanney"];

这句语句所做的事情如下:

  1. 物理层面
    1. 在APP的 Caches 目录下面新建一个 vanney 的子目录,这个目录用来存放所有属于vanney的cache
    2. 在vanney目录下面新建一个 manifest 的sqlite数据库,这个数据库用来记录属于vanney的所有cache的信息,数据库的每一行记录表示一个cache。每一行的信息包括:
      1. key : cache的唯一标识。manifes的主键
      2. filename : 如果cache的数据大于设置的值的话(默认是20KB),会将cache的二进制数据存放到一个文件中,这个filename就是该cache对应的文件名
      3. size : cache的大小,单位是byte
      4. inline_data : cache的二进制数据,如果cache以文件形式存放的话,这里就为空
      5. modification_time : 上一次对该cache的修改时间
      6. last_access_time : 上一次使用该cache的时间
      7. extended_data : 一些额外的数据
    3. 在vanney的目录下创建 data 的文件夹,用来存放以文件形式存在的cache
    4. 在vanney的目录下创建 trash 的文件夹,删除cache时会将data中的文件先移到trash中,然后再异步的删除该文件夹里面的文件
  2. 程序层面
    1. 创建了一个 YYDiskCache 。后面会细说
    2. 创建了一个 YYMemoryCache 。后面会细说

往Cache里面新增记录(set)

也就是所谓的 set 方法。使用下面代码可以新增一条cache记录

1
[newCache setObject:@"New Record" forKey:@"New Key"];

这里newCache做了两件事:

  1. 往YYMemoryCache里面写入这条数据
  2. 往YYDiskCache里面写入这条数据

向Cache中读取缓存记录(get)

也就是所谓的 get 方法。使用下面代码可以读取一条cache记录

1
NSString *cacheValue = [newCache objectForKey:@"New Key"];
  1. 现从memory里面获取这个cache,也就是从YYMemoryCache里面获取
  2. 如果没有发现的话,就从YYDiskCache里面获取,再将它存放到YYMemoryCache中

从Cache中删除记录(remove)

也就是 remove 方法。

1
[newCache removeObjectForKey:@"New Key"];
  1. 从YYMemoryCache中删除
  2. 从YYDiskCache中删除

YYDiskCache

YYDiskCache负责将cache存放在物理硬盘中,也就是上面所说的,存放在 Cache 目录下面的相应的cacheName下面,(例子中使用的是 vanney 目录)。存放的方式是:

  1. 使用sqlite数据库记录所有的cache的信息
  2. 大于某个限额的cache以文件形式存放,在数据库中记录下该cache对应的文件名;而对于小型的cache就直接将二进制的数据存放在数据库里面就好了

初始化(init)

使用下面的代码初始化一个YYDIskCache,

1
YYDiskCache *diskCache = [YYDiskCache alloc] iniwtWithPath:path];
  1. 有一个全局的存放YYDiskCache的 NSMapTable ,以path为 key 。这里的path就是上面所说的 vanney 目录,以YYDiskCache为 value
  2. 先从这个NSMapTable中找看是否有这个path的YYDiskCache,有就直接返回
  3. 没有的话,就开始创建。这个创建过程如下:
    1. 创建 vanney 文件夹
    2. 创建 vanney 文件夹下面的各个部件: datatrash 文件夹和 manifest 数据库
    3. 将创建的YYDiskCache以 pathYYDiskCache 的形式存放在全局的NSMapTable中
  4. 添加一些观测事件

写入缓存(set)

使用下面的代码写入Disk缓存:

1
[diskCache setObject:@"DiskCache" forKey:@"DiskCacheKey"];
  1. 先将object,这里就是 DiskCache 字符串压缩,使他变成一个 NSData
  2. 如果NSData大小没超过限制,那么将这条cache以 key : DiskCacheKey value:NSData filename:NULL 的形式写入manifest数据库
  3. 如果NSData的大小超过限制的话,先将NSData写入文件名为 md5(DiskCacheKey) 的文件中,文件存放在 data 文件夹;然后再将这条cache以 key:DiskCacheKey value:NULL filename:md5(DiskCacheKey) 的形式写入manifest数据库

读取缓存(get)

使用下面的代码读取Disk缓存:

1
NSString *diskCacheValue = [diskCache objectForKey:@"DiskCacheKey"];
  1. 进入数据库获取key为DiskCacheKey的记录,并且修改这条记录的 last_access_time
  2. 如果是将cache存放在数据库的话,获取数据库的value字段;如果是以文件形式存在的话,读取存放NSData的文件
  3. 将cache的NSData解压,还原成最初的形式,返回

删除缓存(remove)

使用下面的代码删除Disk的缓存:

1
[diskCache removeObjectForKey:@"DiskCacheKey"];
  1. 删除数据库中key为DiskCacheKey的记录
  2. 如果存在文件的话,删除

YYMemoryCache

YYMemoryCache维护着一个双向链表(YYLinkedMap),链表里面的每一个节点就是一个memory的缓存记录,表头的节点表示最近访问过的,表尾的节点很久没去访问了。每个节点是一个 YYLinkedMapNode 对象,这个对象里面有指向前后节点的指针,和包含缓存信息的 keyvalue 属,以及一些缓存的其他属性:costtime

初始化(init)

使用下面的代码初始化一个YYMemoryCache:

1
YYMemoryCache *memoryCache = [YYMemoryCache new];
  1. 新建一个YYLinkedMap
  2. 添加一些监听事件,设置一些属性等等

写入缓存(set)

使用下面的代码写入缓存:

1
[memoryCache setObject:@"Memory Cache" forKey:@"MemoryCacheKey"];
  1. 先在YYLinkedMap中寻找是否有这个key,YYLinkedMap对象中有一个dic属性,它是一个键值对,键就是每个cache的key,值就是每个cache对应的YYLinkedMapNode
  2. 如果存在的话,那就修改这个key对应的YYLinkedMapNode的value为@"Memory Cache",并且修改它的其他属性,将time属性改为现在的时间,并将这个Node放在链表的表头。
  3. 如果不存在的话,就直接生成一个YYLinkedMapNode,放在链表的表头

读取缓存(get)

使用下面的代码读取缓存:

1
NSString *cacheValue = [memoryCache objectForKey:@"MemoryCacheKey"];
  1. 从YYLinkedMap的dic中获取key为MemoryCacheKey的YYLinkedMapNode,修改node的time,并将这个node放在链表的表头
  2. 返回所需的缓存,也就是node的value属性

删除缓存(remove)

使用下面的代码删除缓存:

1
[memoryCache removeObjectForKey:@"MemoryCacheKey"];
  1. 删除YYLinkedMap中的dic里面的key为MemoryCacheKey的YYLinkedMapNode
  2. 调整双向链表

其他

关于线程安全

YYDiskCache

  1. 每个YYDiskCache有一个信号量为1的锁 _lock ,使用 dispatch_semaphore_create(1);来创建。当对这个YYDiskCache里面的数据进行操作时,比如添加或者读取缓存时,一次只允许一个操作在进行。

  2. 还有一个信号量也为1的全局的锁 _globalInstancesLock ,它用来对存储每个YYDiskCache的NSMapTable进行加锁,对这个NSMapTable进行读写的操作只能同时存在一个。

  3. 每一个YYDiskCache有一个并行的queue,这个queue用来执行读写删之后的block。因为有这样的API:

    1
    - (void)objectForKey:(NSString *)key withBlock:(void(^)(NSString *key, id<NSCoding> _Nullable object))block;

    所以这里的block就在这个queue里面执行,达到多线程操作的效果

YYMemoryCache

  1. 每一个YYMemoryCache都有一个pthread_mutex_t锁,同理,对YYMemoryCache的写入和读取缓存等操作只能同时存在一个。
  2. 有一个YYMemoryCacheGetReleaseQueue, 用来释放删除的缓存。 这里还需要细看代码。。。

关于各种限制

YYCache可以设置各种限制,比如缓存的最大数量,最大size以及缓存的生存时间等等。对于YYDiskCache和YYMemoryCache有两套不同的处理方法。当然也有相同的一点就是:他们都会定时清理缓存,在新建一个YYDiskCache或YYMemoryCache时,都会设一个定时事件,按时清理缓存。

YYDiskCache

在YYDiskCache里面,每一个cache的大小都在数据库里面的size字段里面有记录,而最近的缓存操作时间也有记录。如果操过某个限制的话,比如总容量限制或总数量限制或者存活时间限制,那么会删掉最长时间没有去使用的缓存。这个操作很简单,只需要对数据库的各个缓存按时间排序就行了。

YYMemoryCache

在YYMemoryCache里面,使用双向链表来记录每个缓存的使用情况。表头的表示刚刚使用过,而表尾的则很久没使用了。每个YYLinkedMapNode都有记录自己的time和cost,而YYLinkedMap则有记录总的cost和总的缓存数量。当超过某个限制的话,就会从表尾开始删除缓存,直到达到要求

参考