一.类结构图
简单引用一些作者(ibireme)[https://github.com/ibireme/]在YYCache 设计思路的介绍。
1)YYMemoryCache: 内存缓存,没有异步访问的接口,尽量优化了同步访问的性能,用OSSpinLock来保证线程安全。缓存内部使用双向链表和NSDictionary实现了LRU淘汰算法。
2)YYDiskCache: 磁盘缓存,采用SQLite配合文件的存储方式。据作者在iPhone6 64G上测试,在存取小数据的时候,它的性能远远高于基于文件存储的库;而较大数据的存取性能则比较接近。YYDiskCache也实现了LRU淘汰算法。

二.学前准备
2.1 LRU淘汰算法
2.2 OSSpinLock及一些锁
iOS/MacOS自有的自旋锁。当一个线程获得锁之后,其他线程会一直循环,查看该锁是否被释放。所以,该锁适用于锁的持有者保存时间较短的情况下。
顺带提及一下dispatch_semaphore(信号量),GCD用它来控制多线程并发。参考
还有,pthread_mutex是一种互斥锁,当锁被占用时,别的想使用该锁的线程都会被阻塞。
2.3 YYLinkedMapNode && YYLinkedMap
YYCache实现了LRU淘汰算法,我们看一下代码中是如何实现的。(首先你要确保你知道什么是LRU淘汰算法)
2.3.1 YYLinkedMapNode
YYLinkedMapNode的实例作为双向链表的节点,提供了指向前后节点的指针_prev和_next、key-value、cost、time
2.3.2 YYLinkedMap
YYLinkedMap不是线程安全的,也不会检测数据的有效性。它是用于服务YYMemoryCache的,作者不希望别人直接使用它。
作为链表,它提供了一些方法。提升节点至表头的方法主要是为了提高查找的命中率。
//插入节点
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
//将某一存在的节点提升至表头
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
//删除节点
- (void)removeNode:(_YYLinkedMapNode *)node;
//如果链表存在,删除尾节点
- (_YYLinkedMapNode *)removeTailNode;
//在后台线程删除所有节点
- (void)removeAll;
三.代码阅读
github上下载Demo工程CacheBenchmark,后文都以此为基础学习。demo中存在不同缓存类的对比,本文主要学习YYCache,也只关心这部分的代码。
3.1 YYMemoryCache添加数据
Demo对内存缓存的测试主要集中于 + (void)memoryCacheBenchmark;
方法中。首先对比了 setObject:forKey: 存取基本数据类型的性能。我们从YYMemoryCache的这个方法开始学习它内部的处理。
- (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);
}
代码分析:
1)pthread_mutex_lock: 这里用了互斥锁,确保了线程安全
2)根据传入的key值去链表_lru中取node
3)如果取到node,更新node,并将其提升至表头;如果没有取到node,生成一个新的node,并插入链表
4)如果_totalCost超过了链表的最大内存开销长度,'修剪'链表。即调用 - (void)_trimToCost:(NSUInteger)costLimit;
方法(不贴代码了)。它会做2件事:
- CACurrentMediaTime() 可以引申出iOS中关于时间的处理,建议仔细阅读MrPeak的iOS关于时间的处理
① 把链表末尾的节点删除,直到内存开销长度够用
② 根据_lru是否在要在主线程销毁数据(可配置,_releaseOnMainThread变量),创建线程,并销毁节点。
totalCost默认是没有上限的。如果,手动设置为10MB,那么当链表的数据长度超过10MB时,就会触发清理操作。
5)同理,如果存储的节点数量超过了上限,也会进行清理的。
最后,我们顺便看一下数据获取方法,应该是一目了然的。
- (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;
}
3.2 YYMemoryCache其他方法
我们看一下init方法:
- (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;
}
添加了2个通知:
1)_appDidReceiveMemoryWarningNotification: 内存警告通知
2)_appDidEnterBackgroundNotification: 程序进入后台通知
2个通知内部都提供了block调用,我们可以根据需求实现自定义操作,还可以清空所有数据。
总结
从demo的时间来看,YYMemoryCache在性能上还是比较接近NSDictionary的。当然,在这种情况下,肯定首选还是NSDictionary。接下去,我们将看一下磁盘缓存的代码
网友评论