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的过程中学习了不少二进制文件操作的方法,收益良多,也为这个系列开了一个好头。