YYCache源码阅读:YYMemoryCache

一直想写一写自己读源码的感受与理解,可惜一直未付诸行动。最近闲来无事,索性整理一下,记录于此.

YYCache

YYCahce 包括 内存缓存(YYMemoryCache) 与 磁盘缓存(YYDiskCache), 而且都是线程安全的!

YYMemoryCache 内存缓存

YYMemoryCache用于对内存缓存进行管理,因为需要高效查询数据,所以 key-value 模式储存。为什么不直接使用字典呢,因为需要保证线程安全。而为什么不直接使用 NSCache 呢,因为 YYMemoryCache 基于自定义的双向链表,提供一种 LRU(least-recently-used: 最近最久少使用的会先删除) 淘汰算法去操作缓存,进行性能优化。

_YYLinkedMap

为了实现算法,YYMemoryCache 内部封装了一个 _YYLinkedMap 的双向链表

1
2
3
4
5
6
7
8
9
10
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // 哈希字典,存放 (key : _YYLinkedMapNode)
NSUInteger _totalCost; // 缓存占用内存总大小
NSUInteger _totalCount; // 缓存节点个数
_YYLinkedMapNode *_head; // 头结点,最近使用最多的节点 MRU
_YYLinkedMapNode *_tail; // 尾节点,最近使用最少的节点 LRU
BOOL _releaseOnMainThread; // 在主线程释放_dic
BOOL _releaseAsynchronously; // 在异步线程释放_dic
}

_YYLinkedMapNode 链表节点数据

1
2
3
4
5
6
7
8
9
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // 指向前一个节点的指针
__unsafe_unretained _YYLinkedMapNode *_next; // 指向后一个节点的指针
id _key; // 缓存数据的key
id _value; // 缓存的数据
NSUInteger _cost; // 节点内存占用大小
NSTimeInterval _time; // 节点操作时间
}
  1. CFMutableDictionaryRef 操作: CoreFoundation 的字典,其性能更好

    1
    2
    3
    4
    5
    6
    7
    8
    /// 初始化
    _dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    /// 存储数据
    CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
    /// 根据key删除数据
    CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));
    /// 获取字典元素个数
    CFDictionaryGetCount(_dic);

2._YYLinkedMap 方法列表:

1
2
3
4
5
6
7
8
9
10
/// 插入头节点
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
/// 将节点设为头结点
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
/// 删除节点
- (void)removeNode:(_YYLinkedMapNode *)node;
/// 删除尾节点
- (_YYLinkedMapNode *)removeTailNode;
/// 删除所有数据
- (void)removeAll;

YYMemoryCache

1
2
3
pthread_mutex_t _lock;
_YYLinkedMap *_lru;
dispatch_queue_t _queue;

pthread_mutex_t 互斥锁,保证线程安全

操作方法:

1
2
3
4
pthread_mutex_init(&_lock, NULL);   // 初始化
pthread_mutex_lock(&_lock); // 加锁
pthread_mutex_unlock(&_lock); // 解锁
pthread_mutex_trylock(&_lock) == 0 // 是否加锁,0:未锁住,其他值:锁住

Init 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
- (instancetype)init {
self = super.init;
// 1. 初始化锁
pthread_mutex_init(&_lock, NULL);
// 2. 初始化lru链表
_lru = [_YYLinkedMap new];
// 3. 创建串行队列
_queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);

// 4. 初始化默认值
_countLimit = NSUIntegerMax;
_costLimit = NSUIntegerMax;
_ageLimit = DBL_MAX;
_autoTrimInterval = 5.0; // 5s 执行一次边界检测
_shouldRemoveAllObjectsOnMemoryWarning = YES; // 内存警告删除所有元素
_shouldRemoveAllObjectsWhenEnteringBackground = YES; // 进入后台是删除所有元素

// 5. 监听内存警告和进入后台的通知
[[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;
}

边界检测:内存检测

YYMemoryCache 有一个边界检测机制,从 YYMemoryCache 初始化就开始工作(类似于定时器,隔一段时间就执行一次操作,但不是使用定时器),使用递归调用

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]; // 遍历节点,删除和now相差 _ageLimit 的节点
});
}

_trimToCost _trimToCount

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)_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); //休息10s,在进行检测是否可以获得锁
}
}
if (holder.count) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[holder count]; // 在这里 释放 node,防止分散控制
});
}
}
// 与 `_trimToCost` 差不多的逻辑
- (void)_trimToCount:(NSUInteger)countLimit {
...
}

这里有个tip,就是让block捕获一个局部变量,然后扔到后台队列去随便发送个消息以避免编译器警告,这样就可以让对象在后台线程销毁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
NSMutableArray *holder = [NSMutableArray new];

if (holder.count) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[holder count]; // release in queue
});
}

// 另外一个简单点的方式
NSArray *tmp = self.array;
self.array = nil;
dispatch_async(queue, ^{
[tmp class];
});

_trimToAge

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)_trimToAge:(NSTimeInterval)ageLimit {
...

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
}
}
...
}

监听内存警告通知/进入后台通知

自动清空内存缓存,调用 removeAllObjects 方法

增删查改

所有操作都需要进行线程安全控制:加锁

  • 增/改

    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)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];
    ...
    }
    pthread_mutex_unlock(&_lock);
    }
  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /// 根据key进行删除
    - (void)removeObjectForKey:(id)key {
    ...
    pthread_mutex_lock(&_lock);
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    if (node) {
    [_lru removeNode:node];
    ...
    }
    pthread_mutex_unlock(&_lock);
    }
    /// 删除所有
    - (void)removeAllObjects;
  • 1
    2
    3
    4
    /// 是否包含某个数据
    - (BOOL)containsObjectForKey:(id)key;
    /// 获取缓存数据: 1.更新节点的时间戳,2.并将节点移到头结点
    - (id)objectForKey:(id)key;

总结

  • YYMemoryCache 操作了内存缓存, 相较于磁盘缓存需要进行I/O操作, 在性能上快很多, 因此YYCache访问缓存时, 优先用的是YYMemoryCache。
  • YYMemoryCache 就是操作双向链表和哈希字典,使用 LRU 算法操作数据


-------------The End-------------

本文标题:YYCache源码阅读:YYMemoryCache

文章作者:kysonyangs

发布时间:2018年12月16日 - 19:12

最后更新:2020年05月17日 - 18:05

原始链接:https://kysonyangs.github.io/default/YYCache源码阅读1/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。