源码解析--YYCache
前言:准备看下YY系列中的YYWebImage框架,发现该框架是使用YYCache来做缓存的。那就从缓存开始吧.
先奉上YYCache框架的地址以及作者的设计思路
学习YYCache框架你可以get到:
1.优雅的代码风格
2.优秀的接口设计
3.YYCache的层次结构
4.YYMemoryCache类的层次结构和缓存机制
5.YYDiskCache类的层次结构和缓存机制
YYCache
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类结构图
YYMemoryCache初始化的时候会建立空的私有对象YYLinkedMap链表,接下来所有的操作其实就是对这个链表的操作。当然,YYMemoryCache提供了一个定时器接口给你,你可以通过设置autoTrimInterval属性去完成每隔一定时间去检查countLimit,costLimit是否达到了最大限制,并做相应的操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
- (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节点的操作。下图绿色部分代表节点:
双向链表结构
下图是链表节点的结构图:
链表节点
现在我们先来看如何去构造一个链表添加节点:
setObject
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
- (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);
}
|
你可以点击这里自己去操作双向链表
addNode.gif
链表移除节点的操作:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
- (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);
}
|
removeNode.gif
YYMemoryCache类还为我们提供了下列接口方便我们调用:
1
2
3
|
- (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来确定是那种写入方式:
1
2
3
4
5
6
7
8
9
10
11
|
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类型所用的时间:
你可以发现在储存小型数据NSNumberYYKVStorageTypeFile类型是YYKVStorageTypeSQLite大约4倍多,而在大型数据的时候两者的表现是相反的。显然选择合适的储存方式是很有必要的。
这里需要提醒的事:
Demo中YYKVStorageTypeFile类型其实不仅写入了本地文件也同时写入了数据库,只不过数据库里面存的是除了value值以外的key, filename, size, inline_data(NULL), modification_time , last_access_time, extended_data字段。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
- (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];
}
}
|
插入或者是更新数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
- (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对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
- (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删除掉数据库就好了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
- (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类有两种初始化方式:
1
2
3
|
- (nullable instancetype)initWithPath:(NSString *)path;
- (nullable instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold
|
YYDiskCache类持有一个YYKVStorage对象,但是你不能手动的去控制YYKVStorage对象的YYKVStorageType。YYDiskCache类初始化提供一个threshold的参数,默认的为20KB。然后根据这个值得大小来确定YYKVStorageType的类型。
1
2
3
4
5
6
7
8
9
|
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储存方法,下面是实现代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
- (void)setObject:(id(NSCoding))object forKey:(NSString *)key { (因识别问题,此处(NSCoding)替换)
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属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
- (id(NSCoding))objectForKey:(NSString *)key { (因识别问题,此处(NSCoding)替换)
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对象来操作了。
1
2
3
4
5
6
|
- (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。
读后感只有四个字:
如沐春风
转载地址http://www.cocoachina.com/ios/20161230/18479.html
源码解析--YYCache相关推荐
- YYCache 源码解析
YYCache 源码解析 YYCache是国内开发者ibireme开源的一个线程安全的高性能缓存组件,代码风格简洁清晰,在GitHub上已经有了1600+颗星. 阅读它的源码有助于建立比较完整的缓存设 ...
- YYCache 源码解析(一):使用方法,架构与内存缓存的设计
YYCache是国内开发者ibireme开源的一个线程安全的高性能缓存组件,代码风格简洁清晰,阅读它的源码有助于建立比较完整的缓存设计的思路,同时也能巩固一下双向链表,线程锁,数据库操作相关的知识. ...
- iOS本地缓存方案之YYCache源码解析
iOS持久化方案有哪些? 简单列举一下,iOS的本地缓存方案有挺多,各有各的适用场景: NSUserDefault : 系统提供的最简便的key-value本地存储方案,适合比较轻量的数据存储,比如一 ...
- 谷歌BERT预训练源码解析(二):模型构建
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/weixin_39470744/arti ...
- 谷歌BERT预训练源码解析(三):训练过程
目录 前言 源码解析 主函数 自定义模型 遮蔽词预测 下一句预测 规范化数据集 前言 本部分介绍BERT训练过程,BERT模型训练过程是在自己的TPU上进行的,这部分我没做过研究所以不做深入探讨.BE ...
- 谷歌BERT预训练源码解析(一):训练数据生成
目录 预训练源码结构简介 输入输出 源码解析 参数 主函数 创建训练实例 下一句预测&实例生成 随机遮蔽 输出 结果一览 预训练源码结构简介 关于BERT,简单来说,它是一个基于Transfo ...
- Gin源码解析和例子——中间件(middleware)
在<Gin源码解析和例子--路由>一文中,我们已经初识中间件.本文将继续探讨这个技术.(转载请指明出于breaksoftware的csdn博客) Gin的中间件,本质是一个匿名回调函数.这 ...
- Colly源码解析——结合例子分析底层实现
通过<Colly源码解析--框架>分析,我们可以知道Colly执行的主要流程.本文将结合http://go-colly.org上的例子分析一些高级设置的底层实现.(转载请指明出于break ...
- libev源码解析——定时器监视器和组织形式
我们先看下定时器监视器的数据结构.(转载请指明出于breaksoftware的csdn博客) /* invoked after a specific time, repeatable (based o ...
最新文章
- PowerPoint中的LinkFormat对象
- python定义方法继承类_Python类的定义、继承及类对象使用方法简明教程
- cad一键标注闭合区域lisp_CAD快捷键大全,你值得学会!
- 50. Pow(x, n)(递归,穷举)
- 允许其他网络用户通过此计算机的internet连接来连_「Azure云」什么是Azure虚拟网络?...
- php 放大镜代码,jQuery实现放大镜效果实例代码_jquery
- NHibernate (一) 五部曲
- bl系列刀片(blade)服务器,HPE Integrity BL870c i6 刀片服务器
- 房天下搜房网二手房_【杭州二手房|杭州二手房出售】 - 杭州房天下
- 愿天下有情人都是失散多年的兄妹 (25 分)
- 网站SEO优化高质量内容怎么写
- 题解 CF722E 【Research Rover】
- IX redis(1)
- PS中改变人物衣服图案
- Problem N: 设计飞机类Plane及其派生类
- Property 'X' not found on type entity.Customer错误原因分析
- python如何保存excel文件
- C语言中,头文件和源文件的关系(转)
- 蚂蚁集团开源大规模视频侵权定位数据集
- 关于企业微信服务商入门考试v2.0题库
热门文章
- 戒掉这七种思维方式,走向真正的成熟
- 荐书丨企业业务架构的发展及与IT架构的关系
- QQ收藏的表情如何在不同PC端上同步
- 菜菜的刷题日记 | 蓝桥杯 — 十六进制转八进制(纯手撕版)附进制转换笔记
- LeetCode 第 59 场力扣夜喵双周赛(最短路径数+迪杰斯特拉、动态规划+最长公共前缀问题) / 第255场周赛(二进制转换,分组背包,子集还原数组(脑筋急转弯))
- 计算机组成原理片级逻辑图,《组成原理》综合题库
- caffe ssd 优化
- 五五划算节淘宝商家店播成交占比七成,商户分账平台助力直播革新
- 手机WEB页面自动化_在电脑上模拟手机模式页面实现自动化
- 如何用ZBrush给皮肤添加纹理