登录 立即注册
金钱:

Code4App-iOS开发-iOS 开源代码库-iOS代码实例搜索-iOS特效示例-iOS代码例子下载-Code4App.com

从 YYCache 源码 Get 到如何设计一个优秀的缓存 [复制链接]

2017-12-7 11:23
BlueManlove 阅读:186 评论:0 赞:0
Tag:  

前言

iOS 开发中总会用到各种缓存,但是各位有没有考虑过什么样的缓存才能被叫做优秀的缓存,或者说优秀的缓存应该具备哪些特质? 

闭上眼睛,想一想如果面试官让你设计一个缓存你会怎么回答?

本文将结合 YYCache 的源码逐步带大家找到答案。

YYCache 是一个线程安全的高性能键值缓存(该项目是 YYKit 组件之一)。YYKit 是在 2015 年发布到 Github 的,由于其代码质量很高,在短时间内就收获了大量的 Star(目前已经 1w+ Star 了),而且在 iOS 各大社区反响广泛,Google 一下也是漫天赞叹。

YYKit 作者是 @ibireme,原名郭曜源(猜测 YY 前缀来源于曜源?),是我个人非常喜欢的国人开发者(何止喜欢,简直是迷弟)。

YYCache 的代码逻辑清晰,注释详尽,加上自身不算太大的代码量使得其阅读非常简单,更加难能可贵的是它的性能还非常高。

我对它的评价是小而美,这种小而美的缓存源码对于我们今天的主题太合适不过了(本文中 YYCache 源码版本为 v1.0.4)。

索引

  • YYCache 简介

  • YYMemoryCache 细节剖析

  • YYDiskCache 细节剖析

  • 优秀的缓存应该具备哪些特质

  • 总结

YYCache 简介

简单把 YYCache 从头到尾撸了一遍,最大的感触就是代码风格干净整洁,代码思路清晰明了。

由于代码整体阅读难度不是非常大,本文不会去逐字逐句的解读源码,而是提炼 YYCache 作为一个小而美的缓存实现了哪些缓存该具备的特质,并且分析实现细节。

我们先来简单看一下 YYCache 的代码结构,YYCache 是由 YYMemoryCache 与 YYDiskCache 两部分组成的,其中 YYMemoryCache 作为高速内存缓存,而 YYDiskCache 则作为低速磁盘缓存。

通常一个缓存是由内存缓存和磁盘缓存组成,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储。

1
2
3
4
5
6
7
8
9
10
11
12
@interface YYCache : NSObject
 
@property (copy, readonly) NSString *name;
@property (strong, readonly) YYMemoryCache *memoryCache;
@property (strong, readonly) YYDiskCache *diskCache;
 
- (BOOL)containsObjectForKey:(NSString *)key;
- (nullable id)objectForKey:(NSString *)key;
- (void)setObject:(nullable id)object forKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key;
 
@end

上面的代码我做了简化,只保留了最基本的代码(我认为作者在最初设计 YYCache 雏形时很可能也只是提供了这些基本的接口),其他的接口只是通过调用基本的接口再附加对应处理代码而成。

Note: 其实源码中作者用了一些技巧性的宏,例如 NS_ASSUME_NONNULL_BEGIN 与 NS_ASSUME_NONNULL_END 来通过编译器层检测入参是否为空并给予警告,参见 Nullability and Objective-C

类似上述的编码技巧还有很多,我并非不想与大家分享我 get 到的这些编码技巧,只是觉得它与本文的主题似乎不太相符。我准备在之后专门写一篇文章来与大家分享我在阅读各大源码库过程中 get 到的编码技巧(感兴趣的话可以 关注我)。

从代码中我们可以看到 YYCache 中持有 YYMemoryCache 与 YYDiskCache,并且对外提供了一些接口。这些接口基本都是基于 Key 和 Value 设计的,类似于 iOS 原生的字典类接口(增删改查)。

YYMemoryCache 细节剖析

YYMemoryCache 是一个高速的内存缓存,用于存储键值对。它与 NSDictionary 相反,Key 被保留并且不复制。API 和性能类似于 NSCache,所有方法都是线程安全的。

YYMemoryCache 对象与 NSCache 的不同之处在于:

  • YYMemoryCache 使用 LRU(least-recently-used) 算法来驱逐对象;NSCache 的驱逐方式是非确定性的。

  • YYMemoryCache 提供 age、cost、count 三种方式控制缓存;NSCache 的控制方式是不精确的。

  • YYMemoryCache 可以配置为在收到内存警告或者 App 进入后台时自动逐出对象。

Note: YYMemoryCache 中的 Access Methods 消耗时长通常是稳定的 (O(1))。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@interface YYMemoryCache : NSObject
 
#pragma mark - Attribute
@property (nullable, copy) NSString *name; // 缓存名称,默认为 nil
@property (readonly) NSUInteger totalCount; // 缓存对象总数
@property (readonly) NSUInteger totalCost; // 缓存对象总开销
 
 
#pragma mark - Limit
@property NSUInteger countLimit; // 缓存对象数量限制,默认无限制,超过限制则会在后台逐出一些对象以满足限制
@property NSUInteger costLimit; // 缓存开销数量限制,默认无限制,超过限制则会在后台逐出一些对象以满足限制
@property NSTimeInterval ageLimit; // 缓存时间限制,默认无限制,超过限制则会在后台逐出一些对象以满足限制
 
@property NSTimeInterval autoTrimInterval; // 缓存自动清理时间间隔,默认 5s
 
@property BOOL shouldRemoveAllObjectsOnMemoryWarning; // 是否应该在收到内存警告时删除所有缓存内对象
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground; // 是否应该在 App 进入后台时删除所有缓存内对象
 
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache); // 我认为这是一个 hook,便于我们在收到内存警告时自定义处理缓存
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache); // 我认为这是一个 hook,便于我们在收到 App 进入后台时自定义处理缓存
 
@property BOOL releaseOnMainThread; // 是否在主线程释放对象,默认 NO,有些对象(例如 UIView/CALayer)应该在主线程释放
@property BOOL releaseAsynchronously; // 是否异步释放对象,默认 YES
 
- (BOOL)containsObjectForKey:(id)key;
 
- (nullable id)objectForKey:(id)key;
 
- (void)setObject:(nullable id)object forKey:(id)key;
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
- (void)removeObjectForKey:(id)key;
- (void)removeAllObjects;
 
 
#pragma mark - Trim
- (void)trimToCount:(NSUInteger)count; // 用 LRU 算法删除对象,直到 totalCount <= count
- (void)trimToCost:(NSUInteger)cost; // 用 LRU 算法删除对象,直到 totalCost <= cost
- (void)trimToAge:(NSTimeInterval)age; // 用 LRU 算法删除对象,直到所有到期对象全部被删除
 
@end

YYMemoryCache 的定义代码比较简单~ 该有的注释我已经加到了上面,这里 LRU 算法的实现我准备单独拎出来放到后面和(_YYLinkedMapNode 与 _YYLinkedMap)一起讲。我们这里只需要再关注一下 YYMemoryCache 是如何做到线程安全的。

YYMemoryCache 是如何做到线程安全的

1
2
3
4
5
@implementation YYMemoryCache {
    pthread_mutex_t _lock; // 线程锁,旨在保证 YYMemoryCache 线程安全
    _YYLinkedMap *_lru; // _YYLinkedMap,YYMemoryCache 通过它间接操作缓存对象
    dispatch_queue_t _queue; // 串行队列,用于 YYMemoryCache 的 trim 操作
}

没错,这里 ibireme 选择使用 pthread_mutex 线程锁来确保 YYMemoryCache 的线程安全。

有趣的是,这里 ibireme 使用 pthread_mutex 是有一段小故事的。在最初 YYMemoryCache 这里使用的锁是 OSSpinLock 自旋锁(详见 YYCache 设计思路 备注-关于锁),后面有人在 Github 向作者提 issue 反馈 OSSpinLock 不安全,经过作者的确认(详见 不再安全的 OSSpinLock)最后选择用 pthread_mutex 替代 OSSpinLock。

上面是 ibireme 在确认 OSSpinLock 不再安全之后为了寻找替代方案做的简单性能测试,对比了一下几种能够替代 OSSpinLock 锁的性能。在 不再安全的 OSSpinLock 文末的评论中,我找到了作者使用 pthread_mutex 的原因。

ibireme: 苹果员工说 libobjc 里 spinlock 是用了一些私有方法 (mach_thread_switch),贡献出了高线程的优先来避免优先级反转的问题,但是我翻了下 libdispatch 的源码倒是没发现相关逻辑,也可能是我忽略了什么。在我的一些测试中,OSSpinLock 和 dispatch_semaphore 都不会产生特别明显的死锁,所以我也无法确定用 dispatch_semaphore 代替 OSSpinLock 是否正确。能够肯定的是,用 pthread_mutex 是安全的。

_YYLinkedMapNode 与 _YYLinkedMap

上文介绍了 YYMemoryCache,其实 YYMemoryCache 并不直接操作缓存对象,而是通过内部的 _YYLinkedMapNode 与 _YYLinkedMap 来间接的操作缓存对象。这两个类对于上文中提到的 LRU 缓存算法的理解至关重要,所以我把他们俩单独拎出来放在这里详细解读一下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
 _YYLinkedMap 中的一个节点。
 通常情况下我们不应该使用这个类。
 */
@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // __unsafe_unretained 是为了性能优化,节点被 _YYLinkedMap 的 _dic 强引用
    __unsafe_unretained _YYLinkedMapNode *_next; // __unsafe_unretained 是为了性能优化,节点被 _YYLinkedMap 的 _dic 强引用
    id _key;
    id _value;
    NSUInteger _cost; // 记录开销,对应 YYMemoryCache 提供的 cost 控制
    NSTimeInterval _time; // 记录时间,对应 YYMemoryCache 提供的 age 控制
}
@end
 
 
/**
 YYMemoryCache 内的一个链表。
 _YYLinkedMap 不是一个线程安全的类,而且它也不对参数做校验。
 通常情况下我们不应该使用这个类。
 */
@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; // 不要直接设置该对象
    NSUInteger _totalCost;
    NSUInteger _totalCount;
    _YYLinkedMapNode *_head; // MRU, 最常用节点,不要直接修改它
    _YYLinkedMapNode *_tail; // LRU, 最少用节点,不要直接修改它
    BOOL _releaseOnMainThread; // 对应 YYMemoryCache 的 releaseOnMainThread
    BOOL _releaseAsynchronously; // 对应 YYMemoryCache 的 releaseAsynchronously
}
 
// 链表操作,看接口名称应该不需要注释吧~
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
- (void)removeNode:(_YYLinkedMapNode *)node;
- (_YYLinkedMapNode *)removeTailNode;
- (void)removeAll;
 
@end

为了方便大家阅读,我标注了必要的中文注释。其实对数据结构与算法不陌生的同学应该一眼就看的出来 _YYLinkedMapNode 与 _YYLinkedMap 这俩货的本质。没错,丫就是双向链表节点和双向链表。

_YYLinkedMapNode 作为双向链表节点,除了基本的 _prev、_next,还有键值缓存基本的 _key 与 _value,我们可以把 _YYLinkedMapNode 理解为 YYMemoryCache 中的一个缓存对象

_YYLinkedMap 作为由 _YYLinkedMapNode 节点组成的双向链表,使用 CFMutableDictionaryRef _dic 字典存储 _YYLinkedMapNode。这样在确保 _YYLinkedMapNode 被强引用的同时,能够利用字典的 Hash 快速定位用户要访问的缓存对象,这样既符合了键值缓存的概念又省去了自己实现的麻烦(笑)。

嘛~ 总得来说 YYMemoryCache 是通过使用 _YYLinkedMap 双向链表来操作 _YYLinkedMapNode 缓存对象节点的。

LRU(least-recently-used) 算法的实现

上文我们认清了 _YYLinkedMap 与 _YYLinkedMapNode 本质上就是双向链表和链表节点,这里我们简单讲一下 YYMemoryCache 是如何利用双向链表实现 LRU(least-recently-used) 算法的。

缓存替换策略

首先 LRU 是缓存替换策略(Cache replacement policies)的一种,还有很多缓存替换策略诸如:

  • First In First Out (FIFO)

  • Last In First Out (LIFO)

  • Time aware Least Recently Used (TLRU)

  • Most Recently Used (MRU)

  • Pseudo-LRU (PLRU)

  • Random Replacement (RR)

  • Segmented LRU (SLRU)

  • Least-Frequently Used (LFU)

  • Least Frequent Recently Used (LFRU)

  • LFU with Dynamic Aging (LFUDA)

  • Low Inter-reference Recency Set (LIRS)

  • Adaptive Replacement Cache (ARC)

  • Clock with Adaptive Replacement (CAR)

  • Multi Queue (MQ) caching algorithm|Multi Queue (MQ)

  • Pannier: Container-based caching algorithm for compound objects

是不是被唬到了?不要担心,我这里会表述的尽量易懂。

缓存命中率


为什么有这么多缓存替换策略,或者说搞这么多名堂究竟是为了什么呢?

答案是提高缓存命中率,那么何谓缓存命中率呢?

Google 一下自然是有不少解释,不过很多都是 web 相关的,而且不说人话(很难理解),我个人非常讨厌各种不说人话的“高深”抽象概念。

这里抖了好几抖胆才敢谈一下我对于缓存命中率的理解(限于 YYCache 和 iOS 开发)。

  • 缓存命中 = 用户要访问的缓存对象在高速缓存中,我们直接在高速缓存中通过 Hash 将其找到并返回给用户。

  • 缓存命中率 = 用户要访问的缓存对象在高速缓存中被我们访问到的概率。

既然谈到了自己的理解,我索性说个够。

  • 缓存丢失 = 由于高速缓存数量有限(占据内存等原因),所以用户要访问的缓存对象很有可能被我们从有限的高速缓存中淘汰掉了,我们可能会将其存储于低速的磁盘缓存中(如果磁盘缓存还有资源的话),那么就要从磁盘缓存中获取该缓存对象以返回给用户,这种情况我理解为(高速)缓存未命中,即缓存丢失(并不是真的被我们丢掉了,但肯定是被我们从高速缓存淘汰掉了)。

缓存命中是 cache-hit,那么如果你玩游戏,可以理解为这次 hit miss 了(笑,有人找我开黑吗)。

LRU

首先来讲一下 LRU 的概念让大家有一个基本的认识。LRU(least-recently-used) 翻译过来是“最近使用”,顾名思义这种缓存替换策略是基于用户最近访问过的缓存对象而建立。

我认为 LRU 缓存替换策略的核心思想在于:LRU 认为用户最新使用(访问)过的缓存对象为高频缓存对象,即用户很可能还会再次使用(访问)该缓存对象;而反之,用户很久之前使用(访问)过的缓存对象(期间一直没有再次访问)为低频缓存对象,即用户很可能不会再去使用(访问)该缓存对象,通常在资源不足时会先去释放低频缓存对象。

_YYLinkedMapNode 与 _YYLinkedMap 实现 LRU

YYCache 作者通过 _YYLinkedMapNode 与 _YYLinkedMap 双向链表实现 LRU 缓存替换策略的思路其实很简捷清晰,我们一步一步来看。

双向链表中有头结点和尾节点:

  • 头结点 = 链表中用户最近一次使用(访问)的缓存对象节点,MRU。

  • 尾节点 = 链表中用户已经很久没有再次使用(访问)的缓存对象节点,LRU。

如何让头结点和尾节点指向我们想指向的缓存对象节点?我们结合代码来看:

  • 在用户使用(访问)时更新缓存节点信息,并将其移动至双向链表头结点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (id)objectForKey:(id)key {
    // 判断入参
    if (!key) return nil;
    pthread_mutex_lock(&_lock);
    // 找到对应缓存节点
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    if (node) {
        // 更新缓存节点时间,并将其移动至双向链表头结点
        node->_time = CACurrentMediaTime();
        [_lru bringNodeToHead:node];
    }
    pthread_mutex_unlock(&_lock);
    // 返回找到的缓存节点 value
    return node ? node->_value : nil;
}
  • 在用户设置缓存对象时,判断入参 key 对应的缓存对象节点是否存在?存在则更新缓存对象节点并将节点移动至链表头结点;不存在则根据入参生成新的缓存对象节点并插入链表表头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    // 判断入参,省略
    ...
    pthread_mutex_lock(&_lock);
    // 判断入参 key 对应的缓存对象节点是否存在
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    NSTimeInterval now = CACurrentMediaTime();
    if (node) {
        // 存在则更新缓存对象节点并将节点移动至链表头结点
        _lru->_totalCost -= node->_cost;
        _lru->_totalCost += cost;
        node->_cost = cost;
        node->_time = now;
        node->_value = object;
        [_lru bringNodeToHead:node];
    else {
        // 不存在则根据入参生成新的缓存对象节点并插入链表表头
        node = [_YYLinkedMapNode new];
        node->_cost = cost;
        node->_time = now;
        node->_key = key;
        node->_value = object;
        [_lru insertNodeAtHead:node];
    }
    // 判断插入、更新节点之后是否超过了限制 cost、count,如果超过则 trim,省略
    ...
    pthread_mutex_unlock(&_lock);
}
  • 在资源不足时,从双线链表的尾节点(LRU)开始清理缓存,释放资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 这里拿 count 资源举例,cost、age 自己举一反三
- (void)_trimToCount:(NSUInteger)countLimit {
    // 判断 countLimit 为 0,则全部清空缓存,省略
    // 判断 _lru->_totalCount <= countLimit,没有超出资源限制则不作处理,省略
    ...
     
    NSMutableArray *holder = [NSMutableArray new];
    while (!finish) {
        if (pthread_mutex_trylock(&_lock) == 0) {
            if (_lru->_totalCount > countLimit) {
                // 从双线链表的尾节点(LRU)开始清理缓存,释放资源
                _YYLinkedMapNode *node = [_lru removeTailNode];
                if (node) [holder addObject:node];
            else {
                finish = YES;
            }
            pthread_mutex_unlock(&_lock);
        else {
            // 使用 usleep 以微秒为单位挂起线程,在短时间间隔挂起线程
            // 对比 sleep 用 usleep 能更好的利用 CPU 时间
            usleep(10 1000); //10 ms
        }
    }
     
    // 判断是否需要在主线程释放,采取释放缓存对象操作
    if (holder.count) {
        dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
        dispatch_async(queue, ^{
            // 异步释放,我们单独拎出来讲
            [holder count]; // release in queue
        });
    }
}

嘛~ 是不是感觉敲简单?上面代码去掉了可能会分散大家注意力的代码,我们这里仅仅讨论 LRU 的实现,其余部分的具体实现源码也非常简单,我觉得没必要贴出来单独讲解,感兴趣的同学可以自己去 YYCache 下载源码查阅。

异步释放技巧

关于上面的异步释放缓存对象的代码,我觉得还是有必要单独拎出来讲一下的:

1
2
3
4
5
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
    // 异步释放,我们单独拎出来讲
    [holder count]; // release in queue
});

这个技巧 ibireme 在他的另一篇文章 iOS 保持界面流畅的技巧 中有提及:

Note: 对象的销毁虽然消耗资源不多,但累积起来也是不容忽视的。通常当容器类持有大量对象时,其销毁时的资源消耗就非常明显。同样的,如果对象可以放到后台线程去释放,那就挪到后台线程去。这里有个小 Tip:把对象捕获到 block 中,然后扔到后台队列去随便发送个消息以避免编译器警告,就可以让对象在后台线程销毁了。

而上面代码中的 YYMemoryCacheGetReleaseQueue 这个队列源码为:

1
2
3
4
// 静态内联 dispatch_queue_t
static inline dispatch_queue_t YYMemoryCacheGetReleaseQueue() {
    return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
}

在源码中可以看到 YYMemoryCacheGetReleaseQueue 是一个低优先级 DISPATCH_QUEUE_PRIORITY_LOW 队列,猜测这样设计的原因是可以让 iOS 在系统相对空闲时再来异步释放缓存对象。

YYDiskCache 细节剖析

YYDiskCache 是一个线程安全的磁盘缓存,用于存储由 SQLite 和文件系统支持的键值对(类似于 NSURLCache 的磁盘缓存)。

YYDiskCache 具有以下功能:

  • 它使用 LRU(least-recently-used) 来删除对象。

  • 支持按 cost,count 和 age 进行控制。

  • 它可以被配置为当没有可用的磁盘空间时自动驱逐缓存对象。

  • 它可以自动抉择每个缓存对象的存储类型(sqlite/file)以便提供更好的性能表现。

Note: 您可以编译最新版本的 sqlite 并忽略 iOS 系统中的 libsqlite3.dylib 来获得 2x〜4x 的速度提升。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
@interface YYDiskCache : NSObject
 
#pragma mark - Attribute
@property (nullable, copy) NSString *name; // 缓存名称,默认为 nil
@property (readonly) NSString *path; // 缓存路径
 
@property (readonly) NSUInteger inlineThreshold; // 阈值,大于阈值则存储类型为 file;否则存储类型为 sqlite
 
@property (nullable, copy) NSData *(^customArchiveBlock)(id object); // 用来替换 NSKeyedArchiver,你可以使用该代码块以支持没有 conform `NSCoding` 协议的对象
@property (nullable, copy) id (^customUnarchiveBlock)(NSData *data); // 用来替换 NSKeyedUnarchiver,你可以使用该代码块以支持没有 conform `NSCoding` 协议的对象
 
@property (nullable, copy) NSString *(^customFileNameBlock)(NSString *key); // 当一个对象将以 file 的形式保存时,该代码块用来生成指定文件名。如果为 nil,则默认使用 md5(key) 作为文件名
 
#pragma mark - Limit
@property NSUInteger countLimit; // 缓存对象数量限制,默认无限制,超过限制则会在后台逐出一些对象以满足限制
@property NSUInteger costLimit; // 缓存开销数量限制,默认无限制,超过限制则会在后台逐出一些对象以满足限制
@property NSTimeInterval ageLimit; // 缓存时间限制,默认无限制,超过限制则会在后台逐出一些对象以满足限制
@property NSUInteger freeDiskSpaceLimit; // 缓存应该保留的最小可用磁盘空间(以字节为单位),默认无限制,超过限制则会在后台逐出一些对象以满足限制
 
@property NSTimeInterval autoTrimInterval; // 缓存自动清理时间间隔,默认 60s
@property BOOL errorLogsEnabled; // 是否开启错误日志
 
#pragma mark - Initializer
- (nullable instancetype)initWithPath:(NSString *)path
                      inlineThreshold:(NSUInteger)threshold NS_DESIGNATED_INITIALIZER;
 
- (BOOL)containsObjectForKey:(NSString *)key;
 
- (nullable id)objectForKey:(NSString *)key;
 
- (void)setObject:(nullable id)object forKey:(NSString *)key;
 
- (void)removeObjectForKey:(NSString *)key;
- (void)removeAllObjects;
                                 
- (NSInteger)totalCount;
- (NSInteger)totalCost;
 
#pragma mark - Trim
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;
 
#pragma mark - Extended Data
+ (nullable NSData *)getExtendedDataFromObject:(id)object;
+ (void)setExtendedData:(nullable NSData *)extendedData toObject:(id)object;
 
@end

YYDiskCache 结构与 YYMemoryCache 类似,由于很多接口都是基于基本的接口做了扩展所得,这里贴的代码省略了一些接口。代码还是一如既往的干净简洁,相信各位都能看懂。

YYDiskCache 是基于 sqlite 和 file 来做的磁盘缓存,我们的缓存对象可以自由的选择存储类型,下面简单对比一下:

  • sqlite: 对于小数据(例如 NSNumber)的存取效率明显高于 file。

  • file: 对于较大数据(例如高质量图片)的存取效率优于 sqlite。

所以 YYDiskCache 使用两者配合,灵活的存储以提高性能。

NSMapTable

NSMapTable 是类似于字典的集合,但具有更广泛的可用内存语义。NSMapTable 是 iOS6 之后引入的类,它基于 NSDictionary 建模,但是具有以下差异:

  • 键/值可以选择 “weakly” 持有,以便于在回收其中一个对象时删除对应条目。

  • 它可以包含任意指针(其内容不被约束为对象)。

  • 您可以将 NSMapTable 实例配置为对任意指针进行操作,而不仅仅是对象。

Note: 配置映射表时,请注意,只有 NSMapTableOptions 中列出的选项才能保证其余的 API 能够正常工作,包括复制,归档和快速枚举。 虽然其他 NSPointerFunctions 选项用于某些配置,例如持有任意指针,但并不是所有选项的组合都有效。使用某些组合,NSMapTableOptions 可能无法正常工作,甚至可能无法正确初始化。

更多信息详见 NSMapTable 官方文档

需要特殊说明的是,YYDiskCache 内部是基于一个单例 NSMapTable 管理的,这点有别于 YYMemoryCache。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
static NSMapTable *_globalInstances; // 引用管理所有的 YYDiskCache 实例
static dispatch_semaphore_t _globalInstancesLock; // YYDiskCache 使用 dispatch_semaphore 保障 NSMapTable 线程安全
 
static void _YYDiskCacheInitGlobal() {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        _globalInstancesLock = dispatch_semaphore_create(1);
        _globalInstances = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
    });
}
 
static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path) {
    if (path.length == 0return nil;
    _YYDiskCacheInitGlobal();
    dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
    id cache = [_globalInstances objectForKey:path];
    dispatch_semaphore_signal(_globalInstancesLock);
    return cache;
}
 
static void _YYDiskCacheSetGlobal(YYDiskCache *cache) {
    if (cache.path.length == 0return;
    _YYDiskCacheInitGlobal();
    dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
    [_globalInstances setObject:cache forKey:cache.path];