用心阅读 YYCache 并改装
架构
YYKIT 分为内存缓存和 磁盘缓存,磁盘缓存又分为文件缓存和数据库缓存,作者认为,大于20K应该使用文件缓存,否则sqlite缓存
YYMemoryCache
YYMemoryCache 是内存缓存,速度最快,使用的是 LRU 思想和双向链表数据结构来实现的
LRU 淘汰算法是淘汰最近最少使用的数据,那么利用双向链表完全可以实现,当有新数据插入的时候,就把新数据插入到链表的头部,当使用了内存中已经存在的数据的时候,就把这条数据移动到链表的头部,然后把这个节点的前驱节点和后继节点接到一起。可以看代码,
_YYLinkedMapNode
源码里面有这样的一个类 _YYLinkedMapNode ,他其实就是链表的节点,每一个节点都有自己的前驱节点和后继节点,还有自己的数据,YYMemoryCache 除了这个之外还存放了每一个节点占用的开销大小,不过其实 YYMemoryCache 没有用到这个开销大小,像 SDWebImage 里面就用到了这个开销大小,存放图片的时候以图片的像素大小堆起来的开销大小,不过算法思想代码已经写好了,至于这个开销大小怎么来定义,开发者可以自己定义,还有一个 time,也就是时间, YYMemoryCache 也可以根据时间,淘汰旧的数据。
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
_YYLinkedMap
这个类其实就是双向链表,
{
@package
CFMutableDictionaryRef _dic; // do not set object directly
NSUInteger _totalCost;
NSUInteger _totalCount;
_YYLinkedMapNode *_head; // MRU, do not change it directly
_YYLinkedMapNode *_tail; // LRU, do not change it directly
BOOL _releaseOnMainThread;
BOOL _releaseAsynchronously;
}
dic 指的就是链表,链表中存放每一个 节点,就是 _YYLinkedMapNode , _totalCost 指的是总的开销大小,_totalCount 指的是总的缓存数量,_head 是链表的头结点,_tail 是链表的尾节点,_releaseOnMainThread 是否在主线程中释放,_releaseAsynchronously 在子线程中释放,这个之后会讲到,这个地方的处理,我个人觉得真的是太牛逼了,想了好一会才想明白,可想 YY 大神真的是牛逼,思想也牛逼。
内部的几个接口,这个需要有点链表基础,懂点就能看懂代码
/// Insert a node at head and update the total cost.
/// Node and node.key should not be nil.
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
/// Bring a inner node to header.
/// Node should already inside the dic.
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
/// Remove a inner node and update the total cost.
/// Node should already inside the dic.
- (void)removeNode:(_YYLinkedMapNode *)node;
/// Remove tail node if exist.
- (_YYLinkedMapNode *)removeTailNode;
/// Remove all node in background queue.
- (void)removeAll;
节点的插入删除没什么说的,自己看代码,如果看不懂,去看看双向链表的节点插入删除再回来看这个。
YYMemoryCache
YYMemoryCache是线程安全的,作者通过互斥锁pthread_mutex_t来控制的线程安全
内部有一个递归算法,来每隔一段时间来清理缓存,主要分为以开销大小清理和数量大小清理
- (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)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);
}
当吧数据放到缓存中的时候,首先会检查缓存中是否已经存在这个节点,如果存在的话,那么就修改数据并且把这个节点移动到链表的头部,这个节点就视为常用数据,如果不存在就创建一个节点,并赋值,插入到链表的头部,如果在插入的过程中发现超过了开销的大小或者数量的大小,就用LRU算法来清理数据,这里指的一提的是节点的释放
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
});
}
}
我们都知道,局部变量在过了作用域之后会被释放,有系统释放,首先将释放的节点赋值给了局部变量 _YYLinkedMapNode *node = [_lru removeTailNode];,node 就为持有者,如果我们不做操作,那么系统就会在当前节点来释放节点,也就是在当前主线程去释放节点,坐着这里实在另一个block里面取操作了这个拥有者,这个block就拥有了这个局部变量,那么过了作用域之后,这个节点也不会释放,引用计数减一但不为0,在block里面作为任务之后,再减一,这时候就被释放了,就做到了在子线程异步释放的目的,这是我个人理解,个人感觉实在是高,高,实在是高,对于我这种菜鸟来说实在是膜拜。
YYDiskCache
先看初始化方法
- (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;
}
如果阀值设置为0,那么所有的磁盘缓存都以文件的形式存储,如果设置为最大,那么就以sqlite的形式存储,否则介于两者之间,自动决定,也就是作者认为 20k 应该以文件的形式,否则数据库。里面的接口,作者都给出了同步方法和异步方法,开发者可以根据自己的需求选择使用,用到的线程队列也是 YY 大神自己设计的YYDispatchQueuePool,我的二次封装改装并开发了几个接口DispatchQueuePool.
YYKVStorage
是磁盘缓存,里面创建了 manifest 的表,表里面有
key text, filename text, size integer, inline_data blob, modification_time integer, last_access_time integer, extended_data blob
一下几个字段,分别为 key ,文件名,大小,文件的二进制数据,修改时间,访问时间和附加二进制数据,sqlite 是通过 sqlite3_wal_checkpoint 的形式,启用 wal 模式之后,改写操作是附加到 wal 文件中的,而不是直接改动数据库文件,所以数据库文件可以同时被读取,执行sqlite3_wal_checkpoint的时候,wal 文件被写到数据库文件当中,磁盘缓存,如果选择的缓存策略不是纯sqlite缓存那么就一定会涉及到文件缓存,那么在入口的时候,就会判断一下,然后传入需要缓存的文件名字,如果传入了文件名字,那么数据会被写入到文件,并且数据库中不会存数据的二进制数据,只会存文件名字,如果没有文件名字,那么会把数据的整个二进制数据存储到数据库当中,如果存储方式是数据库的形式,首先从数据库搜索该数据的key是否对应有文件名,如果有的话就去磁盘把文件名对应的文件删除,因为只存数据库,否则的话存储到数据库当中,代码
- (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];
}
}
存储的对象这样生成二进制数据?
作者是通过街归档来形成的,
@try {
value = [NSKeyedArchiver archivedDataWithRootObject:object];
}
@catch (NSException *exception) {
// nothing to do...
}
当开发者可以自己定义街归档,作者给出了 block
value = _customArchiveBlock(object);
例如
[cache.diskCache setCustomArchiveBlock:^NSData * _Nonnull(id _Nonnull object) {
NSString *name = [NSString stringWithFormat:@"luban"];
return [name dataUsingEncoding:NSUTF8StringEncoding];
}];
[cache.diskCache setCustomUnArchiveBlock:^id _Nonnull(NSData * _Nonnull data) {
RCUserInfo *userInfo = [RCUserInfo new];
userInfo.name = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
userInfo.address = @"召唤师峡谷";
userInfo.age = 18;
return userInfo;
}];
删除数据
- (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;
}
}
如果是数据库存储那么直接删除数据库数据就好,否则先查一下文件名字,然后删除磁盘文件,最后删除数据库的数据。
还有几个方法就不一一解释了,比如删除超过指定大小的文件,删除过旧的文件,都是先从数据库搜索符合条件的数据然后删除,看代码都可以明白。
我得小改装 RCCache
因为YYCache 已经很完美了,个人觉得,所以我只是改了他的内存缓存的一点接口,也不能说是改,就是封装了一下,更加面向OC,比如之前的插入节点,
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
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;
}
}
我得
/**
头插法
@param node 插入的节点
*/
-(void)insertNodeToHead:(RCCacheLinkNode *)node{
CFDictionarySetValue(_linkDic, (__bridge const void *)(node.key), (const void *)node);
[self changeTheCountAndTheCost:node match:@"+"];
if (_head) {
node.nextEqualTo(_head);
_head.prevEqualTo(node);
_head = node;
} else {
_head = _tail = node;
}
}
/**
将节点切换到头部
@param node 需要变换的节点
*/
-(void)bringNodeToHead:(RCCacheLinkNode *)node{
if (_head == node) {
return;
}
if (_tail == node) {
_tail = node.prev;
_tail.nextEqualTo(nil);
} else {
node.next.prevEqualTo(node.prev);
node.prev.nextEqualTo(node.next);
}
node.nextEqualTo(_head);
node.prevEqualTo(nil);
_head.prevEqualTo(node);
_head = node;
}
但核心还是 YY 大神的 没有变过
总结
YY 大神太牛了,有时间大家一定要看看他的开源代码