内容简介:本篇文章将带来YYCache的解读,YYCache支持内存和本地两种方式的数据存储。我们先抛出两个问题:先来看看YYCache整体框架设计图**分析下这个框架 **
本篇文章将带来YYCache的解读,YYCache支持内存和本地两种方式的数据存储。我们先抛出两个问题:
- YYCache是如何把数据写入内存之中的?又是如何实现的高效读取?
- YYCache采用了何种方式把数据写入磁盘?
先来看看YYCache整体框架设计图
**分析下这个框架 **
从上图可以很清晰的看出这个框架的基础框架为:
交互层-逻辑层-处理层。
无论是最上层的YYCache或下面两个独立体YYMemoryCache和YYDiskCache都是这样设计的。逻辑清晰。 来看看YYMemoryCache,逻辑层是负责对交付层的一个转接和对处理层发布一些命令,比如增删改查。而处理层则是对逻辑层发布的命令进行具体对应的处理。组件层则是对处理层的一种补充,也是元数据,扩展性很好。这两个类我们都是可以单独拿出来使用的。而多添加一层YYCache,则可以让我们使用的更加方便,自动为我们同时进行内存缓存和磁盘缓存,读取的时候获得内存缓存的高速读取。
YYMemoryCache
我们使用YYMemoryCache可以把数据缓存进内存之中,它内部会创建了一个YYMemoryCache对象,然后把数据保存进这个对象之中。
但凡涉及到类似这样的操作,代码都需要设计成线程安全的。所谓的线程安全就是指充分考虑多线程条件下的增删改查操作。
要想高效的查询数据,使用字典是一个很好的方法。字典的原理跟哈希有关,总之就是把key直接映射成内存地址,然后处理冲突和和扩容的问题。对这方面有兴趣的可以自行搜索资料
YYMemoryCache内部封装了一个对象_YYLinkedMap,包含了下边这些属性:
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; //哈希字典,存放缓存数据
NSUInteger _totalCost; //缓存总大小
NSUInteger _totalCount; //缓存节点总个数
_YYLinkedMapNode *_head; //头结点
_YYLinkedMapNode *_tail; //尾结点
BOOL _releaseOnMainThread; //在主线程释放
BOOL _releaseAsynchronously;//在异步线程释放
}
@end
复制代码
_dic是哈希字典,负责存放缓存数据,_head和_tail分别是双链表中指向头节点和尾节点的指针,链表中的节点单元是_YYLinkedMapNode对象,该对象封装了缓存数据的信息。
可以看出来,CFMutableDictionaryRef _dic将被用来保存数据。这里使用了CoreFoundation的字典,性能更好。字典里边保存着的是_YYLinkedMapNode对象。
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; //前向前一个节点的指针
__unsafe_unretained _YYLinkedMapNode *_next; //指向下一个节点的指针
id _key; //缓存数据key
id _value; //缓存数据value
NSUInteger _cost; //节点占用大小
NSTimeInterval _time; //节点操作时间戳
}
@end
复制代码
上边的代码,就能知道使用了链表的知识。但是有一个疑问,单用字典我们就能很快的查询出数据,为什么还要实现链表这一数据结构呢?
答案就是淘汰算法,YYMemoryCache使用了LRU淘汰算法,也就是当数据超过某个限制条件后,我们会从链表的尾部开始删除数据,直到达到要求为止。
LRU
LRU全称是Least recently used,基于最近最少使用的原则,属于一种缓存淘汰算法。实现思路是维护一个双向链表数据结构,每次有新数据要缓存时,将缓存数据包装成一个节点,插入双向链表的头部,如果访问链表中的缓存数据,同样将该数据对应的节点移动至链表的头部。这样的做法保证了被使用的数据(存储/访问)始终位于链表的前部。当缓存的数据总量超出容量时,先删除末尾的缓存数据节点,因为末尾的数据最少被使用过。如下图:
通过这种方式,就实现了类似数组的功能,是原本无序的字典成了有序的集合。
如果有一列数据已经按顺序排好了,我使用了中间的某个数据,那么就要把这个数据插入到最开始的位置,这就是一条规则,越是最近使用的越靠前。
在设计上,YYMemoryCache还提供了是否异步释放数据这一选项,在这里就不提了,我们在来看看在YYMemoryCache中用到的锁的知识。
pthread_mutex_lock是一种互斥所:
pthread_mutex_init(&_lock, NULL); // 初始化 pthread_mutex_lock(&_lock); // 加锁 pthread_mutex_unlock(&_lock); // 解锁 pthread_mutex_trylock(&_lock) == 0 // 是否加锁,0:未锁住,其他值:锁住 复制代码
在OC中有很多种锁可以用,pthread_mutex_lock就是其中的一种。YYMemoryCache有这样一种设置,每隔一个固定的时间就要处理数据,及边界检测,代码如下:
边界检测
- (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];//递归调用本方法
});
}
复制代码
上边的代码中,每隔_autoTrimInterval时间就会在后台尝试处理数据,然后再次调用自身,这样就实现了一个类似定时器的功能。这一个小技巧可以学习一下。
- (void)_trimInBackground {
dispatch_async(_queue, ^{
[self _trimToCost:self->_costLimit];
[self _trimToCount:self->_countLimit];
[self _trimToAge:self->_ageLimit];
});
}
复制代码
_trimInBackground分别调用_trimToCost、_trimToCount和_trimToAge方法检测。
_trimToCost方法判断链表中所有节点占用大小之和totalCost是否大于costLimit,如果超过,则从链表末尾开始删除节点,直到totalCost小于等于costLimit为止。代码注释如下:
- (void)_trimToCost:(NSUInteger)costLimit {
BOOL finish = NO;
pthread_mutex_lock(&_lock);
if (costLimit == 0) {
[_lru removeAll];
finish = YES;
} else if (_lru->_totalCost <= costLimit) {
finish = YES;
}
pthread_mutex_unlock(&_lock);
if (finish) return;
NSMutableArray *holder = [NSMutableArray new];
while (!finish) {
if (pthread_mutex_trylock(&_lock) == 0) {
if (_lru->_totalCost > costLimit) {
_YYLinkedMapNode *node = [_lru removeTailNode];//删除末尾节点
if (node) [holder addObject:node];
} else {
finish = YES; //totalCost<=costLimit,检测完成
}
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 释放了资源
});
}
}
复制代码
其中每个节点的cost是人为指定的,默认是0,且costLimit默认是NSUIntegerMax,所以在默认情况下,_trimToCost方法不会删除末尾的节点。
_trimToCount方法判断链表中的所有节点个数之和是否大于countLimit,如果超过,则从链表末尾开始删除节点,直到个数之和小于等于countLimit为止。代码注释如下:
- (void)_trimToCount:(NSUInteger)countLimit {
BOOL finish = NO;
...
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; //totalCount<=countLimit,检测完成
}
pthread_mutex_unlock(&;_lock);
} else {
usleep(10 * 1000); //10 ms等待一小段时间
}
}
...
}
复制代码
初始化时countLimit默认是NSUIntegerMax,如果不指定countLimit,节点的总个数永远不会超过这个限制,所以_trimToCount方法不会删除末尾节点。
_trimToAge方法遍历链表中的节点,删除那些和now时刻的时间间隔大于ageLimit的节点,代码如下:
- (void)_trimToAge:(NSTimeInterval)ageLimit {
BOOL finish = NO;
...
NSMutableArray *holder = [NSMutableArray new];
while (!finish) {
if (pthread_mutex_trylock(&;_lock) == 0) {
if (_lru->_tail &;&; (now - _lru->_tail->_time) > ageLimit) { //间隔大于ageLimit
_YYLinkedMapNode *node = [_lru removeTailNode]; //删除末尾节点
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&;_lock);
} else {
usleep(10 * 1000); //10 ms
}
}
...
}
复制代码
由于链表中从头部至尾部的节点,访问时间由晚至早,所以尾部节点和now时刻的时间间隔较大,从尾节点开始删除。ageLimit的默认值是DBL_MAX,如果不人为指定ageLimit,则链表中节点不会被删除。
当某个变量在出了自己的作用域之后,正常情况下就会被自动释放。
存储数据
调用setObject: forKey:方法存储缓存数据,代码如下:
- (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对应的缓存数据
//更新totalCost
_lru->_totalCost -= node->_cost;
_lru->_totalCost += cost;
node->_cost = cost;
node->_time = now; //更新节点的访问时间
node->_value = object; //更新节点中存放的缓存数据
[_lru bringNodeToHead:node]; //将节点移至链表头部
} else { //如果不能取到,说明链表中之前不存在key对应的缓存数据
node = [_YYLinkedMapNode new]; //创建新的节点
node->_cost = cost;
node->_time = now; //设置节点的时间
node->_key = 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];
...
}
pthread_mutex_unlock(&;_lock); //解锁
}
复制代码
首先判断key和object是否为空,object如果为空,删除缓存中key对应的数据。然后从字典中查找key对应的缓存数据,分为两种情况,如果访问到节点,说明缓存数据存在,则根据最近最少使用原则,将本次操作的节点移动至链表的头部,同时更新节点的访问时间。如果访问不到节点,说明是第一次添加key和数据,需要创建一个新的节点,把节点存入字典中,并且加入链表头部。cost是指定的,默认是0。
访问数据
调用objectForKey:方法访问缓存数据,代码注释如下:
- (id)objectForKey:(id)key {
if (!key) return nil;
pthread_mutex_lock(&;_lock);
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); //从字典中读取key相应的节点
if (node) {
node->_time = CACurrentMediaTime(); //更新节点访问时间
[_lru bringNodeToHead:node]; //将节点移动至链表头部
}
pthread_mutex_unlock(&;_lock);
return node ? node->_value : nil;
}
复制代码
该方法从字典中获取缓存数据,如果key对应的数据存在,则更新访问时间,根据最近最少使用原则,将本次操作的节点移动至链表的头部。如果不存在,则直接返回nil。
线程同步
- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
pthread_mutex_lock(&;_lock);
//操作链表,写缓存数据
pthread_mutex_unlock(&;_lock);
}
- (id)objectForKey:(id)key {
pthread_mutex_lock(&;_lock);
//访问缓存数据
pthread_mutex_unlock(&;_lock);
}
复制代码
如果存在线程A和B,线程A在写缓存的时候,上锁,线程B读取缓存数据时,被阻塞,需要等到线程A执行完写缓存的操作,调用pthread_mutex_unlock后,线程B才能读缓存数据,这个时候新的缓存数据已经写完,保证了操作的数据的同步。
总结
YYMemoryCache操作了内存缓存,相较于硬盘缓存需要进行I/O操作,在性能上快很多,因此YYCache访问缓存时,优先用的是YYMemoryCache。
YYMemoryCache实际上就是创建了一个对象实例,该对象内部使用字典和双向链表实现
YYDiskCache
YYDiskCache通过文件和 SQLite 数据库两种方式存储缓存数据.YYKVStorage核心功能类,实现了文件读写和数据库读写的功能。YYKVStorageYYKVStorage定义了读写缓存数据的三种枚举类型,即typedefNS_ENUM(NSUInteger,YYKVStorageType)
YYKVStorage
YYKVStorage最核心的思想是KV这两个字母,表示key-value的意思,目的是让使用者像使用字典一样操作数据
YYKVStorage让我们只关心3件事:
- 数据保存的路径
- 保存数据,并为该数据关联一个key
- 根据key取出数据或删除数据
同理,YYKVStorage在设计接口的时候,也从这3个方面进行了考虑。这数据功能设计层面的思想。
YYKVStorage定义了读写缓存数据的三种枚举类型,即
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
//文件读取
YYKVStorageTypeFile = 0,
//数据库读写
YYKVStorageTypeSQLite = 1,
//根据策略决定使用文件还是数据库读写数据
YYKVStorageTypeMixed = 2,
};
复制代码
初始化
调用initWithPath: type:方法进行初始化,指定了存储方式,创建了缓存文件夹和SQLite数据库用于存放缓存,打开并初始化数据库。下面是部分代码注释:
- (instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type {
...
self = [super init];
_path = path.copy;
_type = type; //指定存储方式,是数据库还是文件存储
_dataPath = [path stringByAppendingPathComponent:kDataDirectoryName]; //缓存数据的文件路径
_trashPath = [path stringByAppendingPathComponent:kTrashDirectoryName]; //存放垃圾缓存数据的文件路径
_trashQueue = dispatch_queue_create("com.ibireme.cache.disk.trash", DISPATCH_QUEUE_SERIAL);
_dbPath = [path stringByAppendingPathComponent:kDBFileName]; //数据库路径
_errorLogsEnabled = YES;
NSError *error = nil;
//创建缓存数据的文件夹和垃圾缓存数据的文件夹
if (![[NSFileManager defaultManager] createDirectoryAtPath:path
withIntermediateDirectories:YES
attributes:nil
error:&;error] ||
![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:kDataDirectoryName]
withIntermediateDirectories:YES
attributes:nil
error:&;error] ||
![[NSFileManager defaultManager] createDirectoryAtPath:[path stringByAppendingPathComponent:kTrashDirectoryName]
withIntermediateDirectories:YES
attributes:nil
error:&;error]) {
NSLog(@"YYKVStorage init error:%@", error);
return nil;
}
//创建并打开数据库、在数据库中建表
//_dbOpen方法创建和打开数据库manifest.sqlite
//调用_dbInitialize方法创建数据库中的表
if (![self _dbOpen] || ![self _dbInitialize]) {
// db file may broken...
[self _dbClose];
[self _reset]; // rebuild
if (![self _dbOpen] || ![self _dbInitialize]) {
[self _dbClose];
NSLog(@"YYKVStorage init error: fail to open sqlite db.");
return nil;
}
}
//调用_fileEmptyTrashInBackground方法将trash目录中的缓存数据删除
[self _fileEmptyTrashInBackground]; // empty the trash if failed at last time
return self;
}
复制代码
_dbInitialize方法调用 sql 语句在数据库中创建一张表,代码如下:
- (BOOL)_dbInitialize {
NSString *sql = @"pragma journal_mode = wal; pragma synchronous = normal; create table if not exists manifest (key text, filename text, size integer, inline_data blob, modification_time integer, last_access_time integer, extended_data blob, primary key(key)); create index if not exists last_access_time_idx on manifest(last_access_time);";
return [self _dbExecute:sql];
}
复制代码
"pragma journal_mode = wal"表示使用WAL模式进行数据库操作,如果不指定,默认DELETE模式,是"journal_mode=DELETE"。使用WAL模式时,改写操作数据库的操作会先写入WAL文件,而暂时不改动数据库文件,当执行checkPoint方法时,WAL文件的内容被批量写入数据库。checkPoint操作会自动执行,也可以改为手动。WAL模式的优点是支持读写并发,性能更高,但是当wal文件很大时,需要调用checkPoint方法清空wal文件中的内容
dataPath和trashPath用于文件的方式读写缓存数据,当dataPath中的部分缓存数据需要被清除时,先将其移至trashPath中,然后统一清空trashPath中的数据,类似回收站的思路。_dbPath是数据库文件,需要创建并初始化,下面是路径: 在真实的编程中,往往需要把数据封装成一个对象:
调用_dbOpen方法创建和打开数据库manifest.sqlite,调用_dbInitialize方法创建数据库中的表。调用_fileEmptyTrashInBackground方法将trash目录中的缓存数据删除
/** YYKVStorageItem is used by `YYKVStorage` to store key-value pair and meta data. Typically, you should not use this class directly. */ @interface YYKVStorageItem : NSObject @property (nonatomic, strong) NSString *key; //缓存数据的key @property (nonatomic, strong) NSData *value; //缓存数据的value @property (nullable, nonatomic, strong) NSString *filename; //缓存文件名(文件缓存时有用) @property (nonatomic) int size; //数据大小 @property (nonatomic) int modTime; //数据修改时间(用于更新相同key的缓存) @property (nonatomic) int accessTime; //数据访问时间 @property (nullable, nonatomic, strong) NSData *extendedData; //附加数据 @end 复制代码
缓存数据是按一条记录的格式存入数据库的,这条SQL记录包含的字段如下:
key(键)、fileName(文件名)、size(大小)、inline_data(value/二进制数据)、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;
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];
}
}
复制代码
该方法首先判断fileName即文件名是否为空,如果存在,则调用_fileWriteWithName方法将缓存的数据写入文件系统中,同时将数据写入数据库,需要注意的是,调用_dbSaveWithKey:value:fileName:extendedData:方法会创建一条SQL记录写入表中
代码注释如下:
- (BOOL)_dbSaveWithKey:(NSString *)key value:(NSData *)value fileName:(NSString *)fileName extendedData:(NSData *)extendedData {
//构建sql语句,将一条记录添加进manifest表
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]; //准备sql语句,返回stmt指针
if (!stmt) return NO;
int timestamp = (int)time(NULL);
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); //绑定参数值对应"?1"
sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL); //绑定参数值对应"?2"
sqlite3_bind_int(stmt, 3, (int)value.length);
if (fileName.length == 0) { //如果fileName不存在,绑定参数值value.bytes对应"?4"
sqlite3_bind_blob(stmt, 4, value.bytes, (int)value.length, 0);
} else { //如果fileName存在,不绑定,"?4"对应的参数值为null
sqlite3_bind_blob(stmt, 4, NULL, 0, 0);
}
sqlite3_bind_int(stmt, 5, timestamp); //绑定参数值对应"?5"
sqlite3_bind_int(stmt, 6, timestamp); //绑定参数值对应"?6"
sqlite3_bind_blob(stmt, 7, extendedData.bytes, (int)extendedData.length, 0); //绑定参数值对应"?7"
int result = sqlite3_step(stmt); //开始执行sql语句
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;
}
复制代码
该方法首先创建sql语句,value括号中的参数"?"表示参数需要通过变量绑定,"?"后面的数字表示绑定变量对应的索引号,如果VALUES (?1, ?1, ?2),则可以用同一个值绑定多个变量
然后调用_dbPrepareStmt方法构建数据位置指针stmt,标记查询到的数据位置,sqlite3_prepare_v2()方法进行数据库操作的准备工作,第一个参数为成功打开的数据库指针db,第二个参数为要执行的sql语句,第三个参数为stmt指针的地址,这个方法也会返回一个int值,作为标记状态是否成功
接着调用sqlite3_bind_text()方法将实际值作为变量绑定sql中的"?"参数,序号对应"?"后面对应的数字。不同类型的变量调用不同的方法,例如二进制数据是sqlite3_bind_blob方法
同时判断如果fileName存在,则生成的sql语句只绑定数据的相关描述,不绑定inline_data,即实际存储的二进制数据,因为该缓存之前已经将二进制数据写进文件。这样做可以防止缓存数据同时写入文件和数据库,造成缓存空间的浪费。如果fileName不存在,则只写入数据库中,这时sql语句绑定inline_data,不绑定fileName
最后执行sqlite3_step方法执行sql语句,对stmt指针进行移动,并返回一个int值。
删除缓存数据
removeItemForKey:方法
该方法删除指定key对应的缓存数据,区分type,如果是YYKVStorageTypeSQLite,调用_dbDeleteItemWithKey:从数据库中删除对应key的缓存记录,如下:
- (BOOL)_dbDeleteItemWithKey:(NSString *)key {
NSString *sql = @"delete from manifest where key = ?1;"; //sql语句
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql]; //准备stmt
if (!stmt) return NO;
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); //绑定参数
int result = sqlite3_step(stmt); //执行sql语句
...
return YES;
}
复制代码
如果是YYKVStorageTypeFile或者YYKVStorageTypeMixed,说明可能缓存数据之前可能被写入文件中,判断方法是调用_dbGetFilenameWithKey:方法从数据库中查找key对应的SQL记录的fileName字段。该方法的流程和上面的方法差不多,只是sql语句换成了select查询语句。如果查询到fileName,说明数据之前写入过文件中,调用_fileDeleteWithName方法删除数据,同时删除数据库中的记录。否则只从数据库中删除SQL记录
removeItemForKeys:方法
该方法和上一个方法类似,删除一组key对应的缓存数据,同样区分type,对于YYKVStorageTypeSQLite,调用_dbDeleteItemWithKeys:方法指定sql语句删除一组记录,如下:
- (BOOL)_dbDeleteItemWithKeys:(NSArray *)keys {
if (![self _dbCheck]) return NO;
//构建sql语句
NSString *sql = [NSString stringWithFormat:@"delete from manifest where key in (%@);", [self _dbJoinedKeys:keys]];
sqlite3_stmt *stmt = NULL;
int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &;stmt, NULL);
...
//绑定变量
[self _dbBindJoinedKeys:keys stmt:stmt fromIndex:1];
result = sqlite3_step(stmt); //执行参数
sqlite3_finalize(stmt); //对stmt指针进行关闭
...
return YES;
}
复制代码
其中_dbJoinedKeys:方法是拼装,?,?,?格式,_dbBindJoinedKeys:stmt:fromIndex:方法绑定变量和参数,如果?后面没有参数,则sqlite3_bind_text方法的第二个参数,索引值依次对应sql后面的"?"
如果是YYKVStorageTypeFile或者YYKVStorageTypeMixed,通过_dbGetFilenameWithKeys:方法返回一组fileName,根据每一个fileName删除文件中的缓存数据,同时删除数据库中的记录,否则只从数据库中删除SQL记录
removeItemsLargerThanSize:方法删除那些size大于指定size的缓存数据。同样是区分type,删除的逻辑也和上面的方法一致。_dbDeleteItemsWithSizeLargerThan方法除了sql语句不同,操作数据库的步骤相同。_dbCheckpoint方法调用sqlite3_wal_checkpoint方法进行checkpoint操作,将数据同步到数据库中
读取缓存数据
getItemValueForKey:方法
该方法通过key访问缓存数据value,区分type,如果是YYKVStorageTypeSQLite,调用_dbGetValueWithKey:方法从数据库中查询key对应的记录中的inline_data。如果是YYKVStorageTypeFile,首先调用_dbGetFilenameWithKey:方法从数据库中查询key对应的记录中的filename,根据filename从文件中删除对应缓存数据。如果是YYKVStorageTypeMixed,同样先获取filename,根据filename是否存在选择用相应的方式访问。代码注释如下:
- (NSData *)getItemValueForKey:(NSString *)key {
if (key.length == 0) return nil;
NSData *value = nil;
switch (_type) {
case YYKVStorageTypeFile:
{
NSString *filename = [self _dbGetFilenameWithKey:key]; //从数据库中查找filename
if (filename) {
value = [self _fileReadWithName:filename]; //根据filename读取数据
if (!value) {
[self _dbDeleteItemWithKey:key]; //如果没有读取到缓存数据,从数据库中删除记录,保持数据同步
value = nil;
}
}
}
break;
case YYKVStorageTypeSQLite:
{
value = [self _dbGetValueWithKey:key]; //直接从数据中取inline_data
}
break;
case YYKVStorageTypeMixed: {
NSString *filename = [self _dbGetFilenameWithKey:key]; //从数据库中查找filename
if (filename) {
value = [self _fileReadWithName:filename]; //根据filename读取数据
if (!value) {
[self _dbDeleteItemWithKey:key]; //保持数据同步
value = nil;
}
} else {
value = [self _dbGetValueWithKey:key]; //直接从数据中取inline_data
}
}
break;
}
if (value) {
[self _dbUpdateAccessTimeWithKey:key]; //更新访问时间
}
return value;
}
复制代码
调用方法用于更新该数据的访问时间,即sql记录中的last_access_time字段。
getItemForKey:方法
该方法通过key访问数据,返回YYKVStorageItem封装的缓存数据。首先调用_dbGetItemWithKey:excludeInlineData:从数据库中查询,下面是代码注释:
- (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData {
//查询sql语句,是否排除inline_data
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]; //准备工作,构建stmt
if (!stmt) return nil;
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL); //绑定参数
YYKVStorageItem *item = nil;
int result = sqlite3_step(stmt); //执行sql语句
if (result == SQLITE_ROW) {
item = [self _dbGetItemFromStmt:stmt excludeInlineData:excludeInlineData]; //取出查询记录中的各个字段,用YYKVStorageItem封装并返回
} else {
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite query error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
}
}
return item;
}
复制代码
sql语句是查询符合key值的记录中的各个字段,例如缓存的key、大小、二进制数据、访问时间等信息, excludeInlineData表示查询数据时,是否要排除inline_data字段,即是否查询二进制数据,执行sql语句后,通过stmt指针和_dbGetItemFromStmt:excludeInlineData:方法取出各个字段,并创建YYKVStorageItem对象,将记录的各个字段赋值给各个属性,代码注释如下:
- (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData {
int i = 0;
char *key = (char *)sqlite3_column_text(stmt, i++); //key
char *filename = (char *)sqlite3_column_text(stmt, i++); //filename
int size = sqlite3_column_int(stmt, i++); //数据大小
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++);
//用YYKVStorageItem对象封装
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; //返回YYKVStorageItem对象
}
复制代码
最后取出YYKVStorageItem对象后,判断filename属性是否存在,如果存在说明缓存的二进制数据写进了文件中,此时返回的YYKVStorageItem对象的value属性是nil,需要调用_fileReadWithName:方法从文件中读取数据,并赋值给YYKVStorageItem的value属性。代码注释如下:
- (YYKVStorageItem *)getItemForKey:(NSString *)key {
if (key.length == 0) return nil;
//从数据库中查询记录,返回YYKVStorageItem对象,封装了缓存数据的信息
YYKVStorageItem *item = [self _dbGetItemWithKey:key excludeInlineData:NO];
if (item) {
[self _dbUpdateAccessTimeWithKey:key]; //更新访问时间
if (item.filename) { //filename存在,按照item.value从文件中读取
item.value = [self _fileReadWithName:item.filename];
...
}
}
return item;
}
复制代码
getItemForKeys:方法
返回一组YYKVStorageItem对象信息,调用_dbGetItemWithKeys:excludeInlineData:方法获取一组YYKVStorageItem对象。访问逻辑和getItemForKey:方法类似,sql语句的查询条件改为多个key匹配。
getItemValueForKeys:方法
返回一组缓存数据,调用getItemForKeys:方法获取一组YYKVStorageItem对象后,取出其中的value,存入一个临时字典对象后返回。
YYDiskCache
YYDiskCache是上层调用YYKVStorage的类,对外提供了存、删、查、边界控制的方法。内部维护了三个变量,如下:
@implementation YYDiskCache {
YYKVStorage *_kv;
dispatch_semaphore_t _lock;
dispatch_queue_t _queue;
}
复制代码
_kv用于缓存数据,_lock是信号量变量,用于多线程访问数据时的同步操作。
初始化方法
nitWithPath:inlineThreshold:方法用于初始化,下面是代码注释:
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
...
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];
...
return self;
}
复制代码
根据threshold参数决定缓存的type,默认threshold是20KB,会选择YYKVStorageTypeMixed方式,即根据缓存数据的size进一步决定。然后初始化YYKVStorage对象,信号量、各种limit参数。
写缓存
setObject:forKey:方法存储数据,首先判断type,如果是YYKVStorageTypeSQLite,则直接将数据存入数据库中,filename传nil,如果是YYKVStorageTypeFile或者YYKVStorageTypeMixed,则判断要存储的数据的大小,如果超过threshold(默认20KB),则需要将数据写入文件,并通过key生成filename。YYCache的作者认为当数据代销超过20KB时,写入文件速度更快。代码注释如下:
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
...
value = [NSKeyedArchiver archivedDataWithRootObject:object]; //序列化
...
NSString *filename = nil;
if (_kv.type != YYKVStorageTypeSQLite) {
if (value.length > _inlineThreshold) { //value大于阈值,用文件方式存储value
filename = [self _filenameForKey:key];
}
}
Lock();
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData]; //filename存在,数据库中不写入value,即inline_data字段为空
Unlock();
}
//读缓存
objectForKey:方法调用YYKVStorage对象的getItemForKey:方法读取数据,返回YYKVStorageItem对象,取出value属性,进行反序列化。
//删除缓存
removeObjectForKey:方法调用YYKVStorage对象的removeItemForKey:方法删除缓存数据
复制代码
边界控制
在前一篇文章中,YYMemoryCache实现了内存缓存的LRU算法,YYDiskCache也试了LRU算法,在初始化的时候调用_trimRecursively方法每个一定时间检测一下缓存数据大小是否超过容量。
数据同步
YYMemoryCache使用了互斥锁来实现多线程访问数据的同步性,YYDiskCache使用了信号量来实现,下面是两个宏:
#define Lock() dispatch_semaphore_wait(self->_lock, DISPATCH_TIME_FOREVER) #define Unlock() dispatch_semaphore_signal(self->_lock) 复制代码
读写缓存数据的方法中都调用了宏:
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key
{
...
Lock();
[_kv saveItemWithKey:key value:value filename:filename extendedData:extendedData];
Unlock();
}
- (id<NSCoding>)objectForKey:(NSString *)key {
Lock();
YYKVStorageItem *item = [_kv getItemForKey:key];
Unlock();
...
}
复制代码
初始化方法创建信号量,dispatch_semaphore_create(1),值是1。当线程调用写缓存的方法时,调用dispatch_semaphore_wait方法使信号量-1。同时线程B在读缓存时,由于信号量为0,遇到dispatch_semaphore_wait方法时会被阻塞。直到线程A写完数据时,调用dispatch_semaphore_signal方法时,信号量+1,线程B继续执行,读取数据。关于iOS中各种互斥锁性能的对比。
我们看一些接口设计方面的内容:
#pragma mark - Attribute
///=============================================================================
/// @name Attribute
///=============================================================================
@property (nonatomic, readonly) NSString *path; ///< The path of this storage.
@property (nonatomic, readonly) YYKVStorageType type; ///< The type of this storage.
@property (nonatomic) BOOL errorLogsEnabled; ///< Set `YES` to enable error logs for debug.
#pragma mark - Initializer
///=============================================================================
/// @name Initializer
///=============================================================================
- (instancetype)init UNAVAILABLE_ATTRIBUTE;
+ (instancetype)new UNAVAILABLE_ATTRIBUTE;
/**
The designated initializer.
@param path Full path of a directory in which the storage will write data. If
the directory is not exists, it will try to create one, otherwise it will
read the data in this directory.
@param type The storage type. After first initialized you should not change the
type of the specified path.
@return A new storage object, or nil if an error occurs.
@warning Multiple instances with the same path will make the storage unstable.
*/
- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER;
复制代码
接口中的属性都是很重要的信息,我们应该尽量利用好它的读写属性,尽量设计成只读属性。默认情况下,不是只读的,都很容易让其他开发者认为,该属性是可以设置的。
对于初始化方法而言,如果某个类需要提供一个指定的初始化方法,那么就要使用NS_DESIGNATED_INITIALIZER给予提示。同时使用UNAVAILABLE_ATTRIBUTE禁用掉默认的方法。接下来要重写禁用的初始化方法,在其内部抛出异常:
- (instancetype)init {
@throw [NSException exceptionWithName:@"YYKVStorage init error" reason:@"Please use the designated initializer and pass the 'path' and 'type'." userInfo:nil];
return [self initWithPath:@"" type:YYKVStorageTypeFile];
}
复制代码
上边的代码大家可以直接拿来用,千万不要怕程序抛出异常,在发布之前,能够发现潜在的问题是一件好事。使用了上边的一个小技巧后呢,编码水平是不是有所提升?
再给大家简单分析分析下边一样代码:
- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER; 复制代码
上边我们关心的是nullable关键字,表示可能为空,与之对应的是nonnull,表示不为空。可以说,他们都跟swift有关系,swift中属性或参数是否为空都有严格的要求。因此我们在设计属性,参数,返回值等等的时候,要考虑这些可能为空的情况。
// 设置中间的内容默认都是nonnull NS_ASSUME_NONNULL_BEGIN NS_ASSUME_NONNULL_END 复制代码
总结
YYCache库的分析到此为止,其中有许多代码值得学习。例如二级缓存的思想,LRU的实现,SQLite的WAL机制。文中许多地方的分析和思路,表达的不是很准确和清楚,希望通过今后的学习和练习,提升自己的水平,总之路漫漫其修远兮...
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 理解原型其实是理解原型链
- 要理解深度学习,必须突破常规视角去理解优化
- 深入理解java虚拟机(1) -- 理解HotSpot内存区域
- 荐 【C++100问】深入理解理解顶层const和底层const
- 深入理解 HTTPS
- 深入理解 HTTPS
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
The Mechanics of Web Handling
David R. Roisum
This unique book covers many aspects of web handling for manufacturing, converting, and printing. The book is applicable to any web including paper, film, foil, nonwovens, and textiles. The Mech......一起来看看 《The Mechanics of Web Handling》 这本书的介绍吧!