YYCache阅读学习

YYCache 阅读学习

官网 介绍如下:

高性能 iOS 缓存框架。YYKit 组件之一。

性能:(摘自官网)

iPhone 6 上,内存缓存每秒响应次数 (越高越好):

test15169495589776

iPhone 6 上,磁盘缓存每秒响应次数 (越高越好):

test15169495840565

特性

  • 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 *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");
}
}];

//根据key读取数据
[userInfoCache objectForKey:key withBlock:^(NSString * _Nonnull key, id<NSCoding> _Nonnull object) {
NSLog(@"user name : %@",object);
}];

//根据key移除缓存
[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 整体结构

test15169499260958

  • 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; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key; //缓存key
id _value; //key对应值
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; //是否在主线程释放,默认为NO
BOOL _releaseAsynchronously; //是否在子线程释放,默认为YES
}

//在链表头部插入某节点
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

//将链表内部的某个节点移到链表头部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

//移除某个节点
- (void)removeNode:(_YYLinkedMapNode *)node;

//移除链表的尾部节点并返回它
- (_YYLinkedMapNode *)removeTailNode;

//移除所有节点(默认在子线程操作)
- (void)removeAll;

@end

从链表类的属性上看:链表类内置了CFMutableDictionaryRef,用于保存节点的键值对,它还持有了链表内节点的总开销,总数量,头尾节点等数据。

可以参考下面这张图来看一下二者的关系:

test15169551955682

PS:(阅读注)

_YYLinkedMap 类中有一个有一个字典记录着缓存对象信息,分别是 KeyNode对象, 包括了一个头结点尾节点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 {

//设置该node的值
CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));

//增加开销和总缓存数量
_totalCost += node->_cost;
_totalCount++;

if (_head) {

//如果链表内已经存在头节点,则将这个头节点赋给当前节点的尾指针(原第一个节点变成了现第二个节点)
node->_next = _head;

//将该节点赋给现第二个节点的头指针(此时_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) {

//如果该节点是链表的尾部节点
//1. 将该节点的头指针指向的节点变成链表的尾节点(将倒数第二个节点变成倒数第一个节点,即尾部节点)
_tail = node->_prev;

//2. 将新的尾部节点的尾部指针置空
_tail->_next = nil;

} else {

//如果该节点是链表头部和尾部以外的节点(中间节点)
//1. 将该node的头指针指向的节点赋给其尾指针指向的节点的头指针
node->_next->_prev = node->_prev;

//2. 将该node的尾指针指向的节点赋给其头指针指向的节点的尾指针
node->_prev->_next = node->_next;
}

//将原头节点赋给该节点的尾指针(原第一个节点变成了现第二个节点)
node->_next = _head;

//将当前节点的头节点置空
node->_prev = nil;

//将现第二个节点的头结点指向当前节点(此时_head指向的节点是现第二个节点)
_head->_prev = node;

//将该节点设置为链表的头节点
_head = node;
}

再结合链表的图解来看一下:

test15169561271627

移除链表中的某个节点:

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 {

//除去该node的键对应的值
CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));

//减去开销和总缓存数量
_totalCost -= node->_cost;
_totalCount--;

//节点操作
//1. 将该node的头指针指向的节点赋给其尾指针指向的节点的头指针
if (node->_next) node->_next->_prev = node->_prev;

//2. 将该node的尾指针指向的节点赋给其头指针指向的节点的尾指针
if (node->_prev) node->_prev->_next = node->_next;

//3. 如果该node就是链表的头结点,则将该node的尾部指针指向的节点赋给链表的头节点(第二变成了第一)
if (_head == node) _head = node->_next;

//4. 如果该node就是链表的尾节点,则将该node的头部指针指向的节点赋给链表的尾节点(倒数第二变成了倒数第一)
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 {

//如果不存在尾节点,则返回nil
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
//YYMemoryCache.h
@interface YYMemoryCache : NSObject

#pragma mark - Attribute

//缓存名称,默认为nil
@property (nullable, copy) NSString *name;

//缓存总数量
@property (readonly) NSUInteger totalCount;

//缓存总开销
@property (readonly) NSUInteger totalCost;


#pragma mark - Limit

//数量上限,默认为NSUIntegerMax,也就是无上限
@property NSUInteger countLimit;

//开销上限,默认为NSUIntegerMax,也就是无上限
@property NSUInteger costLimit;

//缓存时间上限,默认为DBL_MAX,也就是无上限
@property NSTimeInterval ageLimit;

//清理超出上限之外的缓存的操作间隔时间,默认为5s
@property NSTimeInterval autoTrimInterval;

//收到内存警告时是否清理所有缓存,默认为YES
@property BOOL shouldRemoveAllObjectsOnMemoryWarning;

//app进入后台是是否清理所有缓存,默认为YES
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;

//收到内存警告的回调block
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);

//进入后台的回调block
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);

//缓存清理是否在后台进行,默认为NO
@property BOOL releaseOnMainThread;

//缓存清理是否异步执行,默认为YES
@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;
}

//写入某个缓存对象,开销默认为0
- (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) {
//如果存在与传入的key值匹配的node,则更新该node的value,cost,time,并将这个node移到链表头部

//更新总cost
_lru->_totalCost -= node->_cost;
_lru->_totalCost += cost;

//更新node
node->_cost = cost;
node->_time = now;
node->_value = object;

//将node移动至链表头部
[_lru bringNodeToHead:node];

} else {

//如果不存在与传入的key值匹配的node,则新建一个node,将key,value,cost,time赋给它,并将这个node插入到链表头部
//新建node,并赋值
node = [_YYLinkedMapNode new];
node->_cost = cost;
node->_time = now;
node->_key = key;
node->_value = object;

//将node插入至链表头部
[_lru insertNodeAtHead:node];
}

//如果cost超过了限制,则进行删除缓存操作(从链表尾部开始删除,直到符合限制要求)
if (_lru->_totalCost > _costLimit) {
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}

//如果total count超过了限制,则进行删除缓存操作(从链表尾部开始删除,删除一次即可)
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]; //hold and release in queue
});
} else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
[node class]; //hold and release in queue
});
}
}
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) {

//内部调用了链表的removeNode:方法
[_lru removeNode:node];
if (_lru->_releaseAsynchronously) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[node class]; //hold and release in queue
});
} else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
[node class]; //hold and release in queue
});
}
}
pthread_mutex_unlock(&_lock);
}


//内部调用了链表的removeAll方法
- (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
//YYMemoryCache.m
- (instancetype)init{

...

//开始定期清理
[self _trimRecursively];

...
}


//递归清理,相隔时间为_autoTrimInterval,在初始化之后立即执行
- (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];

});
}

//清理所有不符合限制的缓存,顺序为:cost,count,age
- (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
//YYMemoryCache.m
- (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
//YYDiskCache.h

- (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
//YYDiskCache.h
@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
//YYDiskCache.m
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {

...
NSString *filename = nil;
if (_kv.type != YYKVStorageTypeSQLite) {
//如果长度大临界值,则生成文件名称,使得filename不为nil
if (value.length > _inlineThreshold) {
filename = [self _filenameForKey:key];
}
}

Lock();
//在该方法内部判断filename是否为nil,如果是,则使用sqlite进行缓存;如果不是,则使用文件缓存
[_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
//YYDiskCache.m
- (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
//YYKVStorageItem.h

@interface YYKVStorageItem : NSObject

@property (nonatomic, strong) NSString *key; //键
@property (nonatomic, strong) NSData *value; //值
@property (nullable, nonatomic, strong) NSString *filename; //文件名
@property (nonatomic) int size; //值的大小,单位是byte
@property (nonatomic) int modTime; //修改时间戳
@property (nonatomic) int accessTime; //最后访问的时间戳
@property (nullable, nonatomic, strong) NSData *extendedData; //extended data

@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
//YYKVStorage.h

//写入某个item
- (BOOL)saveItem:(YYKVStorageItem *)item;

//写入某个键值对,值为NSData对象
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;

//写入某个键值对,包括文件名以及data信息
- (BOOL)saveItemWithKey:(NSString *)key
value:(NSData *)value
filename:(nullable NSString *)filename
extendedData:(nullable NSData *)extendedData;

#pragma mark - Remove Items

//移除某个键的item
- (BOOL)removeItemForKey:(NSString *)key;

//移除多个键的item
- (BOOL)removeItemForKeys:(NSArray<NSString *> *)keys;

//移除大于参数size的item
- (BOOL)removeItemsLargerThanSize:(int)size;

//移除时间早于参数时间的item
- (BOOL)removeItemsEarlierThanTime:(int)time;

//移除item,使得缓存总容量小于参数size
- (BOOL)removeItemsToFitSize:(int)maxSize;

//移除item,使得缓存数量小于参数size
- (BOOL)removeItemsToFitCount:(int)maxCount;

//移除所有的item
- (BOOL)removeAllItems;

//移除所有的item,附带进度与结束block
- (void)removeAllItemsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
endBlock:(nullable void(^)(BOOL error))end;


#pragma mark - Get Items
//读取参数key对应的item
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;

//读取参数key对应的data
- (nullable NSData *)getItemValueForKey:(NSString *)key;

//读取参数数组对应的item数组
- (nullable NSArray<YYKVStorageItem *> *)getItemForKeys:(NSArray<NSString *> *)keys;

//读取参数数组对应的item字典
- (nullable NSDictionary<NSString *, NSData *> *)getItemValueForKeys:(NSArray<NSString *> *)keys;

1
2
3
4
5
6
7
8
9
10
11
12
//写入某个item
- (BOOL)saveItem:(YYKVStorageItem *)item;

//写入某个键值对,值为NSData对象
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;

//写入某个键值对,包括文件名以及data信息
- (BOOL)saveItemWithKey:(NSString *)key
value:(NSData *)value
filename:(nullable NSString *)filename
extendedData:(nullable NSData *)extendedData;

这三个接口都比较类似,上面的两个方法都会调用最下面参数最多的方法。

  1. 首先判断传入的key和value是否符合要求,如果不符合要求,则立即返回NO,缓存失败。
  2. 再判断是否type==YYKVStorageTypeFile并且文件名为空字符串(或nil):如果是,则立即返回NO,缓存失败。
  3. 判断filename是否为空字符串:
  4. 如果不为空:写入文件,并将缓存的key,等信息写入数据库,但是不将key对应的data写入数据库。
  5. 如果为空:
  6. 如果缓存类型为YYKVStorageTypeSQLite:将缓存文件删除
  7. 如果缓存类型不为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];
}
}

// 缓存类型是数据库缓存,把元数据和value写入数据库
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 {

//sql语句
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);

//key
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);

//filename
sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);

//size
sqlite3_bind_int(stmt, 3, (int)value.length);

//inline_data
if (fileName.length == 0) {

//如果文件名长度==0,则将value存入数据库
sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);

} else {

//如果文件名长度不为0,则不将value存入数据库
sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
}

//modification_time
sqlite3_bind_int(stmt, 5, timestamp);

//last_access_time
sqlite3_bind_int(stmt, 6, timestamp);

//extended_data
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
//YYKVSorage.m
- (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];
//如果此时获取文件数据失败,则删除对应的item
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) {
//传入stmt来生成YYKVStorageItem实例
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++);

//判断excludeInlineData
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++);

//将数据赋给item的属性
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的实例

需要注意的是:

  1. 字符串类型需要使用stringWithUTF8String:来转成NSString类型。
  2. 这里面会判断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 {

//将开销,缓存数量置为0
_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); // hold and release in specified queue
});
} else if (_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
CFRelease(holder); // hold and release in specified queue
});
} 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]; //hold and release in queue });
} else if (_lru->_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
//重点部分
[node class]; //hold and release in queue
});
}
}
pthread_mutex_unlock(&_lock);
}

为了释放掉这个node对象,在一个异步执行的(主队列或自定义队列里)block里给其发送了class这个消息。

** PS 阅读理解 **

这个线程释放,按我的理解就是,node在执行这个方法后出了作用域,reference 减一,但是block里面调用node,使node 被这个queue Hold 住,reference 加一, 那么,执行完这个block之后,reference count 减一,就达到了再对应线程里面释放目的.如有不对,请指点一下

Reference:
  1. YYCache官网
  2. YYCache 设计思路
  3. 知乎回答 - 信号量与互斥锁区别
  4. CSDN - 信号量与互斥锁区别
  5. YYCache源码解析
Author

Sylar

Posted on

2019-04-28

Updated on

2020-09-21

Licensed under

Comments