适合iOS开发的一种缓存策略YYCache库 的原理
YYCache 是 iOS 上一个高性能的缓存框架,它由内存缓存 YYMemoryCache 和磁盘缓存 YYDiskCache 两部分组成。
核心总览
YYCache 的核心设计目标是 高效、线程安全和高性能。它通过以下方式实现这一目标:
- 分层设计:内存缓存提供极速访问,磁盘缓存提供大容量存储。
- LRU 淘汰算法:两者都使用 LRU 算法来管理缓存项,确保高频数据留在缓存中。
-
数据结构优化:
-
内存缓存:结合
NSDictionary和双向链表。 - 磁盘缓存:结合 SQLite 和文件系统。
-
内存缓存:结合
-
锁策略优化:使用
pthread_mutex锁来保证线程安全,性能优于@synchronized和dispatch_semaphore。
为了更直观地理解其核心工作原理,我们可以用以下流程图来展示其数据结构和关键操作:
上图揭示了YYCache的核心架构,下面我们来详细拆解图中各个部分的工作原理。
一、YYMemoryCache (内存缓存) 原理
YYMemoryCache 使用了一种非常经典且高效的数据结构组合:双向链表 + 哈希表。
1. 核心数据结构:_YYLinkedMapNode 和 _YYLinkedMap
-
_YYLinkedMapNode:链表节点。@interface _YYLinkedMapNode : NSObject { @package __unsafe_unretained _YYLinkedMapNode *_prev; // 指向上一节点 __unsafe_unretained _YYLinkedMapNode *_next; // 指向下一节点 id _key; // 缓存的键 id _value; // 缓存的值 NSUInteger _cost; // 开销成本(用于成本计算) NSTimeInterval _time; // 访问时间 } @end -
_YYLinkedMap:一个双向链表,用于管理所有节点。@interface _YYLinkedMap : NSObject { @package CFMutableDictionaryRef _dic; // 哈希表,用于O(1)的存取 NSUInteger _totalCost; // 总开销 NSUInteger _totalCount; // 总数量 _YYLinkedMapNode *_head; // 链表头(MRU,最近使用) _YYLinkedMapNode *_tail; // 链表尾(LRU,最久未使用) } @end
2. 工作原理
存取过程与LRU管理
其工作流程可以精确地描述为以下步骤:
sequenceDiagram
participant A as Client(客户端)
participant M as YYMemoryCache
participant D as _dic (哈希表)
participant L as 双向链表
A->>M: setObject:forKey:
M->>D: 通过key查找节点
alt 节点已存在
M->>L: 更新节点value,将节点移至_head
else 节点不存在
M->>M: 创建新节点
M->>D: 插入新节点
M->>L: 将新节点插入至_head
M->>M: _totalCount++, _totalCost++
loop 超过限制(count/cost)
M->>L: 移除_tail节点(LRU)
M->>D: 删除对应key
M->>M: 更新_totalCount, _totalCost
end
end
A->>M: objectForKey:
M->>D: 通过key查找节点
alt 节点存在
M->>L: 将节点移至_head
M->>A: 返回value
else 节点不存在
M->>A: 返回nil
end
线程安全:
YYMemoryCache 使用 pthread_mutex 锁来保证上述所有操作(_dic 的读写、链表的修改)的线程安全性。它在每个操作开始时加锁,结束时解锁。
二、YYDiskCache (磁盘缓存) 原理
YYDiskCache 的设计更为复杂,它采用了一种智能的混合存储策略,根据 value 的大小选择不同的存储方式,以在性能和空间之间取得平衡。
1. 核心思想:SQLite + 文件系统
-
SQLite 数据库:
- 存储所有的 元数据(key, 文件名,大小,访问时间等)。
- 如果 value 很小(例如小于 16KB),直接将其作为 BLOB 数据存储在数据库的某一列中。
- 优势:对于小数据,读写非常快,并且数据库事务保证了操作的原子性。
- 方便实现 LRU 淘汰算法,只需要通过 SQL 语句操作元数据即可。
-
文件系统:
- 如果 value 很大(例如大于 16KB),则将其写入单独的文件,在数据库中只记录其文件名和路径。
- 优势:避免大文件塞满 SQLite 数据库,导致性能下降。文件系统对于大文件的读写效率更高。
2. 工作流程
存储过程:
- 根据
key在数据库中查询记录。 - 判断
value的数据大小。 -
小数据:直接写入 SQLite 的
data列。如果之前是文件存储,则删除对应的文件。 -
大数据:将数据写入一个文件,并在数据库的
filename列记录文件名。如果之前 SQLite 的data列有数据,则清空。 - 更新数据库中的元信息(大小、访问时间等)。
读取过程:
- 根据
key从数据库中查询记录。 - 如果记录中有文件名(
filename不为空),则从文件系统中读取该文件。 - 如果记录中没有文件名,则直接从数据库的
data列读取数据。 -
更新访问时间:每次读取后,都会在数据库中更新该记录的
last_access_time字段,这对于实现 LRU 至关重要。
淘汰机制:
- 当磁盘缓存的总大小或总数量超过限制时,触发清理。
- 通过一条 SQL 查询,按照
last_access_time升序排列(最久未使用的在前),获取需要淘汰的项。 - 根据查询结果,如果该项有文件,则删除文件;最后,从数据库中删除该记录。
三、YYCache 的整体协作
-
写入缓存:
- 先写入
YYMemoryCache。 - 再异步写入
YYDiskCache。
- 先写入
-
读取缓存:
- 首先在
YYMemoryCache中查找,找到则返回并更新链表。 - 如果内存中没有,则去
YYDiskCache中查找。 - 如果在磁盘中找到,则将其返回给用户,并根据需要(可配置)写回
YYMemoryCache,以便下次快速访问。
- 首先在
总结
| 特性 | YYMemoryCache | YYDiskCache |
|---|---|---|
| 存储介质 | 内存 | 磁盘 (SQLite + 文件系统) |
| 数据结构 | 双向链表 + 哈希表 | 数据库表 + 文件 |
| 线程安全 | pthread_mutex |
串行队列 + dispatch_semaphore
|
| 淘汰算法 | LRU (链表移动) | LRU (SQL 按时间排序) |
| 性能 | 极快,O(1) | 较快,对小数据优化好 |
| 容量 | 受内存限制 | 受磁盘空间限制 |
YYCache 的成功在于其对经典算法和数据结构的深刻理解,并结合 iOS 平台特性进行了精妙的工程优化,使其成为了一个非常出色和可靠的缓存组件。