文章

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
11
12
```
/// 插入头节点
- (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 算法操作数据
本文由作者按照 CC BY 4.0 进行授权