前言
因项目需要加入了大量的数据缓存功能,在优化项目本地缓存组件的之前。研究阅读了一下YYCache这个国内最优秀的一个线程安全的高性能缓存组件。
阅读之前需要了解的一些基本知识
在YYCache源码中用到的很多内容包括双向链表,线程锁,数据库操作相关等相关的基本了解在开头先说一下,有助于源码的阅读。
双向链表
- 双向链表它的每个数据结点中都有两个指针,分别指向直接后继(n)和直接前驱(p)。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点
- 链表的头部节点指向它前面节点的指针为空;链表尾部节点指向它后侧节点的指针也为空。
比如想要删除某一个节点data1
1.将data1的next的prev指向data1的prev
2.将data1的prev的next指向data1的next
3.释放p
线程锁
在YYCache中共使用到了两种线程锁
- 互斥锁 pthread_mutex(用于内存缓存)
- 信号量 dispatch_semaphore(用于磁盘缓存)
关于如何选择合适的锁YYCache的作者是这么说的
为什么内存缓存使用互斥锁(pthread_mutex)?
作者在最开始内存缓存中使用的OSSpinLock(自旋锁),在某个版本更新以后不再安全以后,改用的pthread_mutex
为什么磁盘缓存使用的是信号量(dispatch_semaphore)?
dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。
简单的代码操作
内存缓存中
pthread_mutex_lock(&_lock);
、、、、
pthread_mutex_unlock(&_lock);
磁盘缓存中使用了宏替代
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER)
#define Unlock() dispatch_semaphore_signal(self->_lock)
架构图及相应职责
架构图
相应职责
- YYcache最外部调用方法
- YYMemoryCache内存缓存:主要缓存容量小,相对高速的缓存
- _YYLinkedMap: 一个双线链表类,负责存放所有缓存及提供相关操作
- _YYLinkedMapNode: 是_YYLinkedMap的节点类也可以看作是一个缓存的类
- YYDiskCache磁盘缓存:负责容量大,相对低俗的缓存
- YYKVSorage:YYDiskCache操作缓存的类
- YYKVStorageItem:相当于是YYDiskCache的一个缓存的类
YYCache
作者提供了四种创建方式
而不论哪儿一种创建方式都最终会调用initWithPath,传入的参数要么给一个字符串Name要么给一个路径path
- (nullable instancetype)initWithName:(NSString *)name;
- (nullable instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
+ (nullable instancetype)cacheWithName:(NSString *)name;
+ (nullable instancetype)cacheWithPath:(NSString *)path;
在这个方法中内存缓存和磁盘缓存都会分别创建出一个
- (instancetype)initWithPath:(NSString *)path {
if (path.length == 0) return nil;
YYDiskCache *diskCache = [[YYDiskCache alloc] initWithPath:path];
if (!diskCache) return nil;
NSString *name = [path lastPathComponent];
YYMemoryCache *memoryCache = [YYMemoryCache new];
memoryCache.name = name;
self = [super init];
_name = name;
_diskCache = diskCache;
_memoryCache = memoryCache;
return self;
}
并且禁用掉了init方法和new方法调用会提示报错
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
YYCache的操作方法
//查询是否已经拥有某个缓存(可能会阻塞线程直到返回结果为止)
- (BOOL)containsObjectForKey:(NSString *)key;
//读取缓存(可能会阻塞线程直到文件读取完成)
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
//写入缓存(可能会阻塞线程直到编写完成)
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
//删除某个缓存(可能会阻塞线程直到文件删除)
- (void)removeObjectForKey:(NSString *)key;
//删除所有缓存
- (void)removeAllObjects;
方法的具体实现
- (BOOL)containsObjectForKey:(NSString *)key {
//先检查是否在内存缓存中,再检查是否在磁盘缓存中
return [_memoryCache containsObjectForKey:key] || [_diskCache containsObjectForKey:key];
}
- (id<NSCoding>)objectForKey:(NSString *)key {
//先从内存缓存中取
id<NSCoding> object = [_memoryCache objectForKey:key];
if (!object) {
//如果没有在磁盘缓存中取
object = [_diskCache objectForKey:key];
if (object) {
//如果磁盘缓存中有就将其写入内存缓存中
[_memoryCache setObject:object forKey:key];
}
}
return object;
}
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
//先写入内存缓存中
[_memoryCache setObject:object forKey:key];
//再写入磁盘缓存中
[_diskCache setObject:object forKey:key];
}
- (void)removeObjectForKey:(NSString *)key {
//先移除内存缓存
[_memoryCache removeObjectForKey:key];
//再移除磁盘缓存
[_diskCache removeObjectForKey:key];
}
- (void)removeAllObjects {
[_memoryCache removeAllObjects];
[_diskCache removeAllObjects];
}
YYMemoryCache
也是先来看下属性与方法
@interface YYMemoryCache : NSObject
#pragma mark - Attribute
//缓存名称
@property (nullable, copy) NSString *name;
//缓存总数量
@property (readonly) NSUInteger totalCount;
//缓存总开销
@property (readonly) NSUInteger totalCost;
#pragma mark - Limit
//缓存中数量的最大值,默认NSUIntegerMax无限制
@property NSUInteger countLimit;
//缓存中开销的最大值,默认NSUIntegerMax无限制
@property NSUInteger costLimit;
//时间限制,默认DBL_MAX无限制(无限制而不是无上限)
@property NSTimeInterval ageLimit;
//当有超出上限的缓存设置清理的间隔时间,默认为五秒
@property NSTimeInterval autoTrimInterval;
//收到内存警告的时候是否清空所有缓存,默认为YES
@property BOOL shouldRemoveAllObjectsOnMemoryWarning;
//App进入后台的时候是否晴空所有缓存,默认为YES
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;
//收到内存警告以后的回掉block
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);
//进入后台以后的回掉block
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);
//是否在主线程清理,默认为NO
@property BOOL releaseOnMainThread;
//清理缓存是否异步执行,默认为YES
@property BOOL releaseAsynchronously;
#pragma mark - Access Methods
// 查询是否存在于缓存中
- (BOOL)containsObjectForKey:(id)key;
// 获取缓存对象
- (nullable id)objectForKey:(id)key;
// 写入缓存对象
- (void)setObject:(nullable id)object forKey:(id)key;
// 写入缓存对象并加入开销
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
// 移除缓存对象
- (void)removeObjectForKey:(id)key;
// 立即清空缓存
- (void)removeAllObjects;
#pragma mark - Trim
// 清理缓存直到一个指定数量之下
- (void)trimToCount:(NSUInteger)count;
// 清理缓存直到一个指定开销之下
- (void)trimToCost:(NSUInteger)cost;
// 清理所有缓存时间小于指定时间的缓存
- (void)trimToAge:(NSTimeInterval)age;
@end
YYMemoryCache内部使用的是LRU缓存算法
- LRU (Least recently used) 最近最少使用,如果数据最近被访问过,那么将来被访问的几率也更高。
- 还有三个个清理标准是count(缓存数量),cost(开销),age(距上一次的访问时间)
这两个是相互结合使用的也就是说比如当我需要清理缓存个数(count)直到一个我给定的数值的情况下,我是先从使用频率最低的那个缓存开始清理, 让我们先了一下_YYLinkedMap和_YYLinkedMapNode然后来根据源码来看下YYCache中LRU算法的具体体现
_YYLinkedMap和_YYLinkedMapNode
_YYLinkedMapNode的结构,就相当于一个链表中的一个节点,也可以说是就是一个缓存
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // 上一个节点
__unsafe_unretained _YYLinkedMapNode *_next; // 下一个节点
id _key; // 缓存的key
id _value; // 缓存的值
NSUInteger _cost; // 缓存开销
NSTimeInterval _time; // 缓存的访问时间
}
@end
@implementation _YYLinkedMapNode
@end
_YYLinkedMap是一个双向链表其中存放了所有的缓存(一个节点也可以说是一个缓存)
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; //缓存对象存放,不直接设置对象
NSUInteger _totalCost; //总开销数
NSUInteger _totalCount; //总缓存数(可以说是节点总数)
_YYLinkedMapNode *_head; //链表的头部节点
_YYLinkedMapNode *_tail; //链表的尾部节点
BOOL _releaseOnMainThread; //是否在主线程释放
BOOL _releaseAsynchronously; //是否在子线程释放
}
//向链表的头插入节点,节点及节点的key不应为空
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
//将链表内部的某个节点移动到链表的头部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
//移除某个节点
- (void)removeNode:(_YYLinkedMapNode *)node;
//移除尾部节点,并返回该尾部节点
- (_YYLinkedMapNode *)removeTailNode;
//移除所有节点
- (void)removeAll;
@end
也就是说_YYLinkedMapNode相当于在双向链表图一中的data,_YYLinkedMap就相当于是一个整体的双向链表,存储方式没有选用NSDictionary,而是选用的CFDictionary,因为CFDictionary更底层且快速,然后也简单的看下CFDictionary的结构:
CFDictionaryCreate ( CFAllocatorRef allocator, //为新字典分配内存。通过NULL或kCFAllocatorDefault使用当前默认的分配器。
const void **keys, //key的数组。如果numValues参数为0,则这个值可能是NULL。这个函数没有改变或释放这个数组。该值必须是有效的C数组。
const void **values, //value的数组(同上)。
CFIndex numValues, //键值对数目。>=0 && >=实际数目。
const CFDictionaryKeyCallBacks*keyCallBacks, //键的回调。
const CFDictionaryValueCallBacks*valueCallBacks );//值的回调。
_YYLinkedMap方法实现(需要前文提到的双向链表知识)
//向链表的头插入节点,节点及节点的key不应为空
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
//node放入字典里
CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
//增加开销及缓存数量
_totalCost += node->_cost;
_totalCount++;
if (_head) {
//如果有头节点则将头节点付给node的next(就是给头节点仍该节点后面去了)
node->_next = _head;
//将头节点的prev也指向一下node
_head->_prev = node;
//改变一下头节点的指向
_head = node;
} else {
//如果没有头节点就说明是空的是第一个缓存的数据直接头尾节点都是node
_head = _tail = node;
}
}
//将链表内部的某个节点移动到链表的头部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
//如果已经是头节点了返回
if (_head == node) return;
if (_tail == node) {
//如果是尾部节点,将node上一个节点给尾部节点(也就是说降倒数第二个变成最后一个)
_tail = node->_prev;
//再将新的尾部节点的next置空
_tail->_next = nil;
} else {
//如果是中间节点 这两行代码就的作用就是将该节点的前后两个节点链接在一起
node->_next->_prev = node->_prev;
node->_prev->_next = node->_next;
}
//然后在第一个节点放在node的后面变成第二个
node->_next = _head;
//把node的prev置空
node->_prev = nil;
//把变成第二个的节点的prev指向node
_head->_prev = node;
//变更头节点是谁
_head = node;
}
//移除某个节点
- (void)removeNode:(_YYLinkedMapNode *)node {
//先从字典里移除
CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));
//减少开销和个数
_totalCost -= node->_cost;
_totalCount--;
//如果node的next不为空把他下一个节点的prev指向上一个节点
if (node->_next) node->_next->_prev = node->_prev;
//如果node的prev不为空将它上一个节点的next指向他的下一个节点
if (node->_prev) node->_prev->_next = node->_next;
//如果是头节点直接把头节点变更成他的下一个节点
if (_head == node) _head = node->_next;
//如果是尾部节点直接把尾部节点变更成他的上一个节点
if (_tail == node) _tail = node->_prev;
}
//移除并返回尾部节点
- (_YYLinkedMapNode *)removeTailNode {
//如果不是尾部节点返回
if (!_tail) return nil;
//创建一个节点=尾部节点留return用并且在字典中移除并减少相应开销和个数
_YYLinkedMapNode *tail = _tail;
CFDictionaryRemoveValue(_dic, (__bridge const void *)(_tail->_key));
_totalCost -= _tail->_cost;
_totalCount--;
if (_head == _tail) {
//如果头节点等于尾部节点说明就一个节点直接全部置空变成空链表
_head = _tail = nil;
} else {
//尾部节点变成原尾部节点的上一个节点
_tail = _tail->_prev;
//新尾部节点置空
_tail->_next = nil;
}
return tail;
}
- (void)removeAll {
//所有东西置空为0移除字典的东西并且都给释放掉(根据参数选择在什么线程做这些操作)
_totalCost = 0;
_totalCount = 0;
_head = nil;
_tail = nil;
if (CFDictionaryGetCount(_dic) > 0) {
CFMutableDictionaryRef holder = _dic;
_dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
if (_releaseAsynchronously) {
dispatch_queue_t queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
CFRelease(holder); // hold and release in specified queue
});
} else if (_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
CFRelease(holder); // hold and release in specified queue
});
} else {
CFRelease(holder);
}
}
}
YYMemoryCache的内部底层原理实现了解以后我们就可以根据两个函数来看下LRU算法在其中的应用了
- (id)objectForKey:(id)key {
、、、
[_lru bringNodeToHead:node];
、、、
}
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost{
、、、
[_lru insertNodeAtHead:node];
[_lru bringNodeToHead:node];
、、、
}
也就是说无论是你访问还是存入缓存的时候该数据都会变成第一重要数据在我清理缓存的时候最后清理头部的,也可以说最近使用的话最后清除。
再来看下YYMemoryCache的方法实现来确定下是从尾部开始清除
- (void)_trimToCount:(NSUInteger)countLimit {
BOOL finish = NO;
pthread_mutex_lock(&_lock);
if (countLimit == 0) {
[_lru removeAll];
finish = YES;
} else if (_lru->_totalCount <= countLimit) {
finish = YES;
}
pthread_mutex_unlock(&_lock);
if (finish) return;
NSMutableArray *holder = [NSMutableArray new];
while (!finish) {
//尝试加锁也就是询问是有已经有锁
if (pthread_mutex_trylock(&_lock) == 0) {
if (_lru->_totalCount > countLimit) {
//如果没有的话移除尾部节点
_YYLinkedMapNode *node = [_lru removeTailNode];
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&_lock);
} else {
usleep(10 * 1000); //10 ms
}
}
if (holder.count) {
//这是释放
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
[holder count]; // release in queue
});
}
}
关于这段代码
else {
usleep(10 * 1000); //10 ms
}
我去看了下作者说的到底是干啥用的,作者是这样说的
为了尽量保证所有对外的访问方法都不至于阻塞,这个对象移除的方法应当尽量避免与其他访问线程产生冲突。当然这只能在很少一部分使用场景下才可能有些作用吧,而且作用可能也不明显。。。 :?:
说句实话我理解的也不是很到位,有理解的朋友可以评论说一下,而[holder count]和会出现的[holder class]的作用都是一样的保证node是在这个queue上release掉,而YYMemoryCache的方法实现的源码阅读起来相对来说还是比较容易的就不一一分析了。
YYDiskCache
在查询,写入,读取,删除缓存方面接口封装和YYMemoryCache几乎可以说是一样的,同时也支持LRU算法来清理缓存以及cost,count和age三个维度,主要说下不同点
- 会根据需要缓存的数据的大小来自动选择缓存方式而缓存方式分为
** 数据库 :小容量缓存,缓存的data和元数据都保存在数据库里
** 数据库 + 文件 :大容量缓存,缓存的data写在文件系统里,其元数据保存在数据库里。 - 除了cost,count和age三个维度还支持磁盘容量清除
而对于第一条来说有一个很重要的属性:
// The default value is 20480 (20KB).
@property (readonly) NSUInteger inlineThreshold;
inlineThreshold默认值为20KB,也就是说当缓存的数据大与20KB的时候自动会选择数据库+文件的缓存方式,而当小于的时候使用的是数据库缓存的方式。而只有在下面这个初始化方法中
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
...
_inlineThreshold = threshold;
...
}
是唯一一次关于threshold的赋值操作而在另外两种初始化方法中都已经给定了值
- (instancetype)init {
@throw [NSException exceptionWithName:@"YYDiskCache init error" reason:@"YYDiskCache must be initialized with a path. Use 'initWithPath:' or 'initWithPath:inlineThreshold:' instead." userInfo:nil];
return [self initWithPath:@"" inlineThreshold:0];
}
- (instancetype)initWithPath:(NSString *)path {
return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
}
然后看下写入方法
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
if (!key) return;
if (!object) {
//这一步的作用我的理解就是置空等同于删除逻辑当传入一个空的object的时候
[self removeObjectForKey:key];
return;
}
//获取缓存数据的信息
NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];
NSData *value = nil;
if (_customArchiveBlock) {
value = _customArchiveBlock(object);
} else {
@try {
value = [NSKeyedArchiver archivedDataWithRootObject:object];
}
@catch (NSException *exception) {
// nothing to do...
}
}
if (!value) return;
NSString *filename = nil;
//判断操作类型
if (_kv.type != YYKVStorageTypeSQLite) {
if (value.length > _inlineThreshold) {
filename = [self _filenameForKey:key];
}
}
Lock();
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}
接下来看看YYDiskCache的基础操作类
YYKVStorage
YYKVStorage中有一个类叫YYKVStorageItem两者的关系类似与_YYLinkedMapNode与_YYLinkedMap的关系,YYKVStorage也将某个单独的磁盘缓存封装成了一个类。
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key; //键
@property (nonatomic, strong) NSData *value; //值
@property (nullable, nonatomic, strong) NSString *filename; //文件名(nil if inline)
@property (nonatomic) int size; //值的大小,单位是byte
@property (nonatomic) int modTime; //修改时间戳
@property (nonatomic) int accessTime; //最后一次访问的时间戳
@property (nullable, nonatomic, strong) NSData *extendedData; //extended data
@end
然后在YYKVStorageItem中有一个存储类型的选择
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
/// 纯文件存储方式
YYKVStorageTypeFile = 0,
///数据库存储方式
YYKVStorageTypeSQLite = 1,
/// 存储在文件或数据库中
YYKVStorageTypeMixed = 2,
};
但是第一种YYKVStorageTypeFile纯文件的存储方式的具体实现并没有写- -
接下来看YYKVStorage的具体操作接口都有啥
/**
保存或者更新某个Item
*/
- (BOOL)saveItem:(YYKVStorageItem *)item;
/**
保存或更新某个键值对,值为NSData对象
*/
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value;
/**
保存或更新某个键值对,包括文件名以及data信息
*/
- (BOOL)saveItemWithKey:(NSString *)key
value:(NSData *)value
filename:(nullable NSString *)filename
extendedData:(nullable NSData *)extendedData;
#pragma mark - Remove Items
/**
移除某个键的item
*/
- (BOOL)removeItemForKey:(NSString *)key;
/**
移除多个键的item
*/
- (BOOL)removeItemForKeys:(NSArray<NSString *> *)keys;
/**
移除大于参数size的item
*/
- (BOOL)removeItemsLargerThanSize:(int)size;
/**
移除时间早于参数时间的item
*/
- (BOOL)removeItemsEarlierThanTime:(int)time;
/**
移除item,使得缓存总容量小于参数size
*/
- (BOOL)removeItemsToFitSize:(int)maxSize;
/**
移除item,使得缓存数量小于参数size
*/
- (BOOL)removeItemsToFitCount:(int)maxCount;
/**
移除所有的item
*/
- (BOOL)removeAllItems;
/**
移除所有的item,带有进度和结束回掉
*/
- (void)removeAllItemsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
endBlock:(nullable void(^)(BOOL error))end;
#pragma mark - Get Items
/**
获取key的item
*/
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;
/**
获取ket的item信息 在这个item中value值将被忽略
*/
- (nullable YYKVStorageItem *)getItemInfoForKey:(NSString *)key;
/**
获取参数key对应的item的value(data数据)
*/
- (nullable NSData *)getItemValueForKey:(NSString *)key;
/**
获取多个
*/
- (nullable NSArray<YYKVStorageItem *> *)getItemForKeys:(NSArray<NSString *> *)keys;
/**
获取多个
*/
- (nullable NSArray<YYKVStorageItem *> *)getItemInfoForKeys:(NSArray<NSString *> *)keys;
/**
获取参数数组对应的item字典
*/
- (nullable NSDictionary<NSString *, NSData *> *)getItemValueForKeys:(NSArray<NSString *> *)keys;
#pragma mark - Get Storage Status
/**
某个key值对应的item是否存在
*/
- (BOOL)itemExistsForKey:(NSString *)key;
/**
获取item数量
*/
- (int)getItemsCount;
/**
获取item占用的总容量
*/
- (int)getItemsSize;
接下来看下接口的实现
- (BOOL)saveItem:(YYKVStorageItem *)item {
return [self saveItemWithKey:item.key value:item.value filename:item.filename extendedData:item.extendedData];
}
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value {
return [self saveItemWithKey:key value:value filename:nil extendedData:nil];
}
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
//判断key和value值是否为空
if (key.length == 0 || value.length == 0) return NO;
//判断当type为YYKVStorageTypeFile时文件名是否为空
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 {
//如果为空判断是否为YYKVStorageTypeSQLite如果是的话不进行文件缓存
if (_type != YYKVStorageTypeSQLite) {
//如果缓存类型不是数据库缓存,则查找出相应的文件名并删除
NSString *filename = [self _dbGetFilenameWithKey:key];
if (filename) {
[self _fileDeleteWithName:filename];
}
}
//把key和value写入数据库
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}
看一下_dbSaveWithKey:value:fileName:extendedData:这个函数不管文件名是否为空的时候都调用了唯一的区别就是在fileName参数一个传入filename一个传入了nil我们看下该方法的内部实现
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
//sql语句
NSString *sql = @"insert or replace into manifest (key, filename, size, inline_data, modification_time, last_access_time, extended_data) values (?1, ?2, ?3, ?4, ?5, ?6, ?7);";
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
if (!stmt) return NO;
int timestamp = (int)time(NULL);
//key
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
//filename
sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);
//size
sqlite3_bind_int(stmt, 3, (int)value.length);
//inline_data
if (fileName.length == 0) {
//如果文件名长度==0,则将value存入数据库
sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
} else {
//如果文件名长度不为0,则不将value存入数据库
sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
}
//modification_time
sqlite3_bind_int(stmt, 5, timestamp);
//last_access_time
sqlite3_bind_int(stmt, 6, timestamp);
//extended_data
sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);
int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite insert error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
return YES;
}
也就是说在内部fileName为空的时候将数据存入数据库而当不为0的时候也代表着已经写入过了文件缓存所以仅仅将其他信息写入数据库而不将data写入数据库。下面是读取缓存接口
- (YYKVStorageItem *)getItemForKey:(NSString *)key {
if (key.length == 0) return nil;
//拿到item
YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
if (item) {
//更新内存访问的时间
[self _dbUpdateAccessTimeWithKey:key];
if (item.filename) {
//如果有文件名,则尝试获取文件数据赋值给value而没有filename时在数据库中就已经可以拿到value了
item.value = [self _fileReadWithName:item.filename];
//如果此时获取文件数据失败,则删除对应的item
if (!item.value) {
[self _dbDeleteItemWithKey:key];
item = nil;
}
}
}
return item;
}
_dbGetItemWithKey: excludeInlineData:
- (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData {
NSString *sql = excludeInlineData ? @"select key, filename, size, modification_time, last_access_time, extended_data from manifest where key = ?1;" : @"select key, filename, size, inline_data, modification_time, last_access_time, extended_data from manifest where key = ?1;";
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
if (!stmt) return nil;
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
YYKVStorageItem *item = nil;
int result = sqlite3_step(stmt);
if (result == SQLITE_ROW) {
//传入stmt来生成YYKVStorageItem实例
item = [self _dbGetItemFromStmt:stmt excludeInlineData:excludeInlineData];
} else {
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
}
}
return item;
}
_dbGetItemFromStmt:excludeInlineData
- (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData {
//提取数据
int i = 0;
char *key = (char *)sqlite3_column_text(stmt, i++);
char *filename = (char *)sqlite3_column_text(stmt, i++);
int size = sqlite3_column_int(stmt, i++);
//判断excludeInlineData
const void *inline_data = excludeInlineData ? NULL : sqlite3_column_blob(stmt, i);
int inline_data_bytes = excludeInlineData ? 0 : sqlite3_column_bytes(stmt, i++);
int modification_time = sqlite3_column_int(stmt, i++);
int last_access_time = sqlite3_column_int(stmt, i++);
const void *extended_data = sqlite3_column_blob(stmt, i);
int extended_data_bytes = sqlite3_column_bytes(stmt, i++);
//将数据赋给item的属性
YYKVStorageItem *item = [YYKVStorageItem new];
if (key) item.key = [NSString stringWithUTF8String:key];
if (filename && *filename != 0) item.filename = [NSString stringWithUTF8String:filename];
item.size = size;
if (inline_data_bytes > 0 && inline_data) item.value = [NSData dataWithBytes:inline_data length:inline_data_bytes];
item.modTime = modification_time;
item.accessTime = last_access_time;
if (extended_data_bytes > 0 && extended_data) item.extendedData = [NSData dataWithBytes:extended_data length:extended_data_bytes];
return item;
}
上面两个方法的作用分别为
1.获取数据库里每一个字段对应的数据
2.将数据赋给YYKVStorageItem的实例
补充一些细节
线程的选择
无论缓存的自动清理和释放,作者默认把这些任务放到子线程去做
- (void)removeAll {
//将开销,缓存数量置为0
_totalCost = 0;
_totalCount = 0;
//将链表的头尾节点置空
_head = nil;
_tail = nil;
if (CFDictionaryGetCount(_dic) > 0) {
CFMutableDictionaryRef holder = _dic;
_dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
//是否在子线程操作
if (_releaseAsynchronously) {
dispatch_queue_t queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
CFRelease(holder); // hold and release in specified queue
});
} else if (_releaseOnMainThread && !pthread_main_np()) {
dispatch_async(dispatch_get_main_queue(), ^{
CFRelease(holder); // hold and release in specified queue
});
} else {
CFRelease(holder);
}
}
}
内存警告和进入后台的监听
YYCache默认在收到内存警告和进入后台时,自动清除所有内存缓存
(instancetype)init{
...
//监听app生命周期
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];
...
}
参考资料: