看YYKit的源码
前言
YYKit 源码分析 之 YYCache
笔记
YYCache 是一个线程安全的 key-value 的缓存框架。使用 YYMemoryCache 来将对象存放在小且快的内存中;使用 YYDiskCache 来将对象存放在较慢的磁盘上。
YYCache
先来看看 YYCache 的工作流程。
创建Cache(init)
使用下面代码可以创建一个叫做vanney的新的cache。
1 | YYCache *newCache = [YYCache alloc] initWithName:@"vanney"]; |
这句语句所做的事情如下:
- 物理层面
- 在APP的 Caches 目录下面新建一个 vanney 的子目录,这个目录用来存放所有属于vanney的cache
- 在vanney目录下面新建一个 manifest 的sqlite数据库,这个数据库用来记录属于vanney的所有cache的信息,数据库的每一行记录表示一个cache。每一行的信息包括:
- key : cache的唯一标识。manifes的主键
- filename : 如果cache的数据大于设置的值的话(默认是20KB),会将cache的二进制数据存放到一个文件中,这个filename就是该cache对应的文件名
- size : cache的大小,单位是byte
- inline_data : cache的二进制数据,如果cache以文件形式存放的话,这里就为空
- modification_time : 上一次对该cache的修改时间
- last_access_time : 上一次使用该cache的时间
- extended_data : 一些额外的数据
- 在vanney的目录下创建 data 的文件夹,用来存放以文件形式存在的cache
- 在vanney的目录下创建 trash 的文件夹,删除cache时会将data中的文件先移到trash中,然后再异步的删除该文件夹里面的文件
- 程序层面
- 创建了一个 YYDiskCache 。后面会细说
- 创建了一个 YYMemoryCache 。后面会细说
往Cache里面新增记录(set)
也就是所谓的 set 方法。使用下面代码可以新增一条cache记录
1 | [newCache setObject:@"New Record" forKey:@"New Key"]; |
这里newCache做了两件事:
- 往YYMemoryCache里面写入这条数据
- 往YYDiskCache里面写入这条数据
向Cache中读取缓存记录(get)
也就是所谓的 get 方法。使用下面代码可以读取一条cache记录
1 | NSString *cacheValue = [newCache objectForKey:@"New Key"]; |
- 现从memory里面获取这个cache,也就是从YYMemoryCache里面获取
- 如果没有发现的话,就从YYDiskCache里面获取,再将它存放到YYMemoryCache中
从Cache中删除记录(remove)
也就是 remove 方法。
1 | [newCache removeObjectForKey:@"New Key"]; |
- 从YYMemoryCache中删除
- 从YYDiskCache中删除
YYDiskCache
YYDiskCache负责将cache存放在物理硬盘中,也就是上面所说的,存放在 Cache 目录下面的相应的cacheName下面,(例子中使用的是 vanney 目录)。存放的方式是:
- 使用sqlite数据库记录所有的cache的信息
- 大于某个限额的cache以文件形式存放,在数据库中记录下该cache对应的文件名;而对于小型的cache就直接将二进制的数据存放在数据库里面就好了
初始化(init)
使用下面的代码初始化一个YYDIskCache,
1 | YYDiskCache *diskCache = [YYDiskCache alloc] iniwtWithPath:path]; |
- 有一个全局的存放YYDiskCache的 NSMapTable ,以path为 key 。这里的path就是上面所说的 vanney 目录,以YYDiskCache为 value 。
- 先从这个NSMapTable中找看是否有这个path的YYDiskCache,有就直接返回
- 没有的话,就开始创建。这个创建过程如下:
- 创建 vanney 文件夹
- 创建 vanney 文件夹下面的各个部件: data ,trash 文件夹和 manifest 数据库
- 将创建的YYDiskCache以 path :YYDiskCache 的形式存放在全局的NSMapTable中
- 添加一些观测事件
写入缓存(set)
使用下面的代码写入Disk缓存:
1 | [diskCache setObject:@"DiskCache" forKey:@"DiskCacheKey"]; |
- 先将object,这里就是 DiskCache 字符串压缩,使他变成一个 NSData
- 如果NSData大小没超过限制,那么将这条cache以 key : DiskCacheKey value:NSData filename:NULL 的形式写入manifest数据库
- 如果NSData的大小超过限制的话,先将NSData写入文件名为 md5(DiskCacheKey) 的文件中,文件存放在 data 文件夹;然后再将这条cache以 key:DiskCacheKey value:NULL filename:md5(DiskCacheKey) 的形式写入manifest数据库
读取缓存(get)
使用下面的代码读取Disk缓存:
1 | NSString *diskCacheValue = [diskCache objectForKey:@"DiskCacheKey"]; |
- 进入数据库获取key为DiskCacheKey的记录,并且修改这条记录的 last_access_time
- 如果是将cache存放在数据库的话,获取数据库的value字段;如果是以文件形式存在的话,读取存放NSData的文件
- 将cache的NSData解压,还原成最初的形式,返回
删除缓存(remove)
使用下面的代码删除Disk的缓存:
1 | [diskCache removeObjectForKey:@"DiskCacheKey"]; |
- 删除数据库中key为DiskCacheKey的记录
- 如果存在文件的话,删除
YYMemoryCache
YYMemoryCache维护着一个双向链表(YYLinkedMap),链表里面的每一个节点就是一个memory的缓存记录,表头的节点表示最近访问过的,表尾的节点很久没去访问了。每个节点是一个 YYLinkedMapNode 对象,这个对象里面有指向前后节点的指针,和包含缓存信息的 key 和 value 属,以及一些缓存的其他属性:cost 和 time
初始化(init)
使用下面的代码初始化一个YYMemoryCache:
1 | YYMemoryCache *memoryCache = [YYMemoryCache new]; |
- 新建一个YYLinkedMap
- 添加一些监听事件,设置一些属性等等
写入缓存(set)
使用下面的代码写入缓存:
1 | [memoryCache setObject:@"Memory Cache" forKey:@"MemoryCacheKey"]; |
- 先在YYLinkedMap中寻找是否有这个key,YYLinkedMap对象中有一个dic属性,它是一个键值对,键就是每个cache的key,值就是每个cache对应的YYLinkedMapNode
- 如果存在的话,那就修改这个key对应的YYLinkedMapNode的value为
@"Memory Cache"
,并且修改它的其他属性,将time属性改为现在的时间,并将这个Node放在链表的表头。 - 如果不存在的话,就直接生成一个YYLinkedMapNode,放在链表的表头
读取缓存(get)
使用下面的代码读取缓存:
1 | NSString *cacheValue = [memoryCache objectForKey:@"MemoryCacheKey"]; |
- 从YYLinkedMap的dic中获取key为MemoryCacheKey的YYLinkedMapNode,修改node的time,并将这个node放在链表的表头
- 返回所需的缓存,也就是node的value属性
删除缓存(remove)
使用下面的代码删除缓存:
1 | [memoryCache removeObjectForKey:@"MemoryCacheKey"]; |
- 删除YYLinkedMap中的dic里面的key为MemoryCacheKey的YYLinkedMapNode
- 调整双向链表
其他
关于线程安全
YYDiskCache
每个YYDiskCache有一个信号量为1的锁 _lock ,使用
dispatch_semaphore_create(1);
来创建。当对这个YYDiskCache里面的数据进行操作时,比如添加或者读取缓存时,一次只允许一个操作在进行。还有一个信号量也为1的全局的锁 _globalInstancesLock ,它用来对存储每个YYDiskCache的NSMapTable进行加锁,对这个NSMapTable进行读写的操作只能同时存在一个。
每一个YYDiskCache有一个并行的queue,这个queue用来执行读写删之后的block。因为有这样的API:
1
- (void)objectForKey:(NSString *)key withBlock:(void(^)(NSString *key, id<NSCoding> _Nullable object))block;
所以这里的block就在这个queue里面执行,达到多线程操作的效果
YYMemoryCache
- 每一个YYMemoryCache都有一个
pthread_mutex_t
锁,同理,对YYMemoryCache的写入和读取缓存等操作只能同时存在一个。 - 有一个YYMemoryCacheGetReleaseQueue, 用来释放删除的缓存。 这里还需要细看代码。。。
关于各种限制
YYCache可以设置各种限制,比如缓存的最大数量,最大size以及缓存的生存时间等等。对于YYDiskCache和YYMemoryCache有两套不同的处理方法。当然也有相同的一点就是:他们都会定时清理缓存,在新建一个YYDiskCache或YYMemoryCache时,都会设一个定时事件,按时清理缓存。
YYDiskCache
在YYDiskCache里面,每一个cache的大小都在数据库里面的size字段里面有记录,而最近的缓存操作时间也有记录。如果操过某个限制的话,比如总容量限制或总数量限制或者存活时间限制,那么会删掉最长时间没有去使用的缓存。这个操作很简单,只需要对数据库的各个缓存按时间排序就行了。
YYMemoryCache
在YYMemoryCache里面,使用双向链表来记录每个缓存的使用情况。表头的表示刚刚使用过,而表尾的则很久没使用了。每个YYLinkedMapNode都有记录自己的time和cost,而YYLinkedMap则有记录总的cost和总的缓存数量。当超过某个限制的话,就会从表尾开始删除缓存,直到达到要求