前言:准备看下YY系列中的YYWebImage
框架,发现该框架是使用YYCache
来做缓存的。那就从缓存开始吧.
先奉上YYCache
框架的地址以及作者的设计思路
学习YYCache
框架你可以get到:
1.优雅的代码风格
2.优秀的接口设计
3.YYCache的层次结构
4.YYMemoryCache类的层次结构和缓存机制
5.YYDiskCache类的层次结构和缓存机制
YYCache
YYCache
最为食物链的最顶端的男人,并没有什么好说的,所以我们就从YYMemoryCache
和YYDiskCache
开始吧。
YYMemoryCache
YYMemoryCache
内存储存是的原理是利用CFDictionary
对象的 key-value
开辟内存储存机制和双向链表原理来实现LRU算法。这里是官方文档对CFDictionary
的解释:
CFMutableDictionary creates dynamic dictionaries where you can add or delete key-value pairs at any time, and the dictionary automatically allocates memory as needed.
YYMemoryCache
初始化的时候会建立空的私有对象YYLinkedMap
链表,接下来所有的操作其实就是对这个链表的操作。当然,YYMemoryCache
提供了一个定时器接口给你,你可以通过设置autoTrimInterval
属性去完成每隔一定时间去检查countLimit
,costLimit
是否达到了最大限制,并做相应的操作。
- (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];
//当前的时间和链表最后的节点时间的差值是否大于设定的_ageLimit值,移除大于该值得节点
[self _trimToAge:self->_ageLimit];
});
}
YYMemoryCache
以block的形式给你提供了下面接口:
- didReceiveMemoryWarningBlock(当app接受到内存警告)
- didEnterBackgroundBlock (当app进入到后台)
当然,你也可以通过设置相应的shouldRemoveAllObjectsOnMemoryWarning
和 shouldRemoveAllObjectsWhenEnteringBackground
值来移除YYMemoryCache
持有的链表。
下面我们来看看YYMemoryCache
类的增,删,查等操作。在这之前我们先看看YYLinkedMap
这个类。
1.YYLinkedMap内部结构
YYLinkedMap
作为双向链表,主要的工作是为YYMemoryCache
类提供对YYLinkedMapNode
节点的操作。下图绿色部分代表节点:
下图是链表节点的结构图:
现在我们先来看如何去构造一个链表添加节点:
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
if (!key) return;
if (!object) {
[self removeObjectForKey:key];
return;
}
//锁
pthread_mutex_lock(&_lock);
//查找是否存在对应该key的节点
_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算法原理,将访问的点移到最前面
[_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);
}
你可以点击这里自己去操作双向链表
链表移除节点的操作:
- (void)removeObjectForKey:(id)key {
if (!key) return;
//锁
pthread_mutex_lock(&_lock);
//根据key拿到相应的节点
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
if (node) {
[_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);
}
YYMemoryCache
类还为我们提供了下列接口方便我们调用:
- (BOOL)containsObjectForKey:(id)key;
- (nullable id)objectForKey:(id)key;
- (void)removeAllObjects;
总结:YYMemoryCache
是利用key-value机制内存缓存类,所有的方法都是线程安全的。如果你熟悉NSCache
类,你会发现两者的接口很是相似。
当然YYMemoryCache
有着自己的特点:
1.YYMemoryCache
采用LRU(least-recently-used)算法来移除节点。
2.YYMemoryCache
可以用countLimit
,costLimit
,ageLimit
属性做相应的控制。
3.YYMemoryCache
类可以设置相应的属性来控制退到后台或者接受到内存警告的时候移除链表。
YYKVStorage
YYKVStorage
是一个基于sql数据库和文件写入的缓存类,注意它并不是线程安全。你可以自己定义YYKVStorageType
来确定是那种写入方式:
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
/// The `value` is stored as a file in file system.
YYKVStorageTypeFile = 0,
/// The `value` is stored in sqlite with blob type.
YYKVStorageTypeSQLite = 1,
/// The `value` is stored in file system or sqlite based on your choice.
YYKVStorageTypeMixed = 2,
};
1.写入和更新
我们看看Demo
中直接用YYKVStorage
储存NSNumber和NSData YYKVStorageTypeFile
和YYKVStorageTypeSQLite
类型所用的时间:
你可以发现在储存小型数据NSNumber
YYKVStorageTypeFile
类型是YYKVStorageTypeSQLite
大约4倍多,而在大型数据的时候两者的表现是相反的。显然选择合适的储存方式是很有必要的。这里需要提醒的事:
Demo
中YYKVStorageTypeFile
类型其实不仅写入了本地文件也同时写入了数据库,只不过数据库里面存的是除了value值以外的key, filename, size, inline_data(NULL), modification_time , last_access_time, extended_data字段。
- (BOOL)saveItemWithKey:(NSString *)key value:(NSData *)value filename:(NSString *)filename extendedData:(NSData *)extendedData {
if (key.length == 0 || value.length == 0) return NO;
//_type为YYKVStorageTypeSQLite时候filename应该为空,不然还是会写入文件
//_type为YYKVStorageTypeFile时候filename的值不能为空
if (_type == YYKVStorageTypeFile && filename.length == 0) {
return NO;
}
//是否写入文件是根据filename.length长度来判断的
if (filename.length) {
//先储存在文件里面
if (![self _fileWriteWithName:filename data:value]) {
return NO;
}
//储存在sql数据库
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];
}
}
//储存在sql数据库
return [self _dbSaveWithKey:key value:value fileName:nil extendedData:extendedData];
}
}
插入或者是更新数据库
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
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);
//sqlite3_bind_xxx函数给这条语句绑定参数
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);
sqlite3_bind_int(stmt, 3, (int)value.length);
//当fileName为空的时候存在数据库的是value.bytes,不然存的是NULl对象
if (fileName.length == 0) {
sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
} else {
sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
}
sqlite3_bind_int(stmt, 5, timestamp);
sqlite3_bind_int(stmt, 6, timestamp);
sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0);
//通过sqlite3_step命令执行创建表的语句
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;
}
2.读取
我们尝试的去缓存里面拿取数据,我们发现当为YYKVStorage
对象type不同,存取的方式不同所以读取的方式也不同:
1.因为在插入的时候我们就说了,当为YYKVStorageTypeFile
类型的时候数据是存在本地文件的其他存在数据库。所以YYKVStorage
对象先根据key从数据库拿到数据然后包装成YYKVStorageItem
对象,然后再根据filename
读取本地文件数据赋给YYKVStorageItem
对象的value属性。
2.当为YYKVStorageTypeSQLite
类型就是直接从数据库把所有数据都读出来赋给YYKVStorageItem
对象。
- (YYKVStorageItem *)getItemForKey:(NSString *)key {
if (key.length == 0) return nil;
/*先从数据库读包装item,
当时filename不为空的时候,以为着数据库里面没有存Value值,还得去文件里面读出来value值
当时filename为空的时候,意味着直接从数据库来拿取Value值
*/
YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
if (item) {
//更新的last_access_time字段
[self _dbUpdateAccessTimeWithKey:key];
if (item.filename) {
//从文件里面读取value值
item.value = [self _fileReadWithName:item.filename];
if (!item.value) {
//数据为空则从数据库删除这条记录
[self _dbDeleteItemWithKey:key];
item = nil;
}
}
}
return item;
}
3.删除
YYKVStorage
的type当为YYKVStorageTypeFile
类型是根据key将本地和数据库都删掉,而YYKVStorageTypeSQLite
是根据key删除掉数据库就好了。
- (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;
}
}
我们这里分别列取了增删改查的单个key的操作,你还可以去批量的去操作key的数组。但是其实都大同小异的流程,就不一一累述了。上个图吧:
这个类也就看的差不多了,但是要注意的事,YYCache作者并不希望我们直接使用这个类,而是使用更高层的
YYDiskCache
类。那我们就继续往下面看吧。
YYDiskCache
YYDiskCache
类有两种初始化方式:
- (nullable instancetype)initWithPath:(NSString *)path;
- (nullable instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold
YYDiskCache
类持有一个YYKVStorage
对象,但是你不能手动的去控制YYKVStorage
对象的YYKVStorageType
。YYDiskCache
类初始化提供一个threshold
的参数,默认的为20KB。然后根据这个值得大小来确定YYKVStorageType
的类型。
YYKVStorageType type;
if (threshold == 0) {
type = YYKVStorageTypeFile;
} else if (threshold == NSUIntegerMax) {
type = YYKVStorageTypeSQLite;
} else {
type = YYKVStorageTypeMixed;
}
YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
因为YYDiskCache
类的操作其实就是去操作持有的YYKVStorage
对象,所以下面的部分会比较建简略。
写入和更新
在调用YYKVStorage
对象的储存操作前主要做了下面几项操作:
1.key和object的判空容错机制
2.利用runtime机制去取extendedData数据
3.根据是否定义了_customArchiveBlock来判断选择序列化object还是block回调得到value
4.value的判空容错机制
5.根据YYKVStorage
的type判断以及_inlineThreshold和value值得长度来判断是否选择以文件的形式储存value值。上面我们说过当value比较大的时候文件储存速度比较快速。
6.如果_customFileNameBlock为空,则根据key通过md5加密得到转化后的filename.不然直接拿到_customFileNameBlock关联的filename。生成以后操作文件的路径
做完上面的操作则直接调用YYKVStorage
储存方法,下面是实现代码:
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
if (!key) return;
if (!object) {
[self removeObjectForKey:key];
return;
}
//runtime 取extended_data_key的value
NSData *extendedData = [YYDiskCache getExtendedDataFromObject:object];
NSData *value = nil;
if (_customArchiveBlock) {
//block返回
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) {
//长度判断这个储存方式,value.length当大于_inlineThreshold则文件储存
if (value.length > _inlineThreshold) {
//将key 进行md5加密
filename = [self _filenameForKey:key];
}
}
Lock();
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}
读取
读取操作一般都是和写入操作相辅相成的,我们来看看在调用YYKVStorage
对象的读取操作后做了哪些操作:
1.item.value的判空容错机制
2.根据_customUnarchiveBlock值来判断是直接将item.value block回调还是反序列化成object
3.根据object && item.extendedData 来决定是否runtime添加extended_data_key属性
- (id<NSCoding>)objectForKey:(NSString *)key {
if (!key) return nil;
Lock();
YYKVStorageItem *item = [_kv getItemForKey:key];
Unlock();
if (!item.value) return nil;
id object = nil;
if (_customUnarchiveBlock) {
object = _customUnarchiveBlock(item.value);
} else {
@try {
object = [NSKeyedUnarchiver unarchiveObjectWithData:item.value];
}
@catch (NSException *exception) {
// nothing to do...
}
}
if (object && item.extendedData) {
[YYDiskCache setExtendedData:item.extendedData toObject:object];
}
return object;
}
删除
删除操作就是直接调用的YYKVStorage
对象来操作了。
- (void)removeObjectForKey:(NSString *)key {
if (!key) return;
Lock();
[_kv removeItemForKey:key];
Unlock();
}
当然,YYDiskCache
和YYMemoryCache
一样也给你提供了一些类似limit
的接口供你操作。
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;
和YYKVStorage
不一样的是,作为更高层的YYDiskCache
是一个线程安全的类。你应该使用YYDiskCache
而不是YYKVStorage
。
最后再带一笔食物端最顶端的男人YYCache
,当他写入的时候会同时调用YYDiskCache
磁盘操作和YYMemoryCache
内存操作。读取的时候先从内存读取,因为在内存的读取速度比磁盘快很多,如果没有读取到数据才会去磁盘读取。
读后感只有四个字:
如沐春风