发布于 

YYCache 源码梳理

本篇是对 YYCache 源码阅读过程中的梳理。YYCache 是一个线程安全的高性能 Key-Value 缓存框架。代码质量很高,值得拿来学习。

YYCache 的框架结构

如上是 YYCache 的框架结构图,本篇按照下面的分类来进行梳理:

  • YYCache
  • YYMemoryCache
  • YYDiskCache
  • NSMapTable
  • 如何保证的线程安全

YYCache

YYCache.h 做一个简化:

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
@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 内的增删改查都提供了有无 Block 回调的两种方式。以有 Block 回调来看下,增删改查的实现。

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
- (void)containsObjectForKey:(NSString *)key withBlock:(void (^)(NSString *key, BOOL contains))block {
if (!block) return;

if ([_memoryCache containsObjectForKey:key]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
block(key, YES);
});
} else {
[_diskCache containsObjectForKey:key withBlock:block];
}
}

- (void)objectForKey:(NSString *)key withBlock:(void (^)(NSString *key, id<NSCoding> object))block {
if (!block) return;
id<NSCoding> object = [_memoryCache objectForKey:key];
if (object) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
block(key, object);
});
} else {
[_diskCache objectForKey:key withBlock:^(NSString *key, id<NSCoding> object) {
if (object && ![_memoryCache objectForKey:key]) {
[_memoryCache setObject:object forKey:key];
}
block(key, object);
}];
}
}

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key withBlock:(void (^)(void))block {
[_memoryCache setObject:object forKey:key];
[_diskCache setObject:object forKey:key withBlock:block];
}

- (void)removeObjectForKey:(NSString *)key withBlock:(void (^)(NSString *key))block {
[_memoryCache removeObjectForKey:key];
[_diskCache removeObjectForKey:key withBlock:block];
}

- (void)removeAllObjectsWithBlock:(void(^)(void))block {
[_memoryCache removeAllObjects];
[_diskCache removeAllObjectsWithBlock:block];
}

- (void)removeAllObjectsWithProgressBlock:(void(^)(int removedCount, int totalCount))progress
endBlock:(void(^)(BOOL error))end {
[_memoryCache removeAllObjects];
[_diskCache removeAllObjectsWithProgressBlock:progress endBlock:end];

}

如上实现可以看到:

  • YYCache 每次的增删改查操作都是优先操作**_memoryCache,再操作_diskCache**。
  • 关于回调:
    • -containsObjectForKey:withBlock:-objectForKey:withBlock: 优先以 _memoryCache 的返回数据回调。
    • 其他方法,以 _diskCache 的回调为准。

到这里我们已经知道,YYCache 有哪些对数据项的操作接口;也可以看出在 YYCache 这一层实际上并没有自身去处理数据,而是借助于 _memoryCache_diskCache

依次来看下他们是如何操作数据的。

LRU

前面提到 YYCache 对数据的操作借助于 _memoryCache_diskCache。这里延伸出一个问题:为什么缓存设计框架,需要同时存在内存缓存和磁盘缓存呢?

对于数据有个命中率的概念。所谓命中率,即:

1
命中率 = 命中数 / (命中数 + 未命中数)

命中率是判断一个缓存框架加速效果好坏的重要标准之一。对于缓存框架为什么分内存缓存磁盘缓存,自己的理解是,命中数代表拿到了数据,未命中数据代表没拿到数据;而拿到数据的过程也分快和满,我们知道的是读取内存缓存会比磁盘缓存快很多。借助于这一点,在保证数据命中率的前提下,如果能够尽量使用内存缓存进行操作,是一个很好的提速方案。

当然,随着自我发问,这里也引出了另一个问题:

  • 内存是有限的,怎样在合适的时机将数据放到内存?

YYCache 内部使用了 LRU 来达到这个目的。我们来看下 LRU 的源码实现,以 YYMemoryCache 内实现为例。

YYMemoryCache 是通过一个链表节点类_YYLinkedMapNode)来保存某个单独的内存缓存;再通过一个双向链表类_YYLinkedMap)来保存和管理这些链表节点。依次看下它们的源码接口的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key; // 缓存 key
id _value; // 缓存内容
NSUInteger _cost; // 缓存消耗
NSTimeInterval _time; // 上次访问时间
}
@end

@implementation _YYLinkedMapNode
@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // 存放节点(_YYLinkedMapNode)
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

@implementation _YYLinkedMap
// 方法实现省略...
@end

从分析 LRU 实现角度出发,核心的几个属性分别为:

  • _YYLinkedMapNode
    • _prev:前指针
    • _next:后指针
  • _YYLinkedMap
    • _dic:存放节点
    • _head:头节点
    • _tail:尾节点

带着这些属性,看下具体的源码对双向链表 LRU 的实现过程。

insertNodeAtHead:

将节点插入为头节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
// 将 node 的 key-value 存入 Map 的 _dic
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;
}
}

过程描述:

  • 将 node 的 key-value 存入**_YYLinkedMap**的 _dic
  • 更新 _totalCost_totalCount
  • 若 map 中有 _head,将 node 插到 headNode,并更新 _head
  • 若 map 中无 _head,初始化 _head_tail 为 node

bringNodeToHead:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (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;
}

过程描述:

  • 若已经是 _head,return
  • 若 node 为 _tail,更新 _tail 为 node 前节点。将 node 放入当前头节点前,并修改 _head
  • 若 node 不为 _tail,更新 node 前后节点的 _next_prev(可以理解为先删除 node)。将 node 放入当前头节点前,并修改 _head

removeNode:

1
2
3
4
5
6
7
8
9
10
- (void)removeNode:(_YYLinkedMapNode *)node {
// 在 _dic 中移除 node 对应的 key-value
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;
}

过程描述:

  • 从 Map 的 _dic 中移除 node 对应的 key-value
  • 更新 _totalCost_totalCount
  • 更新 node 后前节点的指针,需判断是否有 _next_prev
  • 若 node 为 _head,更新 _head
  • 若 node 为 _tail,更新 _tail

removeTailNode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (_YYLinkedMapNode *)removeTailNode {
if (!_tail) return nil;
// 取出 _tail,并从 map 的 _dic 中移除 key-value
_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;
}

过程描述:

  • 根据 Map 的 _tail 取出尾节点 node,并从 _dic 中移除 key-value
  • 更新 _totalCost_totalCount
  • 若链表只有一个节点(_head == _tail),置空 _head_tail
  • 否则更新 _tail
  • 返回被移除的尾节点 node

removeAll:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (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); // 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);
}
}
}

过程描述:

  • 重置 _totalCost_totalCount_head_tail
  • 判断是否 _dic 是否有内容,有则继续
  • 若异步释放 _dic,再判断是否主线程释放:
    • 主线程释放
    • YYMemoryCacheGetReleaseQueue() 释放
  • 若主线程释放,判断当前是否在主线程:
    • 不在主线程,放到主线程释放
    • 在主线程,直接释放

LRU 小结

到这里,我们已经完成了针对 node 增删改查的所有操作。在每次的操作过程中,都会根据需要修改链表中的节点和指针。这些也是实现 LRU 的基础。YYCache 中 LRU 的实现依赖于这里的双向链表。

YYMemoryCache

简化后的 YYMemoryCache.h

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 YYMemoryCache : NSObject

@property (nullable, copy) NSString *name;
@property (readonly) NSUInteger totalCount;
@property (readonly) NSUInteger totalCost;

@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;

- (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;

@end

顾名思义的接口,看的很舒服,不需要额外的注释。对应看下实现文件的源码实现。

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (instancetype)init {
self = super.init;
pthread_mutex_init(&_lock, NULL);
_lru = [_YYLinkedMap new];
_queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);

_countLimit = NSUIntegerMax;
_costLimit = NSUIntegerMax;
_ageLimit = DBL_MAX;
_autoTrimInterval = 5.0;
_shouldRemoveAllObjectsOnMemoryWarning = YES;
_shouldRemoveAllObjectsWhenEnteringBackground = YES;

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];

[self _trimRecursively];
return self;
}

其中 _lru 是 YYMemoryCache 与 _YYLinkedMap 的一个联系点,所有的增删改查操作,都需要通过 _lru 来实现。

除此之外,我们看到了一些属性的默认值,如:

  • 不限制 _countLimit_costLimit_ageLimit
  • _autoTrimInterval 自动清理时间为 5 s
  • 默认内存警告时,清空内存缓存
  • 默认进入后台时,清空内存缓存
  • 开启自动清理,后面再细说清理过程

增删改查的接口实现

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
- (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]; //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) {
[_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);
}

- (void)removeAllObjects {
pthread_mutex_lock(&_lock);
[_lru removeAll];
pthread_mutex_unlock(&_lock);
}

需要知道的几个点:

  • 我们把增删改查统一称为访问数据
  • 在每次的访问数据开始前,加锁 pthread_mutex_lock(&_lock);
  • 每次访问数据(增、改)时,会更新 _time
  • 在每次的访问数据结束后,解锁 pthread_mutex_unlock(&_lock);

关于锁的内容,放到后面和 YYDiskCache 一起对比分析。

缓存清理策略

我们从 _YYLinkedMapNode 的头文件结构可以知道,每份缓存 node 都带有2个属性:

  • **NSUInteger _cost;**:内存消耗
  • **NSTimeInterval _time;**:最新访问时间

_YYLinkedMap 的头文件结构中可以知道,缓存 map 带有的2个属性:

  • **NSUInteger _totalCost;**:总缓存消耗
  • **NSUInteger _totalCount;**:总缓存数

再从 YYMemoryCache 的头文件中,知道了三个参数:

  • NSUInteger countLimit:缓存数量上限值
  • NSUInteger costLimit:缓存消耗上限值
  • NSTimeInterval ageLimit:缓存访问时间距离现在最久允许值

由这些维护,我们大概知道了缓存清理策略的过程,及清理依据。具体看下代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
- (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];
});
}

过程描述:

  • _trimRecursively
    • 初始化 YYMemoryCache 后即开始递归调用
    • _autoTrimInterval 自动清理时间默认为 5s
  • _trimInBackground
    • 根据 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
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
- (void)_trimToCost:(NSUInteger)costLimit {
BOOL finish = NO;
pthread_mutex_lock(&_lock);
if (costLimit == 0) {
[_lru removeAll];
finish = YES;
} else if (_lru->_totalCost <= costLimit) {
finish = YES;
}
pthread_mutex_unlock(&_lock);
if (finish) return;

NSMutableArray *holder = [NSMutableArray new];
while (!finish) {
if (pthread_mutex_trylock(&_lock) == 0) {
if (_lru->_totalCost > costLimit) {
_YYLinkedMapNode *node = [_lru removeTailNode];
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&_lock);
} else {
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
});
}
}

- (void)_trimToCount:(NSUInteger)countLimit {
BOOL finish = NO;
pthread_mutex_lock(&_lock);
if (countLimit == 0) {
[_lru removeAll];
finish = YES;
} else if (_lru->_totalCount <= countLimit) {
finish = YES;
}
pthread_mutex_unlock(&_lock);
if (finish) return;

NSMutableArray *holder = [NSMutableArray new];
while (!finish) {
if (pthread_mutex_trylock(&_lock) == 0) {
if (_lru->_totalCount > countLimit) {
_YYLinkedMapNode *node = [_lru removeTailNode];
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&_lock);
} else {
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
});
}
}

- (void)_trimToAge:(NSTimeInterval)ageLimit {
BOOL finish = NO;
NSTimeInterval now = CACurrentMediaTime();
pthread_mutex_lock(&_lock);
if (ageLimit <= 0) {
[_lru removeAll];
finish = YES;
} else if (!_lru->_tail || (now - _lru->_tail->_time) <= ageLimit) {
finish = YES;
}
pthread_mutex_unlock(&_lock);
if (finish) return;

NSMutableArray *holder = [NSMutableArray new];
while (!finish) {
if (pthread_mutex_trylock(&_lock) == 0) {
if (_lru->_tail && (now - _lru->_tail->_time) > ageLimit) {
_YYLinkedMapNode *node = [_lru removeTailNode];
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&_lock);
} else {
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
});
}
}

过程梳理:

  • _trimToCost:
    • 非空判断
    • 若有缓存,判断 _lru->_totalCost > costLimit,从链表尾部依次取出 nodes
    • 根据 _lru->_releaseOnMainThread 判断释放线程,释放 nodes
  • _trimToCount:
    • 非空判断
    • 若有缓存,判断 _lru->_totalCount > countLimit,从链表尾部依次取出 nodes
    • 根据 _lru->_releaseOnMainThread 判断释放线程,释放 nodes
  • _trimToAge:
    • 非空判断
    • 若有缓存,判断 _lru->_tail && (now - _lru->_tail->_time) > ageLimit,从尾节点依次向头节点判断,若尾节点的时间不满足,则取出加入 nodes;直到第一个满足时间要求的节点为止。(本身链表的排序也是按照 time 来的,越旧未访问的内容,越靠近尾部)
    • 根据 _lru->_releaseOnMainThread 判断释放线程,释放 nodes

自己的思考:

  • _trimToAge: 中的 age 指的是最近访问时间,而不是创建时间。带着这个认知,再看 _trimToAge: 的实现,思路就会很清楚了。

缓存清理策略小结

缓存清理策略的维度:

  • cost:缓存消耗
  • count:缓存数量
  • age:缓存的最近访问时间

缓存清理的方式:

  • 默认支持自动缓存清理
  • 也支持手动清理

YYDiskCache

简化后的 YYDiskCache.h

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
@interface YYDiskCache : NSObject

@property (nullable, copy) NSString *name;
@property (readonly) NSString *path;
@property (readonly) NSUInteger inlineThreshold;
@property (nullable, copy) NSData *(^customArchiveBlock)(id object);
@property (nullable, copy) id (^customUnarchiveBlock)(NSData *data);
@property (nullable, copy) NSString *(^customFileNameBlock)(NSString *key);

@property NSUInteger countLimit;
@property NSUInteger costLimit;
@property NSTimeInterval ageLimit;
@property NSUInteger freeDiskSpaceLimit;
@property NSTimeInterval autoTrimInterval;
@property BOOL errorLogsEnabled;

#pragma mark - Initializer
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
- (nullable instancetype)initWithPath:(NSString *)path;
- (nullable instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold NS_DESIGNATED_INITIALIZER;


#pragma mark - Access Methods
- (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;


#pragma mark - Extended Data
+ (nullable NSData *)getExtendedDataFromObject:(id)object;
+ (void)setExtendedData:(nullable NSData *)extendedData toObject:(id)object;

@end

需要注意的点:

  1. YYDiskCache 禁用了 init 和 new 的初始化方法。通过 UNAVAILABLE_ATTRIBUTE,将这两种初始化方法设为私有。
  2. NS_DESIGNATED_INITIALIZER
    • 为什么用这个宏?
      • 为了告诉调用者需要用这个方法来初始化类对象
    • 使用的注意事项:
      • 如果子类指定了新的初始化器,在这个初始化器的内部必须调用父类 Designated Initializer,并重写父类 Designated Initializer,将其指向子类新的初始化器。例如:
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        - (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;

        - (instancetype)init {
        return [self initWithName:@""];
        }

        - (instancetype)initWithName:(NSString *)name {
        self = [super init];
        if (self) {
        // do something
        }
        return self;
        }
  3. 配合使用
    • 配合使用 UNAVAILABLE_ATTRIBUTENS_DESIGNATED_INITIALIZER,目的在于指定初始化方法。格式如下:
      1
      2
      3
      4
      + (instancetype)new NS_UNAVAILABLE;
      - (instancetype)init NS_UNAVAILABLE;

      - (instancetype)initWithName:(NSString *)name NS_DESIGNATED_INITIALIZER;

接着对应看下实现文件的源码实现。

初始化

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
- (instancetype)initWithPath:(NSString *)path {
return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
}

- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
self = [super init];
if (!self) return nil;

YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
if (globalCache) return globalCache;

YYKVStorageType type;
if (threshold == 0) {
type = YYKVStorageTypeFile;
} else if (threshold == NSUIntegerMax) {
type = YYKVStorageTypeSQLite;
} else {
type = YYKVStorageTypeMixed;
}

YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
if (!kv) return nil;

_kv = kv;
_path = path;
_lock = dispatch_semaphore_create(1);
_queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);
_inlineThreshold = threshold;
_countLimit = NSUIntegerMax;
_costLimit = NSUIntegerMax;
_ageLimit = DBL_MAX;
_freeDiskSpaceLimit = 0;
_autoTrimInterval = 60;

[self _trimRecursively];
_YYDiskCacheSetGlobal(self);

[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
return self;
}

其中 _kv 是 YYMemoryCache 与 YYKVStorage 的一个联系点,所有的增删改查操作,都需要通过 _kv 来实现。我们看下 YYKVStorage 内部的是怎么操作数据的。

另一个是 DiskCache 真实的存储分为文件和 SQLite 存储,在初始化时,可以指定存储方式。即设置:

1
@property (readonly) NSUInteger inlineThreshold; // 默认 1024 * 20,20kb

KVStorage

YYKVStorageType

1
2
3
4
5
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
YYKVStorageTypeFile = 0,
YYKVStorageTypeSQLite = 1,
YYKVStorageTypeMixed = 2,
};

梳理:

  • YYKVStorageTypeFile:在 SQLite 中直接存文件的路径,不在 SQLite 中存要存储的值
  • YYKVStorageTypeSQLite:只在 SQLite 中存要存储的值
  • YYKVStorageTypeMixed:根据文件大小来确定要存储的值存放形式(File 或 SQLite),默认使用它。
  • 选用通过 inlineThreshold 确定。

YYKVStorageItem 与 YYKVStorage

YYKVStorage 类似于 MemoryCache 里面的 node。DiskCache 每一项被封装成了 YYKVStorageItem 实例,再通过 YYKVStorage 来管理 YYKVStorageItem。

YYKVStorageItem 结构如下:

1
2
3
4
5
6
7
8
9
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key; ///< key
@property (nonatomic, strong) NSData *value; ///< value
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
@property (nonatomic) int size; ///< value's size in bytes
@property (nonatomic) int modTime; ///< 修改时间戳
@property (nonatomic) int accessTime; ///< 最后访问时间戳
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end

KVStorage 的接口:

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
#pragma mark - Save Items
- (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 YYKVStorageItem *)getItemInfoForKey:(NSString *)key;
- (nullable NSData *)getItemValueForKey:(NSString *)key;
- (nullable NSArray<YYKVStorageItem *> *)getItemForKeys:(NSArray<NSString *> *)keys;
- (nullable NSArray<YYKVStorageItem *> *)getItemInfoForKeys:(NSArray<NSString *> *)keys;
- (nullable NSDictionary<NSString *, NSData *> *)getItemValueForKeys:(NSArray<NSString *> *)keys;

Save/Update 的过程

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
- (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];
}
}

过程描述:

  • 校验 key value
  • type 为 YYKVStorageTypeFile 时,校验 filename 是否存在
  • 判断是否有 filename(**_kv.type != YYKVStorageTypeSQLite** && value.length > _inlineThreshold时创建 filename)
    • 疑问:如果 YYKVStorageTypeFile 的话,依旧会按照上面进行文件大小判断,再创建文件名
    • 若有 filename,value 以 filename 写入文件;文件写入成功后,将相关数据写入数据库 db
    • 若没有 filename,且 type 不为 YYKVStorageTypeSQLite
      • 从 db 查询 filename,若有,清空对应文件
      • 将相关数据写入 db,只是不写入文件

Remove 的过程

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
- (BOOL)removeItemForKey:(NSString *)key {
if (key.length == 0) return NO;
switch (_type) {
case YYKVStorageTypeSQLite: {
return [self _dbDeleteItemWithKey:key];
} break;
case YYKVStorageTypeFile:
case YYKVStorageTypeMixed: {
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
return [self _dbDeleteItemWithKey:key];
} break;
default: return NO;
}
}

- (BOOL)removeItemForKeys:(NSArray *)keys {
if (keys.count == 0) return NO;
switch (_type) {
case YYKVStorageTypeSQLite: {
return [self _dbDeleteItemWithKeys:keys];
} break;
case YYKVStorageTypeFile:
case YYKVStorageTypeMixed: {
NSArray *filenames = [self _dbGetFilenameWithKeys:keys];
for (NSString *filename in filenames) {
[self _fileDeleteWithName:filename];
}
return [self _dbDeleteItemWithKeys:keys];
} break;
default: return NO;
}
}

- (BOOL)removeAllItems {
if (![self _dbClose]) return NO;
[self _reset];
if (![self _dbOpen]) return NO;
if (![self _dbInitialize]) return NO;
return YES;
}

- (void)removeAllItemsWithProgressBlock:(void(^)(int removedCount, int totalCount))progress
endBlock:(void(^)(BOOL error))end {

int total = [self _dbGetTotalItemCount];
if (total <= 0) {
if (end) end(total < 0);
} else {
int left = total;
int perCount = 32;
NSArray *items = nil;
BOOL suc = NO;
do {
items = [self _dbGetItemSizeInfoOrderByTimeAscWithLimit:perCount];
for (YYKVStorageItem *item in items) {
if (left > 0) {
if (item.filename) {
[self _fileDeleteWithName:item.filename];
}
suc = [self _dbDeleteItemWithKey:item.key];
left--;
} else {
break;
}
if (!suc) break;
}
if (progress) progress(total - left, total);
} while (left > 0 && items.count > 0 && suc);
if (suc) [self _dbCheckpoint];
if (end) end(!suc);
}
}

部分 Remove 的过程,以 removeAllItemsWithProgressBlock:endBlock: 为例:

  • 若无缓存,结束,end block
  • 若有缓存,每次从 db 最多取出 32 个缓存项,遍历移除文件和 db 内容
  • 每删除 32 个缓存,回调一次 progressBlock(progress = (total - left)/total)
  • 删除完成 endBlock

Get 的过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (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;
}

getItemForKey: 为例:

  • 校验 key
  • 从 db 中根据 key 取出 YYKVStorageItem 格式的数据
  • 更新 db 中这条数据的时间
  • 如果有 filename,以 filename 从文件中取出 value 并返回给 item.value
  • 若 item.value 为空,从 db 中移除这条无效缓存
  • 返回 item

YYDiskCache 的对应处理

了解了 YYKVStorage 的存取实现逻辑后,回过头再来看下上层 YYDiskCache 是如何实现对应方法的。

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
- (BOOL)containsObjectForKey:(NSString *)key {
if (!key) return NO;
Lock();
BOOL contains = [_kv itemExistsForKey:key];
Unlock();
return contains;
}

- (id<NSCoding>)objectForKey:(NSString *)key {
if (!key) return nil;
Lock();
YYKVStorageItem *item = [_kv getItemForKey:key];
Unlock();
if (!item.value) return nil;

id object = nil;
if (_customUnarchiveBlock) {
object = _customUnarchiveBlock(item.value);
} else {
@try {
object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
}
@catch (NSException *exception) {
// nothing to do...
}
}
if (object && item.extendedData) {
[YYDiskCache setExtendedData:item.extendedData toObject:object];
}
return object;
}

- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
if (!key) return;
if (!object) {
[self removeObjectForKey:key];
return;
}

NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];
NSData *value = nil;
if (_customArchiveBlock) {
value = _customArchiveBlock(object);
} else {
@try {
value = [NSKeyedArchiver archivedDataWithRootObject:object];
}
@catch (NSException *exception) {
// nothing to do...
}
}
if (!value) return;
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();
}

列出了 -containsObjectForKey:objectForKey:setObject:forKey: 方法对应的源码。在每次的 _kv 操作时,都会进行加锁和解锁。

这里使用了宏来代替加锁解锁的代码:

1
2
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)

后面结合 YYMemoryCache,一起看下两种锁的区别。

NSMapTable

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

  • key-value 可以选择弱持有,以便于在回收其中一个对象时删除对应条目。
  • 它可以包含任意指针(其内容不被约束为对象)。
  • 我们可以将 NSMapTable 实例配置为对任意指针进行操作,而不仅仅是对象。

官方文档:NSMapTable 官方文档

YYDiskCache 内部是基于一个单例 NSMapTable 管理的。

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
/// weak reference for all instances
static NSMapTable *_globalInstances; // _globalInstances 管理所有 YYDiskCache 实例
static dispatch_semaphore_t _globalInstancesLock; // 使用 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 == 0) return 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 == 0) return;
_YYDiskCacheInitGlobal();
dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
[_globalInstances setObject:cache forKey:cache.path];
dispatch_semaphore_signal(_globalInstancesLock);
}

每当初始化 YYDiskCache 时,先到 NSMapTable 中获取对应 path 的 YYDiskCache 实例。如果获取不到,会重新初始化一个 YYDiskCache 实例,并且将其引用在 NSMapTable 中,这样做也会提升不少性能。

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
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
// ...

// 先从 NSMapTable 单例中通过 path 获取 YYDiskCache 实例
// 如果获取到,直接返回该实例
YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
if (globalCache) return globalCache;

YYKVStorageType type;
if (threshold == 0) {
type = YYKVStorageTypeFile;
} else if (threshold == NSUIntegerMax) {
type = YYKVStorageTypeSQLite;
} else {
type = YYKVStorageTypeMixed;
}

// 弱没有获取到,初始化一个新的 YYDiskCache 实例
YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
if (!kv) return nil;

_kv = kv;
_path = path;
_lock = dispatch_semaphore_create(1);
_queue = dispatch_queue_create("com.ibireme.cache.disk", DISPATCH_QUEUE_CONCURRENT);
_inlineThreshold = threshold;
_countLimit = NSUIntegerMax;
_costLimit = NSUIntegerMax;
_ageLimit = DBL_MAX;
_freeDiskSpaceLimit = 0;
_autoTrimInterval = 60;

[self _trimRecursively];

// 向 NSMapTable 单例注册新生成的 YYDiskCache 实例
_YYDiskCacheSetGlobal(self);

// ...
return self;
}

如何保证的线程安全

通过前面的代码分析,我们已经知道了 YYCache 在增删改查的过程中,会进行加解锁操作。锁的目的也是为了保证线程的安全。分别看下 MemoryCache 和 DiskCache 分别是怎么实现线程安全的。

YYMemoryCache 线程安全处理

YYMemoryCache 使用了 pthread_mutex 线程锁来确保线程安全。ibireme 使用 pthread_mutex 的缘由:不再安全的 OSSpinLock。简单言之,处于安全考虑选用了现在的 pthread_mutex

pthread_mutex 的基本用法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 申明一个互斥锁
pthread_mutex_t _lock;

// 初始化
pthread_mutex_init(&_lock,NULL);

// 使用 _lock 之前一定要初始化,否则不生效
// 锁定互斥锁
pthread_mutex_lock(&_lock);

// 解除锁定互斥锁
pthread_mutex_unlock(&_lock);

// pthread_mutex_lock() 的非阻塞版本
// 如果 _lock 所引用的互斥对象被任何线程(包括当前线程)锁定,将立刻返回该调用
// 否则该互斥锁将被锁定,调用线程为当前线程
// pthread_mutex_lock() 成功锁定后会返回 0,返回`其他任何值`表示`出错`
pthread_mutex_trylock(&_lock);

// 销毁互斥锁
pthread_mutex_destroy(&_lock);

pthread_mutex 表示互斥锁。互斥锁在申请锁时,调用了 pthread_mutex_lock 方法,会导致线程休眠。

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
- (instancetype)init {
self = super.init;
pthread_mutex_init(&_lock, NULL);
// ...
}

- (void)dealloc {
// ...
pthread_mutex_destroy(&_lock);
}

- (void)_trimToCount:(NSUInteger)countLimit {
BOOL finish = NO;
pthread_mutex_lock(&_lock);
if (countLimit == 0) {
[_lru removeAll];
finish = YES;
} else if (_lru->_totalCount <= countLimit) {
finish = YES;
}
pthread_mutex_unlock(&_lock);
if (finish) return;

NSMutableArray *holder = [NSMutableArray new];
while (!finish) {
if (pthread_mutex_trylock(&_lock) == 0) {
if (_lru->_totalCount > countLimit) {
_YYLinkedMapNode *node = [_lru removeTailNode];
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&_lock);
} else {
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
});
}
}

我们看到 YYCache 中每次对 _lru 操作时都会进行锁定和解除。也用到了 pthread_mutex_trylock(&_lock)

YYDiskCache 线程安全处理

YYDiskCache 使用了信号量(dispatch_semaphore)来保证线程的安全。Dispatch Semaphore 是持有计数的信号,计数为 0 时等待,计数 >= 1 时放行。

dispatch_semaphore 的基本用法:

  • dispatch_semaphore_create 可以生成信号量,参数 value 是信号量计数的初始值;
  • dispatch_semaphore_wait 会让信号量值 -1,当信号量值为 0 时进入等待(直到超时),否则正常执行;
  • dispatch_semaphore_signal 会让信号量值 +1,如果有通过 dispatch_semaphore_wait 函数等待 Dispatch Semaphore 的计数值增加的线程,会由系统唤醒最先等待的线程执行。

YYDiskCache 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@implementation YYDiskCache {
YYKVStorage *_kv;
dispatch_semaphore_t _lock;
dispatch_queue_t _queue;
}

- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
// ...
_lock = dispatch_semaphore_create(1);
// ...
}

// 通过宏来替换加锁解锁代码
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)

- (BOOL)containsObjectForKey:(NSString *)key {
if (!key) return NO;
Lock();
BOOL contains = [_kv itemExistsForKey:key];
Unlock();
return contains;
}

可以看到 YYDiskCache 中每次对 _kv 操作时,都会进行加解锁操作。

为什么线程安全的处理方式不同?

先了解一个概念,信号量互斥锁的区别:

  • 互斥锁用户用于线程的互斥,信号量用于线程的同步。
  • 同步和互斥是针对线程的。同步允许多线程按序访问;互斥只允许单个线程访问。
  • 互斥:指某个资源同时只允许一个访问者对其访问,具有排他性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
  • 同步:指在互斥的基础上,通过其他机制实现访问者对资源的有序访问。即信号量可以让多个线程有序访问某个资源。

YYDiskCache 在写入较大缓存时,会有较长的等待时间,而 dispatch_semaphore 是不消耗 CPU 资源的,所以选用信号量。

总结

  • YYCache 框架结构
    • YYCache
    • YYMemoryCache
      • _YYLinkedMap
      • _YYLinkedMapNode
    • YYDiskCache
      • YYKVStorage
      • YYKVStorageItem
  • YYCache
    • 增删改查方法
    • 先操作 _memoryCache,再操作 _diskCache
    • 回调以 _diskCache 为准
    • 这一层没有真实的做数据处理
  • LRU
    • Least Recently Used 最近最少使用
    • 缓存命中率
    • MemoryCache LRU 的实现
      • 通过双向链表类_YYLinkedMap,保存和管理链表节点 _YYLinkedMapNode
      • _YYLinkedMapNode
        • _prev
        • _next
        • value
      • _YYLinedMap
        • _dic:存放链表节点
        • _head:头节点
        • _tail:尾节点
      • 每次数据访问时,将被访问节点调整到 head 位置
  • YYMemoryCache
    • 初始化
      • pthread_mutex 互斥锁
      • 三个缓存管理维度(count、cost、age)
      • 自动清理时间 5s
      • 默认内存警告、进入后台时,清空 memoryCache
    • 增删改查
      • 每次数据访问
      • pthread_mutex_lock 加锁
      • 访问修改数据,更新相关属性(time 等)
      • pthread_mutex_unlock 解锁
    • 缓存清理
      • 手动或自动
      • 三个维度(count、cost、age)
      • 从双向链表尾部开始清理
  • YYDiskCache
    • 初始化
      • 内有一单例 NSMapTable
      • 先通过 path 从 NSMapTable 中查是否有对应实例,有则用,无责新建并存
      • _kv 实例,类似 MemoryCache 的 _dic
      • dispatch_semaphore 信号量,自旋锁
      • 三个缓存维度(count、cost、age)
      • 自动清理
    • 增删改查
      • YYKVStorageType
        • File
        • SQLite
        • Mixed
      • YYKVStorage 管理 YYKVStorageItem
      • 增删改查过程中既要修改文件、又要修改 SQLite 的数据
      • dispatch_semaphore_wait 加锁
      • 处理数据
      • dispatch_semaphore_signal 解锁
    • NSMapTable
      • NSHashTable 更广泛意义的 NSSet、NSMutableSet
      • NSMapTable 更广泛意义的 NSDictionary、NSMutableDictionary
      • 可以存储指针,内容不被约束为对象
    • 缓存清理
      • 类似memorycache
  • 线程安全处理
    • pthread_mutex
    • dispatch_semaphore

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本站由 @JonyFang 创建,使用 Stellar 作为主题,您可以在 GitHub 找到本站源码。