适合iOS开发的一种缓存策略YYCache库 的原理

236 阅读4分钟

YYCache 是 iOS 上一个高性能的缓存框架,它由内存缓存 YYMemoryCache 和磁盘缓存 YYDiskCache 两部分组成。

核心总览

YYCache 的核心设计目标是 高效、线程安全和高性能。它通过以下方式实现这一目标:

  1. 分层设计:内存缓存提供极速访问,磁盘缓存提供大容量存储。
  2. LRU 淘汰算法:两者都使用 LRU 算法来管理缓存项,确保高频数据留在缓存中。
  3. 数据结构优化
    • 内存缓存:结合 NSDictionary 和双向链表。
    • 磁盘缓存:结合 SQLite 和文件系统。
  4. 锁策略优化:使用 pthread_mutex 锁来保证线程安全,性能优于 @synchronizeddispatch_semaphore

为了更直观地理解其核心工作原理,我们可以用以下流程图来展示其数据结构和关键操作:

image.png

上图揭示了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. 工作流程

存储过程:

  1. 根据 key 在数据库中查询记录。
  2. 判断 value 的数据大小。
  3. 小数据:直接写入 SQLite 的 data 列。如果之前是文件存储,则删除对应的文件。
  4. 大数据:将数据写入一个文件,并在数据库的 filename 列记录文件名。如果之前 SQLite 的 data 列有数据,则清空。
  5. 更新数据库中的元信息(大小、访问时间等)。

读取过程:

  1. 根据 key 从数据库中查询记录。
  2. 如果记录中有文件名(filename 不为空),则从文件系统中读取该文件。
  3. 如果记录中没有文件名,则直接从数据库的 data 列读取数据。
  4. 更新访问时间:每次读取后,都会在数据库中更新该记录的 last_access_time 字段,这对于实现 LRU 至关重要。

淘汰机制:

  1. 当磁盘缓存的总大小或总数量超过限制时,触发清理。
  2. 通过一条 SQL 查询,按照 last_access_time 升序排列(最久未使用的在前),获取需要淘汰的项。
  3. 根据查询结果,如果该项有文件,则删除文件;最后,从数据库中删除该记录。

三、YYCache 的整体协作

  1. 写入缓存

    • 先写入 YYMemoryCache
    • 再异步写入 YYDiskCache
  2. 读取缓存

    • 首先在 YYMemoryCache 中查找,找到则返回并更新链表。
    • 如果内存中没有,则去 YYDiskCache 中查找。
    • 如果在磁盘中找到,则将其返回给用户,并根据需要(可配置)写回 YYMemoryCache,以便下次快速访问。

总结

特性YYMemoryCacheYYDiskCache
存储介质内存磁盘 (SQLite + 文件系统)
数据结构双向链表 + 哈希表数据库表 + 文件
线程安全pthread_mutex串行队列 + dispatch_semaphore
淘汰算法LRU (链表移动)LRU (SQL 按时间排序)
性能极快,O(1)较快,对小数据优化好
容量受内存限制受磁盘空间限制

YYCache 的成功在于其对经典算法和数据结构的深刻理解,并结合 iOS 平台特性进行了精妙的工程优化,使其成为了一个非常出色和可靠的缓存组件。