YYCache是一个线程安全的高性能键值缓存组件,代码质量很高,他的作者是国内开发者ibireme开发并且开源的. 下面就来简单剖析一下他的源码.如有哪里错误希望拍砖!多多交流多多进步!谢谢!!!
基本使用方法
//要缓存的对象
NSString*name =@"JJCoderMa";
//需要缓存的对象在缓存里对应的键
NSString *key =@"username";
//创建一个YYCache实例:userInfoCache
YYCache *userInfoCache = [YYCache cacheWithName:@"userInfocache"];
//存入键值对
[userInfoCache setObject:userName forKey:key withBlock:^{
NSLog(@"caching object succeed");
}];
//判断缓存是否存在
[userInfoCache containsObjectForKey:key withBlock:^(NSString* _Nonnull key,BOOLcontains) {
if(contains){
NSLog(@"存在!");
}
}];
//根据key读取数据
[userInfoCache objectForKey:key withBlock:^(NSString* _Nonnull key,id _Nonnull object) {
NSLog(@"user_name : %@",object);
}];
//根据key移除缓存
[userInfoCache removeObjectForKey:key withBlock:^(NSString* _Nonnull key) {
NSLog(@"remove user name %@",key);
}];
//移除所有缓存
[userInfoCache removeAllObjectsWithBlock:^{
NSLog(@"removing all cache succeed");
}];
//移除所有缓存带进度
[userInfoCache removeAllObjectsWithProgressBlock:^(intremovedCount,inttotalCount) {
NSLog(@"remove all cache objects: removedCount :%d totalCount : %d",removedCount,totalCount);
} endBlock:^(BOOLerror) {
if(!error){
NSLog(@"remove all cache objects: succeed");
}else{
NSLog(@"remove all cache objects: failed");
}
}];
```swift
YYCache 架构图: (第一次尝试使用MindNode,图画的不好~~~~)
- YYCache:提供了最外层的接口,调用了YYMemoryCache与YYDiskCache的相关方法。
- YYMemoryCache:负责处理容量小,相对高速的内存缓存。线程安全,支持自动和手动清理缓存等功能。
- _YYLinkedMap:YYMemoryCache使用的双向链表类。
- _YYLinkedMapNode:是_YYLinkedMap使用的节点类。
- YYDiskCache:负责处理容量大,相对低速的磁盘缓存。线程安全,支持异步操作,自动和手动清理缓存等功能。
- YYKVStorage:YYDiskCache的底层实现类,用于管理磁盘缓存。
- YYKVStorageItem:内置在YYKVStorage中,是YYKVStorage内部用于封装某个缓存的类
YYCache给用户提供所有最外层的缓存操作接口,而这些接口的内部内部实际上是调用了YYMemoryCache和YYDiskCache对象的相关方法。
因为YYMemoryCache和YYDiskCache的实例作为YYCache的两个公开的属性,所以用户无法直接使用YYMemoryCache和YYDiskCache对象,只能通过属性的方式来间接使用它们。
YYCache的部分属性和接口
YYCache的接口实现
从上面的接口实现可以看出:在YYCache中,永远都是先访问内存缓存,然后再访问磁盘缓存(包括了写入,读取,查询,删除缓存的操作)。而且关于内存缓存(_memoryCache)的操作,是不存在block回调的
YYMemoryCache
YYMemoryCache 操作类似于NSCache,它将需要缓存的对象与传入的key关联起来。
YYMemoryCache的内部有:
缓存淘汰算法:使用LRU(least-recently-used) 算法来淘汰(清理)使用频率较低的缓存。
缓存清理策略:使用三个维度来标记,分别是count(缓存数量),cost(开销),age(距上一次的访问时间)。YYMemoryCache提供了分别针对这三个维度的清理缓存的接口。用户可以根据不同的需求(策略)来清理在某一维度超标的缓存。
缓存淘汰算法的目的在于区分出使用频率高和使用频率低的缓存,当缓存数量达到一定限制的时候会优先清理那些使用频率低的缓存。因为使用频率已经比较低的缓存在将来的使用频率也很有可能会低。
YYMemoryCache用一个链表节点类来保存某个单独的内存缓存的信息(键,值,缓存时间等),然后用一个双向链表类来保存和管理这些节点。这两个类的名称分别是:
_YYLinkedMapNode:链表内的节点类,可以看做是对某个单独内存缓存的封装。
_YYLinkedMap:双向链表类,用于保存和管理所有内存缓存(节点)
_YYLinkedMapNode可以被看做是对某个缓存的封装:它包含了该节点上一个和下一个节点的指针,以及缓存的key
和对应的值(对象),还有该缓存的开销和访问时间。
/**
A node in linked map.
Typically, you should not use this class directly.
链表内的节点类,可以看做是对某个单独内存缓存的封装
*/
@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;
}
@end
_YYLinkedMap:
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // 用于存放节点
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
CFMutableDictionaryRef,用于保存节点的键值对,它还持有了链表内节点的总开销,总数量,头尾节点等数据。
[图片上传失败...(image-875de1-1522245469574)]
_YYLinkedMap实现细节:
插入结点(复习双向链表的时候~~~~)
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
//设置该node的值
CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
//增加开销和总缓存数量
_totalCost += node->_cost;
_totalCount++;
if (_head) {
//如果链表内已经存在头节点,则将这个头节点赋给当前节点的尾指针(原第一个节点变成了现第二个节点)
node->_next = _head;
//将该节点赋给现第二个节点的头指针(此时_head指向的节点是先第二个节点)
_head->_prev = node;
//将该节点赋给链表的头结点指针(该节点变成了现第一个节点)
_head = node;
} else {
//如果链表内没有头结点,说明是空链表。说明是第一次插入,则将链表的头尾节点都设置为当前节点
_head = _tail = node;
}
}
在双向链表中:
- 每个节点都有两个分别指向前后节点的指针。所以说每个节点都知道它前一个节点和后一个节点是谁。
- 链表的头部节点指向它前面节点的指针为空;链表尾部节点指向它后侧节点的指针也为空
将某个节点移动到表头
- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
//如果该节点已经是链表头部节点,则立即返回,不做任何操作
if (_head == node) return;
if (_tail == node) {
//如果该节点是链表的尾部节点
//1. 将该节点的头指针指向的节点变成链表的尾节点(将倒数第二个节点变成倒数第一个节点,即尾部节点)
_tail = node->_prev;
//2. 将新的尾部节点的尾部指针置空
_tail->_next = nil;
} else {
//如果该节点是链表头部和尾部以外的节点(中间节点)
//1. 将该node的头指针指向的节点赋给其尾指针指向的节点的头指针
node->_next->_prev = node->_prev;
//2. 将该node的尾指针指向的节点赋给其头指针指向的节点的尾指针
node->_prev->_next = node->_next;
}
//将原头节点赋给该节点的尾指针(原第一个节点变成了现第二个节点)
node->_next = _head;
//将当前节点的头节点置空
node->_prev = nil;
//将现第二个节点的头结点指向当前节点(此时_head指向的节点是现第二个节点)
_head->_prev = node;
//将该节点设置为链表的头节点
_head = node;
}
移除链表中的某个节点:
- (void)removeNode:(_YYLinkedMapNode *)node {
//除去该node的键对应的值
CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));
//减去开销和总缓存数量
_totalCost -= node->_cost;
_totalCount--;
//节点操作
//1. 将该node的头指针指向的节点赋给其尾指针指向的节点的头指针
if (node->_next) node->_next->_prev = node->_prev;
//2. 将该node的尾指针指向的节点赋给其头指针指向的节点的尾指针
if (node->_prev) node->_prev->_next = node->_next;
//3. 如果该node就是链表的头结点,则将该node的尾部指针指向的节点赋给链表的头节点(第二变成了第一)
if (_head == node) _head = node->_next;
//4. 如果该node就是链表的尾节点,则将该node的头部指针指向的节点赋给链表的尾节点(倒数第二变成了倒数第一)
if (_tail == node) _tail = node->_prev;
}
查找/缓存/更新某个对象
//是否包含某个缓存对象
- (BOOL)containsObjectForKey:(id)key {
//尝试从内置的字典中获得缓存对象
if (!key) return NO;
pthread_mutex_lock(&_lock);
BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key));
pthread_mutex_unlock(&_lock);
return contains;
}
//获取某个缓存对象
- (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;
}
//写入某个缓存对象,开销默认为0
- (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) {
//如果存在与传入的key值匹配的node,则更新该node的value,cost,time,并将这个node移到链表头部
//更新总cost
_lru->_totalCost -= node->_cost;
_lru->_totalCost += cost;
//更新node
node->_cost = cost;
node->_time = now;
node->_value = object;
//将node移动至链表头部
[_lru bringNodeToHead:node];
} else {
//如果不存在与传入的key值匹配的node,则新建一个node,将key,value,cost,time赋给它,并将这个node插入到链表头部
//新建node,并赋值
node = [_YYLinkedMapNode new];
node->_cost = cost;
node->_time = now;
node->_key = key;
node->_value = object;
//将node插入至链表头部
[_lru insertNodeAtHead:node];
}
//如果cost超过了限制,则进行删除缓存操作(从链表尾部开始删除,直到符合限制要求)
if (_lru->_totalCost > _costLimit) {
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}
//如果total count超过了限制,则进行删除缓存操作(从链表尾部开始删除,删除一次即可)
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) {
//内部调用了链表的removeNode:方法
[_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);
}
//内部调用了链表的removeAll方法
- (void)removeAllObjects {
pthread_mutex_lock(&_lock);
[_lru removeAll];
pthread_mutex_unlock(&_lock);
}
YYDiskCache
YYDiskCache处理容量大,相对低速的磁盘缓存。线程安全,支持异步操作。作为YYCache的第二级缓存,
与第一级缓存YYMemoryCache的相同点是:
- 都具有查询,写入,读取,删除缓存的接口。
- 不直接操作缓存,也是间接地通过另一个类(YYKVStorage)来操作缓存。
- 它使用LRU算法来清理缓存。
- 支持按 cost,count 和 age 这三个维度来清理不符合标准的缓存。
它与YYMemoryCache不同点是:
- 根据缓存数据的大小来采取不同的形式的缓存:
- 数据库sqlite: 针对小容量缓存,缓存的data和元数据都保存在数据库里。
- 除了 cost,count 和 age 三个维度之外,还添加了一个磁盘容量的维度。
// 缓存方式
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
YYKVStorageTypeFile = 0,
YYKVStorageTypeSQLite = 1,
YYKVStorageTypeMixed = 2,
};
缓存的大致逻辑
- 首先判断传入的key和value是否符合要求,如果不符合要求,则立即返回NO,缓存失败。
- 再判断是否type==YYKVStorageTypeFile并且文件名为空字符串(或nil):如果是,则立即返回NO,缓存失败。
判断filename是否为空字符串:
- 如果不为空:
写入文件,并将缓存的key,等信息写入数据库,但是不将key对应的data写入数据库。 - 如果为空:
如果缓存类型为YYKVStorageTypeSQLite:将缓存文件删除
如果缓存类型不为YYKVStorageTypeSQLite:则将缓存的key和对应的data等其他信息存入数据库。
YYKVStorage
YYKVStorage实例负责保存和管理所有磁盘缓存。和YYMemoryCache里面的_YYLinkedMap将缓存封装成节点类_YYLinkedMapNode类似,YYKVStorage也将某个单独的磁盘缓存封装成了一个类,这个类就是YYKVStorageItem,它保存了某个缓存所对应的一些信息(key, value, 文件名,大小等等):
YYKVStorageItem结构包含了键/值/文件名/值大小/时间戳/扩展数据等等字段
/**
YYKVStorageItem is used by `YYKVStorage` to store key-value pair and meta data.
Typically, you should not use this class directly.
YYKVStorage也将某个单独的磁盘缓存封装成了一个类,这个类就是YYKVStorageItem,它保存了某个缓存所对应的一些信息(key, value, 文件名,大小等等)
*/
@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; ///< modification unix timestamp
@property (nonatomic) int accessTime; ///< last access unix timestamp
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end
还有一些方法来操作数据
//YYKVStorage.h
- (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;
总结一下:
1. 为什么使用双向链表?
- 双向链表可以知道前后节点,所以如果想移动其中一个节点的话,其前后的节点不好做衔接。
- 其节点的关联仅仅是靠指针,所以对于插入和删除操作会很便利,而类似数组的方式寻址操作缺比较费时。由于在LRU策略中会有非常多的移动,插入和删除节点的操作,使用双向链表是比较有优势的。
2. 使用CFDictionary而没有用NSDictionary来实现?
CFDictionary 更加底层,更快.
3. 为什么内存缓存使用互斥锁?而磁盘缓存使用信号量?
作者在最初使用的是自旋锁(OSSpinLock)作为内存缓存的线程锁,但是后来得知其不够安全,所以退而求其次,使用了pthread_mutex 这篇文章有说明
网上说:但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。