SPTPersistentCache
是spotify开源的一套持久化框架。SPTPersistentCache
主要解决NSData
存储到文件系统的过程。
亮点
- 基于LRU
- 线程安全
- 可视化工具
- 类HTTP设计
成员分析
先从Public Api来进行分析
SPTPersistentCache
是核心,同比为manager的角色。SPTPersistentCacheOptions
是配置选项,SPTPersistentCacheHeader
是SPTPersistentCache
的一大设计的特色,数据表头SPTPersistentCacheRecord
为数据单元。SPTPersistentCacheResponse
为操纵数据的基本单位。
这次分析打算从下往上,先从基本的数据单元开始
SPTPersistentCacheRecord
SPTPersistentCacheRecord
中除了data之外还定义三个辅助数据
- refCount 引用计数
- ttl 生存时间
- key
可以从SPTPersistentCacheRecord
看出SPTPersistent
依照引用次数和时间来管理缓存对象
SPTPersistentCacheResponse
SPTPersistentCacheResponse
是对SPTPersistentCacheRecord
的再一层包装。新增了对外属性
- SPTPersistentCacheResponseCode
- NSError
从SPTPersistentCacheResponseCode
可以看出这一层与读写操作相关了。
SPTPersistentCacheRecord
SPTPersistentCacheRecord
并不是一个类,而是一个结构体。在这里,可以将SPTPersistentCacheRecord
与HTTP中的header
进行类比。
关键点:crc,flags
crc:循环冗余校验 在数据写入前通过计算生成32位整数,拼接在数据后面。在读取的时候进行校验,确认数据是否完整。SPTPersistentCache
使用的是CRC-64-ISO
flags: 判断数据是否在上次写入的时候没能完成。这个更多是逻辑而不是一个错误。
SPTPersistentCacheOptions
SPTPersistentCacheOptions
是cache的配置相关的内容。从配置中够可以看出SPTPersistentCache
的一些特性
identifierForQueue
作为唯一队列标识,可以预知每一个cache拥有唯一的队列
cacheIdentifier
作为配置对应的cache唯一标识
优先级设定中分别定义了并发操作数,读写删的优先级
1 | @property (nonatomic) NSInteger maxConcurrentOperations; |
GC设定中对GC的配置进行设定
garbageCollectionInterval
决定了GC的间隔
defaultExpirationPeriod
决定默认过期时间
sizeConstraintBytes
设定了缓存空间的限制
garbageCollectionPriority
设定GC操作的优先级
garbageCollectionQualityOfService
设定GC服务的QOS
从option中可以窥探出,在开发过程中可能会有多个cache,每个cache之间的配置独立,拥有独立的优先级和GC管理
SPTPersistentCache
SPTPersistentCache
是整个持久化库的核心类,包含了读,写,查,删,锁等操作
读操作中,对外公开的两个API最后会归集到下面这个方法中
1 | - (void)loadDataForKeySync:(NSString *)key |
方法并不是很复杂,具体的流程简单描述如下
1 | 首先,根据key去查找是否有对应的文件,木有的话就抛错 (1) |
可以看到,总共有5步抛错的地方,都是对数据完整性的校验。
相对于读,写的难度就小了很多,不太复杂。写方法主要是在
1 | - (NSError *)storeDataSync:(NSData *)data |
将Header与data拼装后组成rowData,然后写入磁盘,注意判断是否写入成功。之后通过block将response回调给开发者
GC和LRU实现
LRU是Least Recently Used 近期最少使用算法。
在最近的面试中,经常会谈到一些缓存库的实现,比如最常见的SDWebImageCache
。对此我常会提出一个疑问,如何设计高效的缓存。几乎没人能够谈谈缓存调度算法。LRU其实就是最常用的一种缓存调度算法,目的就是尽力保证缓存中的数据是常用的。
这里谈谈SPTPersistentCache
中是如何实现GC和LRU算法的。
在上文SPTPersistentCacheRecord
的简介中,我们看到了两个关键的属性:TTL && refcount。我们可以猜测到,SPTPersistentCache
是通过实践和引用值来判断是数据的有效性的。
GC的核心是👇这个方法
1 | - (void)collectGarbageForceExpire:(BOOL)forceExpire forceLocked:(BOOL)forceLocked |
传参分别是强制过期和强制加锁。
过程不复杂,通过NSDirectoryEnumerator
迭代器从self.fileManager
中取出所有文件。
判断是否过期是通过下面这个方法
1 | - (SPTPersistentCacheResponse *)alterHeaderForFileAtPath:(NSString *)filePath |
这个方法是唯一去访问文件header的方法。
传入modifyBlock
将改变header。
回到GC的过程中,可以看到通过传入modifyBlock
来获取header。在modifyBlock
中,通过header中的refcount来判断是否需要移除。
那么什么情况会触发refcount
的变化呢?
在lockDataForKeys
方法中,可以看到这样一行代码
1 | // Satisfy Req.#1.2 |
可知,当执行lock的时候,如果数据本身没过期,会使引用计数+1。
同理,当unlock的时候,会使引用计数-1。
可见,refcount只有1和0两个值。
而TTL(存活时间)
则是数据的的生命周期。如果没有forceExpire
和forceLocked
的话,则当[self isDataExpiredWithHeader:header] && header->refCount == 0
的时候删除数据。
总结
github上实现二进制缓存的cache库不多,SPTPersistentCache
是其中的佼佼者。原因在于spotify本身做在线音乐服务,在二进制缓存上有比较强的需求。
SPTPersistentCache
的实现我认为参考了HTTP协议,通过一个Header来为数据添加metedata,同时实现了GC。可以SPTPersistentCache
的实现方式非常值得学习,在阅读SPTPersistentCache
的过程中学习了不少二进制文件操作的方法,收益良多,也为这个系列开了一个好头。