YYCache 阅读学习
官网 介绍如下:
高性能 iOS 缓存框架。YYKit 组件之一。
性能:(摘自官网)
iPhone 6 上,内存缓存每秒响应次数 (越高越好):
iPhone 6 上,磁盘缓存每秒响应次数 (越高越好):
特性
- LRU: 缓存支持 LRU (least-recently-used) 淘汰算法。
- 缓存控制: 支持多种缓存控制方法:总数量、总大小、存活时间、空闲空间。
- 兼容性: API 基本和 NSCache 保持一致, 所有方法都是线程安全的。
- 内存缓存
- 对象释放控制: 对象的释放(release) 可以配置为同步或异步进行,可以配置在主线程或后台线程进行。
- 自动清空: 当收到内存警告或 App 进入后台时,缓存可以配置为自动清空。
- 磁盘缓存
- 可定制性: 磁盘缓存支持自定义的归档解档方法,以支持那些没有实现 NSCoding 协议的对象。
- 存储类型控制: 磁盘缓存支持对每个对象的存储类型 (SQLite/文件) 进行自动或手动控制,以获得更高的存取性能。
源码解析学习
文中大部分为摘抄原文,然后加上自己的理解注释.
基本使用方法
举一个缓存用户姓名的例子来看一下YYCache的几个API:
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
| NSString *userName = @"Jack"; NSString *key = @"user_name"; YYCache *userInfoCache = [YYCache cacheWithName:@"userInfo"]; [userInfoCache setObject:userName forKey:key withBlock:^{ NSLog(@"caching object succeed"); }]; [userInfoCache containsObjectForKey:key withBlock:^(NSString * _Nonnull key, BOOL contains) { if (contains){ NSLog(@"object exists"); } }];
[userInfoCache objectForKey:key withBlock:^(NSString * _Nonnull key, id<NSCoding> _Nonnull object) { NSLog(@"user name : %@",object); }];
[userInfoCache removeObjectForKey:key withBlock:^(NSString * _Nonnull key) { NSLog(@"remove user name %@",key); }]; [userInfoCache removeAllObjectsWithBlock:^{ NSLog(@"removing all cache succeed"); }];
[userInfoCache removeAllObjectsWithProgressBlock:^(int removedCount, int totalCount) { NSLog(@"remove all cache objects: removedCount :%d totalCount : %d",removedCount,totalCount); } endBlock:^(BOOL error) { if(!error){ NSLog(@"remove all cache objects: succeed"); }else{ NSLog(@"remove all cache objects: failed"); } }];
|
YYCache 整体结构
- YYCache:提供了最外层的接口,调用了YYMemoryCache与YYDiskCache的相关方法。
- YYMemoryCache:负责处理容量小,相对高速的内存缓存。线程安全,支持自动和手动清理缓存等功能。
- _YYLinkedMap:YYMemoryCache使用的双向链表类。
- _YYLinkedMapNode:是_YYLinkedMap使用的节点类。
- YYDiskCache:负责处理容量大,相对低速的磁盘缓存。线程安全,支持异步操作,自动和手动清理缓存等功能。
- YYKVStorage:YYDiskCache的底层实现类,用于管理磁盘缓存。
- YYKVStorageItem:内置在YYKVStorage中,是YYKVStorage内部用于封装某个缓存的类。
代码阅读学习
YYCache
YYCache给用户提供所有最外层的缓存操作接口,而这些接口的内部内部实际上是调用了YYMemoryCache和YYDiskCache对象的相关方法。
因为YYMemoryCache和YYDiskCache的实例作为YYCache的两个公开的属性,所以用户无法直接使用YYMemoryCache和YYDiskCache对象,只能通过属性的方式来间接使用它们。
YYCache的属性和接口
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
| @interface YYCache : NSObject
@property (copy, readonly) NSString *name; @property (strong, readonly) YYMemoryCache *memoryCache; @property (strong, readonly) YYDiskCache *diskCache;
- (BOOL)containsObjectForKey:(NSString *)key;
- (void)containsObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, BOOL contains))block;
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
- (void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id<NSCoding> object))block;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(nullable void(^)(void))block;
- (void)removeObjectForKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key))block;
- (void)removeAllObjects;
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
- (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress endBlock:(nullable void(^)(BOOL error))end;
@end
|
YYCache的接口实现
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
| - (BOOL)containsObjectForKey:(NSString *)key { //先检查内存缓存是否存在,再检查磁盘缓存是否存在 return [_memoryCache containsObjectForKey:key] || [_diskCache containsObjectForKey:key]; }
- (id<NSCoding>)objectForKey:(NSString *)key { //首先尝试获取内存缓存,然后获取磁盘缓存 id<NSCoding> object = [_memoryCache objectForKey:key]; //如果内存缓存不存在,就会去磁盘缓存里面找:如果找到了,则再次写入内存缓存中;如果没找到,就返回nil if (!object) { object = [_diskCache objectForKey:key]; if (object) { [_memoryCache setObject:object forKey:key]; } } return object; }
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key { //先写入内存缓存,后写入磁盘缓存 [_memoryCache setObject:object forKey:key]; [_diskCache setObject:object forKey:key]; }
- (void)removeObjectForKey:(NSString *)key { //先移除内存缓存,后移除磁盘缓存 [_memoryCache removeObjectForKey:key]; [_diskCache removeObjectForKey:key]; }
- (void)removeAllObjects { //先全部移除内存缓存,后全部移除磁盘缓存 [_memoryCache removeAllObjects]; [_diskCache removeAllObjects]; }
|
从上面的接口实现可以看出:在YYCache中,永远都是先访问内存缓存,然后再访问磁盘缓存(包括了写入,读取,查询,删除缓存的操作)。而且关于内存缓存(_memoryCache)的操作,是不存在block回调的。
在读取缓存的操作中,如果在内存缓存中无法获取对应的缓存,则会去磁盘缓存中寻找。如果在磁盘缓存中找到了对应的缓存,则会将该对象再次写入内存缓存中,保证在下一次尝试获取同一缓存时能够在内存中就能返回,提高速度。
YYMemoryCache
NSCache 内存缓存不足
通常一个缓存是由内存缓存和磁盘缓存组成,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储。相对于磁盘缓存来说,内存缓存的设计要更简单些,下面是我调查的一些常见的内存缓存。
NSCache 是苹果提供的一个简单的内存缓存,它有着和 NSDictionary 类似的 API,不同点是它是线程安全的,并且不会 retain key。我在测试时发现了它的几个特点:NSCache 底层并没有用 NSDictionary 等已有的类,而是直接调用了 libcache.dylib,其中线程安全是由 pthread_mutex 完成的。另外,它的性能和 key 的相似度有关,如果有大量相似的 key (比如 “1”, “2”, “3”, …),NSCache 的存取性能会下降得非常厉害,大量的时间被消耗在 CFStringEqual() 上,不知这是不是 NSCache 本身设计的缺陷。
YYMemoryCache
YYMemoryCache负责处理容量小,相对高速的内存缓存:它将需要缓存的对象与传入的key关联起来,操作类似于NSCache。
但是与NSCache不同的是,YYMemoryCache的内部有:
- 缓存淘汰算法:使用LRU(least-recently-used) 算法来淘汰(清理)使用频率较低的缓存。
- 缓存清理策略:使用三个维度来标记,分别是count(缓存数量),cost(开销),age(距上一次的访问时间)。YYMemoryCache提供了分别针对这三个维度的清理缓存的接口。用户可以根据不同的需求(策略)来清理在某一维度超标的缓存。
一个是淘汰算法,另一个是清理维度,乍一看可能没什么太大区别。我在这里先简单区分一下:
缓存淘汰算法的目的在于区分出使用频率高和使用频率低的缓存,当缓存数量达到一定限制的时候会优先清理那些使用频率低的缓存。因为使用频率已经比较低的缓存在将来的使用频率也很有可能会低。
缓存清理维度是给每个缓存添加的标记:
- 如果用户需要删除age(距上一次的访问时间)超过1天的缓存,在YYMemoryCache内部,就会从使用频率最低的那个缓存开始查找,直到所有距上一次的访问时间超过1天的缓存都清理掉为止。
- 如果用户需要将缓存总开销清理到总开销小于或等于某个值,在YYMemoryCache内部,就会从使用频率最低的那个缓存开始清理,直到总开销小于或等于这个值。
- 如果用户需要将缓存总数清理到总开销小于或等于某个值,在YYMemoryCache内部,就会从使用频率最低的那个缓存开始清理,直到总开销小于或等于这个值。
可以看出,无论是以哪个维度来清理缓存,都是从缓存使用频率最低的那个缓存开始清理。而YYMemoryCache保留的所有缓存的使用频率的高低,是由LRU这个算法决定的。
现在知道了这二者的区别,下面来具体讲解一下缓存淘汰算法和缓存清理策略:
YYMemoryCache的缓存淘汰算法
LRU算法
在YYMemoryCache中,使用了双向链表
这个数据结构来保存这些缓存:
- 当写入一个新的缓存时,要把这个缓存节点放在链表头部,并且并且原链表头部的缓存节点要变成现在链表的第二个缓存节点。
- 当访问一个已有的缓存时,要把这个缓存节点移动到链表头部,原位置两侧的缓存要接上,并且原链表头部的缓存节点要变成现在链表的第二个缓存节点。
- (根据清理维度)自动清理缓存时,要从链表的最后端逐个清理。
这样一来,就可以保证链表前端的缓存是最近写入过和经常访问过的。而且该算法总是从链表的最后端删除缓存,这也就保证了留下的都是一些“比较新鲜的”缓存。
YYMemoryCache用一个链表节点类来保存某个单独的内存缓存的信息(键,值,缓存时间等),然后用一个双向链表类来保存和管理这些节点。
这两个类的名称分别是:
- _YYLinkedMapNode:链表内的节点类,可以看做是对某个单独内存缓存的封装。
- _YYLinkedMap:双向链表类,用于保存和管理所有内存缓存(节点)
_YYLinkedMapNode
_YYLinkedMapNode可以被看做是对某个缓存的封装:它包含了该节点上一个和下一个节点的指针,以及缓存的key和对应的值(对象),还有该缓存的开销和访问时间。
1 2 3 4 5 6 7 8 9 10 11 12
| @interface _YYLinkedMapNode : NSObject { @package __unsafe_unretained _YYLinkedMapNode *_prev; __unsafe_unretained _YYLinkedMapNode *_next; id _key; id _value; NSUInteger _cost; NSTimeInterval _time; } @end
|
双向链表类
:
_YYLinkedMap
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
| @interface _YYLinkedMap : NSObject { @package CFMutableDictionaryRef _dic; NSUInteger _totalCost; NSUInteger _totalCount; _YYLinkedMapNode *_head; _YYLinkedMapNode *_tail; BOOL _releaseOnMainThread; BOOL _releaseAsynchronously; }
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
- (void)removeNode:(_YYLinkedMapNode *)node;
- (_YYLinkedMapNode *)removeTailNode;
- (void)removeAll;
@end
|
从链表类的属性上看:链表类内置了CFMutableDictionaryRef,用于保存节点的键值对,它还持有了链表内节点的总开销,总数量,头尾节点等数据。
可以参考下面这张图来看一下二者的关系:
PS:(阅读注)
_YYLinkedMap
类中有一个有一个字典记录着缓存对象信息,分别是 Key
与 Node对象
, 包括了一个头结点
与尾节点
的 Node
,形成了一个OC版链表,
_YYLinkedMap的接口的实现:
将节点插入到链表头部:
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
| - (void)insertNodeAtHead:(_YYLinkedMapNode *)node { CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node)); _totalCost += node->_cost; _totalCount++; if (_head) { node->_next = _head; _head->_prev = node; _head = node; } else { _head = _tail = node; } }
|
要看懂节点操作的代码只要了解双向链表的特性即可。在双向链表中:
- 每个节点都有两个分别指向前后节点的指针。所以说每个节点都知道它前一个节点和后一个节点是谁。
- 链表的头部节点指向它前面节点的指针为空;链表尾部节点指向它后侧节点的指针也为空。
为了便于理解,我们可以把这个抽象概念类比于幼儿园手拉手的小朋友们:
每个小朋友的左手都拉着前面小朋友的右手;每个小朋友的右手都拉着后面小朋友的左手;
而且最前面的小朋友的左手和最后面的小朋友的右手都没有拉任何一个小朋友。
PS:(阅读注)
C
中的链表,头结点指向上一个节点的尾节点,尾节点指向下一个节点的头结点. 如果 插入时候,会判断缓存字典里面有没有数据,有的话,拿出来插到链表第一位,然后将原来位置的前后节点连接起来
将某个节点移动到链表头部:
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
| - (void)bringNodeToHead:(_YYLinkedMapNode *)node { if (_head == node) return; if (_tail == node) { _tail = node->_prev; _tail->_next = nil; } else { node->_next->_prev = node->_prev; node->_prev->_next = node->_next; } node->_next = _head; node->_prev = nil; _head->_prev = node; _head = node; }
|
再结合链表的图解来看一下:
移除链表中的某个节点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| - (void)removeNode:(_YYLinkedMapNode *)node { CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key)); _totalCost -= node->_cost; _totalCount--; if (node->_next) node->_next->_prev = node->_prev; if (node->_prev) node->_prev->_next = node->_next; if (_head == node) _head = node->_next; if (_tail == node) _tail = node->_prev; }
|
移除并返回尾部的node:
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
| - (_YYLinkedMapNode *)removeTailNode { if (!_tail) return nil; _YYLinkedMapNode *tail = _tail; CFDictionaryRemoveValue(_dic, (__bridge const void *)(_tail->_key)); _totalCost -= _tail->_cost; _totalCount--; if (_head == _tail) { _head = _tail = nil; } else { _tail = _tail->_prev; _tail->_next = nil; } return tail; }
|
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| @interface YYMemoryCache : NSObject
#pragma mark - Attribute
@property (nullable, copy) NSString *name;
@property (readonly) NSUInteger totalCount;
@property (readonly) NSUInteger totalCost;
#pragma mark - Limit
@property NSUInteger countLimit;
@property NSUInteger costLimit;
@property NSTimeInterval ageLimit;
@property NSTimeInterval autoTrimInterval;
@property BOOL shouldRemoveAllObjectsOnMemoryWarning;
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);
@property BOOL releaseOnMainThread;
@property BOOL releaseAsynchronously;
#pragma mark - Access Methods
- (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;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;
|
YYMemoryCache的接口实现
在YYMemoryCache的初始化方法里,会实例化一个_YYLinkedMap的实例来赋给_lru这个成员变量。
1 2 3 4 5 6
| - (instancetype)init{ .... _lru = [_YYLinkedMap new]; ... }
|
然后所有的关于缓存的操作,都要用到_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 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
| - (BOOL)containsObjectForKey:(id)key {
if (!key) return NO; pthread_mutex_lock(&_lock); BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key)); pthread_mutex_unlock(&_lock); return contains; }
- (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); return node ? node->_value : nil; }
- (void)setObject:(id)object forKey:(id)key { [self setObject:object forKey:key withCost:0]; }
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost { if (!key) return; if (!object) { [self removeObjectForKey:key]; return; } pthread_mutex_lock(&_lock); _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]; } if (_lru->_totalCost > _costLimit) { dispatch_async(_queue, ^{ [self trimToCost:_costLimit]; }); } if (_lru->_totalCount > _countLimit) { _YYLinkedMapNode *node = [_lru removeTailNode]; if (_lru->_releaseAsynchronously) { dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); dispatch_async(queue, ^{ [node class]; }); } else if (_lru->_releaseOnMainThread && !pthread_main_np()) { dispatch_async(dispatch_get_main_queue(), ^{ [node class]; }); } } pthread_mutex_unlock(&_lock); }
- (void)removeObjectForKey:(id)key { if (!key) return; pthread_mutex_lock(&_lock); _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); if (node) { [_lru removeNode:node]; if (_lru->_releaseAsynchronously) { dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); dispatch_async(queue, ^{ [node class]; }); } else if (_lru->_releaseOnMainThread && !pthread_main_np()) { dispatch_async(dispatch_get_main_queue(), ^{ [node class]; }); } } pthread_mutex_unlock(&_lock); }
- (void)removeAllObjects { pthread_mutex_lock(&_lock); [_lru removeAll]; pthread_mutex_unlock(&_lock); }
|
上面的实现是针对缓存的查询,写入,获取操作的,接下来看一下缓存的清理策略。
YYMemoryCache的缓存清理策略
在YYCache中,缓存的清理可以从缓存总数量,缓存总开销,缓存距上一次的访问时间来清理缓存。而且每种维度的清理操作都可以分为自动和手动的方式来进行。
缓存自动清理
缓存的自动清理功能在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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| - (instancetype)init{ ... [self _trimRecursively]; ... }
- (void)_trimRecursively { __weak typeof(self) _self = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ __strong typeof(_self) self = _self; if (!self) return; [self _trimInBackground]; [self _trimRecursively]; }); }
- (void)_trimInBackground { dispatch_async(_queue, ^{ [self _trimToCost:self->_costLimit]; [self _trimToCount:self->_countLimit]; [self _trimToAge:self->_ageLimit]; }); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| - (void)trimToCount:(NSUInteger)count { if (count == 0) { [self removeAllObjects]; return; } [self _trimToCount:count]; }
- (void)trimToCost:(NSUInteger)cost { [self _trimToCost:cost]; }
- (void)trimToAge:(NSTimeInterval)age { [self _trimToAge:age]; }
|
可以看到,YYMemoryCache是按照缓存数量,缓存开销,缓存时间的顺序来自动清空缓存的。
YYDiskCache
YYDiskCache负责处理容量大,相对低速的磁盘缓存。线程安全,支持异步操作。作为YYCache的第二级缓存,它与第一级缓存YYMemoryCache的相同点是:
- 都具有查询,写入,读取,删除缓存的接口。
- 不直接操作缓存,也是间接地通过另一个类(YYKVStorage)来操作缓存。
- 它使用LRU算法来清理缓存。
- 支持按 cost,count 和 age 这三个维度来清理不符合标准的缓存。
它与YYMemoryCache不同点是:
- 根据缓存数据的大小来采取不同的形式的缓存:
- 数据库sqlite: 针对小容量缓存,缓存的data和元数据都保存在数据库里。
- 文件+数据库的形式: 针对大容量缓存,缓存的data写在文件系统里,其元数据保存在数据库里。
- 除了 cost,count 和 age 三个维度之外,还添加了一个磁盘容量的维度。
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
|
- (BOOL)containsObjectForKey:(NSString *)key; - (void)containsObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key, BOOL contains))block;
- (nullable id<NSCoding>)objectForKey:(NSString *)key; - (void)objectForKey:(NSString *)key withBlock:(void(^)(NSString *key, id<NSCoding> _Nullable object))block;
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key; - (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(void(^)(void))block;
- (void)removeObjectForKey:(NSString *)key; - (void)removeObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key))block;
- (void)removeAllObjects; - (void)removeAllObjectsWithBlock:(void(^)(void))block; - (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress endBlock:(nullable void(^)(BOOL error))end;
- (NSInteger)totalCount; - (void)totalCountWithBlock:(void(^)(NSInteger totalCount))block;
- (NSInteger)totalCost; - (void)totalCostWithBlock:(void(^)(NSInteger totalCost))block;
#pragma mark - Trim - (void)trimToCount:(NSUInteger)count; - (void)trimToCount:(NSUInteger)count withBlock:(void(^)(void))block;
- (void)trimToCost:(NSUInteger)cost; - (void)trimToCost:(NSUInteger)cost withBlock:(void(^)(void))block;
- (void)trimToAge:(NSTimeInterval)age; - (void)trimToAge:(NSTimeInterval)age withBlock:(void(^)(void))block;
|
上面的接口代码可以看出,YYDiskCache与YYMemoryCache在接口设计上是非常相似的。但是,YYDiskCache有一个非常重要的属性,它作为用sqlite做缓存还是用文件做缓存的分水岭:
1 2
| @property (readonly) NSUInteger inlineThreshold;
|
这个属性的默认值是20480byte,也就是20kb。即是说,如果缓存数据的长度大于这个值,就使用文件存储;如果小于这个值,就是用sqlite存储。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| - (void)setObject:(id<NSCoding>)object forKey:(NSString *)key { ... NSString *filename = nil; if (_kv.type != YYKVStorageTypeSQLite) { if (value.length > _inlineThreshold) { filename = [self _filenameForKey:key]; } } Lock(); [_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData]; Unlock(); }
|
YYDiskCache相对于YYMemoryCache最大的不同之处是缓存类型的不同。
(saveItemWithKey:value:filename:extendedData:)实际上是属于_kv的。这个_kv就是上面提到的YYKVStorage的实例,它在YYDiskCache的初始化方法里被赋值:
1 2 3 4 5 6 7 8 9 10
| - (instancetype)initWithPath:(NSString *)path inlineThreshold:(NSUInteger)threshold { ... YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type]; if (!kv) return nil; _kv = kv; ... }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| - (BOOL)containsObjectForKey:(NSString *)key { if (!key) return NO; Lock(); BOOL contains = [_kv itemExistsForKey:key]; Unlock(); return contains; }
- (void)removeObjectForKey:(NSString *)key { if (!key) return; Lock(); [_kv removeItemForKey:key]; Unlock(); }
|
YYKVStorage
YYKVStorage实例负责保存和管理所有磁盘缓存。和YYMemoryCache里面的_YYLinkedMap将缓存封装成节点类_YYLinkedMapNode类似,YYKVStorage也将某个单独的磁盘缓存封装成了一个类,这个类就是YYKVStorageItem,它保存了某个缓存所对应的一些信息(key, value, 文件名,大小等等):
1 2 3 4 5 6 7 8 9 10 11 12 13
|
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key; @property (nonatomic, strong) NSData *value; @property (nullable, nonatomic, strong) NSString *filename; @property (nonatomic) int size; @property (nonatomic) int modTime; @property (nonatomic) int accessTime; @property (nullable, nonatomic, strong) NSData *extendedData;
@end
|
PS:阅读注
YYKVStorageItem
作为一个存储文件对象类,记录了文件信息,获取时候,从数据库读取文件信息,赋值然后返回
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 49 50 51 52 53 54 55
|
- (BOOL)saveItem:(YYKVStorageItem *)item;
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(nullable NSString *)filename extendedData:(nullable NSData *)extendedData;
#pragma mark - Remove Items
- (BOOL)removeItemForKey:(NSString *)key;
- (BOOL)removeItemForKeys:(NSArray<NSString *> *)keys;
- (BOOL)removeItemsLargerThanSize:(int)size;
- (BOOL)removeItemsEarlierThanTime:(int)time;
- (BOOL)removeItemsToFitSize:(int)maxSize;
- (BOOL)removeItemsToFitCount:(int)maxCount;
- (BOOL)removeAllItems;
- (void)removeAllItemsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress endBlock:(nullable void(^)(BOOL error))end;
#pragma mark - Get Items
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;
- (nullable NSData *)getItemValueForKey:(NSString *)key;
- (nullable NSArray<YYKVStorageItem *> *)getItemForKeys:(NSArray<NSString *> *)keys;
- (nullable NSDictionary<NSString *, NSData *> *)getItemValueForKeys:(NSArray<NSString *> *)keys;
|
1 2 3 4 5 6 7 8 9 10 11 12
| - (BOOL)saveItem:(YYKVStorageItem *)item;
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(nullable NSString *)filename extendedData:(nullable NSData *)extendedData;
|
这三个接口都比较类似,上面的两个方法都会调用最下面参数最多的方法。
- 首先判断传入的key和value是否符合要求,如果不符合要求,则立即返回NO,缓存失败。
- 再判断是否type==YYKVStorageTypeFile并且文件名为空字符串(或nil):如果是,则立即返回NO,缓存失败。
- 判断filename是否为空字符串:
- 如果不为空:写入文件,并将缓存的key,等信息写入数据库,但是不将key对应的data写入数据库。
- 如果为空:
- 如果缓存类型为YYKVStorageTypeSQLite:将缓存文件删除
- 如果缓存类型不为YYKVStorageTypeSQLite:则将缓存的key和对应的data等其他信息存入数据库。
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 49
| - (BOOL)saveItem:(YYKVStorageItem *)item { return [self saveItemWithKey:item.key value:item.value filename:item.filename extendedData:item.extendedData]; }
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value { return [self saveItemWithKey:key value:value filename:nil extendedData:nil]; }
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData { if (key.length == 0 || value.length == 0) return NO; if (_type == YYKVStorageTypeFile && filename.length == 0) { return NO; } if (filename.length) { if (![self _fileWriteWithName:filename data:value]) { return NO; } if (![self _dbSaveWithKey:key value:value fileName:filename extendedData:extendedData]) { [self _fileDeleteWithName:filename]; return NO; } return YES; } else { if (_type != YYKVStorageTypeSQLite) { NSString *filename = [self _dbGetFilenameWithKey:key]; if (filename) { [self _fileDeleteWithName:filename]; } } return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData]; } }
|
从上面的代码可以看出,在底层写入缓存的方法是_dbSaveWithKey:value:fileName:extendedData:
,这个方法使用了两次:
- 在以文件(和数据库)存储缓存时
- 在以数据库存储缓存时
不过虽然调用了两次,我们可以从传入的参数是有差别的:第二次filename传了nil。那么我们来看一下_dbSaveWithKey:value:fileName:extendedData:内部是如何区分有无filename的情况的:
- 当filename为空时,说明在外部没有写入该缓存的文件:则把data写入数据库里
- 当filename不为空时,说明在外部有写入该缓存的文件:则不把data也写入了数据库里
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 49 50 51 52
| - (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData { NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return NO; int timestamp = (int)time(NULL); sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL); sqlite3_bind_int(stmt, 3, (int)value.length); if (fileName.length == 0) { sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0); } else { sqlite3_bind_blob(stmt, 4, NULL, 0, 0); } sqlite3_bind_int(stmt, 5, timestamp); sqlite3_bind_int(stmt, 6, timestamp); sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0); int result = sqlite3_step(stmt); if (result != SQLITE_DONE) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); return NO; } return YES; }
|
再来看一下获取缓存的操作:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| - (YYKVStorageItem *)getItemForKey:(NSString *)key { if (key.length == 0) return nil; YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO]; if (item) { [self _dbUpdateAccessTimeWithKey:key]; if (item.filename) { item.value = [self _fileReadWithName:item.filename]; if (!item.value) { [self _dbDeleteItemWithKey:key]; item = nil; } } } return item; }
|
从上面这段代码我们可以看到获取YYKVStorageItem的实例的方法是_dbGetItemWithKey:excludeInlineData:
我们来看一下它的实现:
- 首先根据查找key的sql语句生成stmt
- 然后将传入的key与该stmt进行绑定
- 最后通过这个stmt来查找出与该key对应的有关该缓存的其他数据并生成item。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| - (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData { NSString *sql = excludeInlineData ? @"select key, filename, size, modification_time, last_access_time, extended_data from manifest where key = ?1;" : @"select key, filename, size, inline_data, modification_time, last_access_time, extended_data from manifest where key = ?1;"; sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; if (!stmt) return nil; sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); YYKVStorageItem *item = nil; int result = sqlite3_step(stmt); if (result == SQLITE_ROW) { item = [self _dbGetItemFromStmt:stmt excludeInlineData:excludeInlineData]; } else { if (result != SQLITE_DONE) { if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db)); } } return item; }
|
最终生成YYKVStorageItem实例的是通过_dbGetItemFromStmt:excludeInlineData:
来实现的:
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
| - (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData { int i = 0; char *key = (char *)sqlite3_column_text(stmt, i++); char *filename = (char *)sqlite3_column_text(stmt, i++); int size = sqlite3_column_int(stmt, i++); const void *inline_data = excludeInlineData ? NULL : sqlite3_column_blob(stmt, i); int inline_data_bytes = excludeInlineData ? 0 : sqlite3_column_bytes(stmt, i++); int modification_time = sqlite3_column_int(stmt, i++); int last_access_time = sqlite3_column_int(stmt, i++); const void *extended_data = sqlite3_column_blob(stmt, i); int extended_data_bytes = sqlite3_column_bytes(stmt, i++); YYKVStorageItem *item = [YYKVStorageItem new]; if (key) item.key = [NSString stringWithUTF8String:key]; if (filename && *filename != 0) item.filename = [NSString stringWithUTF8String:filename]; item.size = size; if (inline_data_bytes > 0 && inline_data) item.value = [NSData dataWithBytes:inline_data length:inline_data_bytes]; item.modTime = modification_time; item.accessTime = last_access_time; if (extended_data_bytes > 0 && extended_data) item.extendedData = [NSData dataWithBytes:extended_data length:extended_data_bytes]; return item; }
|
上面这段代码分为两个部分:
- 获取数据库里每一个字段对应的数据
- 将数据赋给YYKVStorageItem的实例
需要注意的是:
- 字符串类型需要使用stringWithUTF8String:来转成NSString类型。
- 这里面会判断excludeInlineData:
- 如果为TRUE,就提取存入的data数据
- 如果为FALSE,就不提取
保证线程安全的方案
对于某个设计来说,它的产生一定是基于某种个特定问题下的某个场景的
由上文可以看出:
- YYMemoryCache 使用了 pthread_mutex 线程锁(互斥锁)来确保线程安全
- YYDiskCache 则选择了更适合它的 dispatch_semaphore。
内存缓存操作的互斥锁
在YYMemoryCache中,是使用互斥锁来保证线程安全的。 首先在YYMemoryCache的初始化方法中得到了互斥锁,并在它的所有接口里都加入了互斥锁来保证线程安全,包括setter,getter方法和缓存操作的实现。
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
| - (NSUInteger)totalCost { pthread_mutex_lock(&_lock); NSUInteger totalCost = _lru->_totalCost; pthread_mutex_unlock(&_lock); return totalCost; }
- (void)setReleaseOnMainThread:(BOOL)releaseOnMainThread { pthread_mutex_lock(&_lock); _lru->_releaseOnMainThread = releaseOnMainThread; pthread_mutex_unlock(&_lock); }
- (BOOL)containsObjectForKey:(id)key { if (!key) return NO; pthread_mutex_lock(&_lock); BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key)); pthread_mutex_unlock(&_lock); return contains; }
- (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); return node ? node->_value : nil; }
|
而且需要在dealloc方法中销毁这个锁头:
1 2 3 4 5 6 7
| - (void)dealloc { ... pthread_mutex_destroy(&_lock); }
|
磁盘缓存使用信号量来代替锁
1 2 3 4 5 6 7
| - (instancetype)initWithPath:(NSString *)path inlineThreshold:(NSUInteger)threshold { ... _lock = dispatch_semaphore_create(1); _queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT); ...
|
然后使用了宏来代替加锁解锁的代码
1 2 3
| #define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER) #define Unlock() dispatch_semaphore_signal(self->_lock)
|
信号量:
dispatch_semaphore是GCD用来同步的一种方式,与他相关的共有三个函数,分别是
- dispatch_semaphore_create:定义信号量
- dispatch_semaphore_signal:使信号量+1
- dispatch_semaphore_wait:使信号量-1
当信号量为0时,就会做等待处理,这是其他线程如果访问的话就会让其等待。所以如果信号量在最开始的的时候被设置为1,那么就可以实现“锁”的功能:
- 执行某段代码之前,执行dispatch_semaphore_wait函数,让信号量-1变为0,执行这段代码。
- 此时如果其他线程过来访问这段代码,就要让其等待。
- 当这段代码在当前线程结束以后,执行dispatch_semaphore_signal函数,令信号量再次+1,那么如果有正在等待的线程就可以访问了。
如果有多个线程等待,那么后来信号量恢复以后访问的顺序就是线程遇到dispatch_semaphore_wait的顺序。
这也就是信号量和互斥锁的一个区别:互斥量用于线程的互斥,信号线用于线程的同步。
PS:(互斥锁与信号量区别)
“信号量用在多线程多任务同步的,一个线程完成了某一个动作就通过信号量告诉别的线程,别的线程再进行某些动作(大家都在semtake的时候,就阻塞在 哪里)。而互斥锁是用在多线程多任务互斥的,一个线程占用了某一个资源,那么别的线程就无法访问,直到这个线程unlock,其他的线程才开始可以利用这 个资源。比如对全局变量的访问,有时要加锁,操作完了,在解锁。有的时候锁和信号量会同时使用的”
也就是说,信号量不一定是锁定某一个资源,而是流程上的概念,比如:有A,B两个线程,B线程要等A线程完成某一任务以后再进行自己下面的步骤,这个任务 并不一定是锁定某一资源,还可以是进行一些计算或者数据处理之类。而线程互斥量则是“锁住某一资源”的概念,在锁定期间内,其他线程无法对被保护的数据进 行操作。在有些情况下两者可以互换。
更形象表达:
虽然 Mutex和Semaphore 在一定程度上可以互相替代,比如你可以把 值最大为1 的Semaphore当Mutex用,也可以用Mutex+计数器当Semaphore。但是对于设计理念上还是有不同的,Mutex管理的是资源的使用权,而Semaphore管理的是资源的数量,有那么一点微妙的小区别。打个比方,在早餐餐厅,大家要喝咖啡。如果用Mutex的方式,同时只有一个人可以使用咖啡机,他获得了咖啡机的使用权后,开始做咖啡,其他人只能在旁边等着,直到他做好咖啡后,另外一个人才能获得咖啡机的使用权。如果用Semaphore的模式,服务员会把咖啡做好放到柜台上,谁想喝咖啡就拿走一杯,服务员会不断做咖啡,如果咖啡杯被拿光了,想喝咖啡的人就排队等着。Mutex管理的是咖啡机的使用权,而Semaphore管理的是做好的咖啡数量。
使用锁思路
为什么内存缓存使用互斥锁(pthread_mutex)?
框架作者在最初使用的是自旋锁(OSSpinLock)作为内存缓存的线程锁,但是后来得知其不够安全,所以退而求其次,使用了pthread_mutex。
为什么磁盘缓存使用的是信号量(dispatch_semaphore)?
dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。
因为YYDiskCache在写入比较大的缓存时,可能会有比较长的等待时间,而dispatch_semaphore在这个时候是不消耗CPU资源的,所以比较适合。
提高缓存性能的几个尝试
选择合适的线程锁
如上所说
选择合适的数据结构
在YYMemoryCache中,作者选择了双向链表来保存这些缓存节点。那么可以思考一下,为什么要用双向链表而不是单向链表或是数组呢?
- 为什么不选择单向链表:单链表的节点只知道它后面的节点(只有指向后一节点的指针),而不知道前面的。所以如果想移动其中一个节点的话,其前后的节点不好做衔接。
- 为什么不选择数组:数组中元素在内存的排列是连续的,对于寻址操作非常便利;但是对于插入,删除操作很不方便,需要整体移动,移动的元素个数越多,代价越大。而链表恰恰相反,因为其节点的关联仅仅是靠指针,所以对于插入和删除操作会很便利,而寻址操作缺比较费时。由于在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
| - (void)removeAll { _totalCost = 0; _totalCount = 0; _head = nil; _tail = nil; if (CFDictionaryGetCount(_dic) > 0) { CFMutableDictionaryRef holder = _dic; _dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); if (_releaseAsynchronously) { dispatch_queue_t queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); dispatch_async(queue, ^{ CFRelease(holder); }); } else if (_releaseOnMainThread && !pthread_main_np()) { dispatch_async(dispatch_get_main_queue(), ^{ CFRelease(holder); }); } else { CFRelease(holder); } } }
static inline dispatch_queue_t YYMemoryCacheGetReleaseQueue() { return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); }
|
选择底层的类
同样是字典实现,但是作者使用了更底层且快速的CFDictionary而没有用NSDictionary来实现。
异步释放对象的技巧
为了异步将某个对象释放掉,可以通过在GCD的block里面给它发个消息来实现。这个技巧在该框架中很常见,举一个删除一个内存缓存的例子:
首先将这个缓存的node类取出,然后异步将其释放掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| - (void)removeObjectForKey:(id)key { if (!key) return; pthread_mutex_lock(&_lock); _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); if (node) { [_lru removeNode:node]; if (_lru->_releaseAsynchronously) { dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); dispatch_async(queue, ^{ [node class]; } else if (_lru->_releaseOnMainThread && !pthread_main_np()) { dispatch_async(dispatch_get_main_queue(), ^{ [node class]; }); } } pthread_mutex_unlock(&_lock); }
|
为了释放掉这个node对象,在一个异步执行的(主队列或自定义队列里)block里给其发送了class这个消息。
** PS 阅读理解 **
这个线程释放,按我的理解就是,node在执行这个方法后出了作用域,reference 减一,但是block里面调用node,使node 被这个queue Hold 住,reference 加一, 那么,执行完这个block之后,reference count 减一,就达到了再对应线程里面释放目的.如有不对,请指点一下
Reference:
- YYCache官网
- YYCache 设计思路
- 知乎回答 - 信号量与互斥锁区别
- CSDN - 信号量与互斥锁区别
- YYCache源码解析