YYKVStorage
上一篇介绍了操作文件的api,接下来介绍数据库。
- (BOOL)_dbOpen {
if (_db) return YES;
int result = sqlite3_open(_dbPath.UTF8String, &_db);
if (result == SQLITE_OK) {
CFDictionaryKeyCallBacks keyCallbacks = kCFCopyStringDictionaryKeyCallBacks;
CFDictionaryValueCallBacks valueCallbacks = {0};
_dbStmtCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &keyCallbacks, &valueCallbacks);
_dbLastOpenErrorTime = 0;
_dbOpenErrorCount = 0;
return YES;
} else {
_db = NULL;
if (_dbStmtCache) CFRelease(_dbStmtCache);
_dbStmtCache = NULL;
_dbLastOpenErrorTime = CACurrentMediaTime();
_dbOpenErrorCount++;
if (_errorLogsEnabled) {
NSLog(@"%s line:%d sqlite open failed (%d).", __FUNCTION__, __LINE__, result);
}
return NO;
}
}
创建并且打开数据库,介绍过,不做介绍
- (BOOL)_dbClose {
if (!_db) return YES;
int result = 0;
BOOL retry = NO;
BOOL stmtFinalized = NO;
if (_dbStmtCache) CFRelease(_dbStmtCache);
_dbStmtCache = NULL;
do {
retry = NO;
result = sqlite3_close(_db);
if (result == SQLITE_BUSY || result == SQLITE_LOCKED) {
if (!stmtFinalized) {
stmtFinalized = YES;
sqlite3_stmt *stmt;
while ((stmt = sqlite3_next_stmt(_db, nil)) != 0) {
sqlite3_finalize(stmt);
retry = YES;
}
}
} else if (result != SQLITE_OK) {
if (_errorLogsEnabled) {
NSLog(@"%s line:%d sqlite close failed (%d).", __FUNCTION__, __LINE__, result);
}
}
} while (retry);
_db = NULL;
return YES;
}
关闭数据库
- (BOOL)_dbCheck {
if (!_db) {
if (_dbOpenErrorCount < kMaxErrorRetryCount &&
CACurrentMediaTime() - _dbLastOpenErrorTime > kMinRetryTimeInterval) {
return [self _dbOpen] && [self _dbInitialize];
} else {
return NO;
}
}
return YES;
}
检查当前数据库状态
1.检查数据库状态
2.数据库关闭的话,检查打开数据库连续发生的次数,并且打开数据库间隔时间过了一定时间,那么再重新打开数据库并且初始化数据库。
- (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];
}
初始化数据库。
这里有个sqlite 语句。上一篇介绍过了。不过没有仔细介绍wal模式。在这里补充下,因为api的部分与wal模式有关
在3.7.0以后,WAL(Write-Ahead Log)模式可以使用,是另一种实现事务原子性的方法。
WAL的优点
在大多数情况下更快
并行性更高。因为读操作和写操作可以并行。
文件IO更加有序化,串行化(more sequential)
使用fsync()的次数更少,在fsync()调用时好时坏的机器上较为未定。
缺点
一般情况下需要VFS支持共享内存模式。(shared-memory primitives)
操作数据库文件的进程必须在同一台主机上,不能用在网络操作系统。
持有多个数据库文件的数据库连接对于单个数据库时原子的,对于全部数据库是不原子的。
进入WAL模式以后不能修改page的size。
不能打开只读的WAL数据库(Read-Only Databases),这进程必须有"-shm"文件的写权限。
对于只进行读操作,很少进行写操作的数据库,要慢那么1到2个百分点。
会有多余的"-wal"和"-shm"文件
需要开发者注意checkpointing
原理
回滚日志的方法是把为改变的数据库文件内容写入日志里,然后把改变后的内容直接写到数据库文件中去。在系统crash或掉电的情况下,日志里的内容被重新写入数据库文件中。日志文件被删除,标志commit着一次commit的结束。
WAL模式于此此相反。原始为改变的数据库内容在数据库文件中,对数据库文件的修改被追加到单独的WAL文件中。当一条记录被追加到WAL文件后,标志着一次commit的结束。因此一次commit不必对数据库文件进行操作,当正在进行写操作时,可以同时进行读操作。多个事务的内容可以追加到一个WAL文件的末尾。
checkpoint
最后WAL文件的内容必须更新到数据库文件中。把WAL文件的内容更新到数据库文件的过程叫做一次checkpoint。
回滚日志的方法有两种操作:读和写。WAL有三种操作,读、写和checkpoint。
默认的,SQL会在WAL文件达到1000page时进行一次checkpoint。进行WAL的时机也可以由应用程序自己决定。
并发性
当一个读操作发生在WAL模式的数据库上时,会首先找到WAL文件中最后一次提交,叫做"end mark"。每一个事务可以有自己的"end point",但对于一个给定额事务来说,end mark是固定的。
当读取数据库中的page时,SQLite会先从WAL文件中寻找有没有对应的page,从找出离end mark最近的那一条记录;如果找不到,那么就从数据库文件中寻找对一个的page。为了避免每次事务都要扫描一遍WAL文件,SQLite在共享内存中维护了一个"wal-index"的数据结构,帮助快速定位page。
写数据库只是把新内容加到WAL文件的末尾,和读操作没有关系。由于只有一个WAL文件,因此同时只能有一个写操作。
checkpoint操作可以和读操作并行。但是如果checkpoint把一个page写入数据库文件,而且这个page超过了当前读操作的end mark时,checkpoint必须停止。否则会把当前正在读的部分覆盖掉。下次checkpoint时,会从这个page开始往数据库中拷贝数据。
当写操作时,会检查WAL文件被拷贝到数据库的进度。如果已经完全被拷贝到数据库文件中,已经同步,并且没有读操作在使用WAL文件,那么会把WAL文件清空,从其实开始追加数据。保证WAL文件不会无限制增长。
性能
写操作是很快的,因为只需要进行一次写操作,并且是顺序的(不是随机的,每次都写到末尾)。而且,把数据刷到磁盘上是不必须的。(如果PRAGMA synchronous是FULL,每次commit要刷一次,否则不刷。)
读操作的性能有所下降,因为需要从WAL文件中查找内容,花费的时间和WAL文件的大小有关。wal-index可以缩短这个时间,但是也不能完全避免。因此需要保证WAL文件的不会太大。
为了保护数据库不被损坏,需要在把WAL文件写入数据库之前把WAL文件刷入磁盘;在重置WAL文件之前要把数据库内容刷入数据库文件。此外checkpoint需要查找操作。这些因素使得checkpoint比写操作慢一些。
默认策略是很多线程可以增长WAL文件。把WAL文件大小变得比1000page大的那个线程要负责进行checkpoint。会导致绝大部分读写操作都是很快的,随机有一个写操作非常慢。也可以禁用自动checkpoint的策略,定期在一个线程或进程中进行checkpoint操作。
高效的写操作希望WAL文件越大越好;高效的读操作希望WAL文件越小越好。两者存在一个tradeoff。
激活和配置WAL模式
PRAGMA journal_mode=WAL;,如果成功,会返回"wal"。
自动checkpoint
可以手动checkpoint
sqlite3_wal_checkpoint(sqlite3*db, const char *zDb)
配置checkpoint
sqlite3_wal_autocheckpoint(sqlite3 *db,intN);
Application-Initiated Checkpoints
可以在任意一个可以进行写操作的数据库连接中调用sqlite3_wal_checkpoint_v2()或sqlite3_wal_checkpoint()。
WAL模式的持久性
当一个进程设置了WAL模式,关闭这个进程,重新打开这个数据库,仍然是WAL模式。
如果在一个数据库连接中设置了WAL模式,那么这个数据库的所有连接都将被设为WAL模式。
只读数据库
如果数据库需要恢复,而你只有读权限,没有写权限,那么你不能读取这个数据库,因为进行读操作的第一步就是恢复数据库。
类似的,因为WAL模式下的数据库进行读操作时,需要类似数据库恢复的操作,因此如果只有读权限,也不能对打开数据库。
WAL的实现需要有一个基于WAL文件的哈希表在共享内存中。在Unix和Windows的VFS实现中,是基于MMap的。将共享内存映射到同目录下的"-shm"文件中。因此即使是对WAL模式下的数据库文件进行读操作,也需要写权限。
为了把数据库文件转化为只读的文件,需要先把这个数据库的日志模式改为"delete".
避免过大的WAL文件
WAL-index的共享内存实现
在WAL发布之前,曾经尝试过将wal-index映射到临时目录,如/dev/shm或/tmp。但是不同的用户看到的目录是不同的,所以此路不通。
后来尝试将wal-index映射到匿名的虚拟内存块中,但是无法在不用的Unix版本中保持一致。
最终决定采用将wal-index映射到同目录下。这样子会导致不必要的磁盘IO。但是问题不大,是因为wal-index很少超过32k,而且从不会调用sync操作。此外,最后一个数据库连接关闭以后,这个文件会被删除。
如果这个数据库只会被一个进程使用,那么可以使用heap memory而不是共享内存。
不用共享内存实现WAL
在3.7.4版本以后,只要SQLite的lock mode被设为EXCLUSIVE,那么即使共享内存不支持,也可以使用WAL模式。
换句话说,如果只有一个进程使用SQLite,那么不用共享内存也可以使用WAL。
此时,将lock mode改为normal是无效的,需要实现取消WAL模式。
采用wal模式,我们就知道了,在数据的读写的时候不是直接写入数据库的。而是先写入wal模式文件下,wal相当于外界和数据库的连接。不探讨为啥采用这用模式的好处。咱只是分析使用就行了。
- (void)_dbCheckpoint {
if (![self _dbCheck]) return;
// Cause a checkpoint to occur, merge `sqlite-wal` file to `sqlite` file.
sqlite3_wal_checkpoint(_db, NULL);
}
这里我们手动checkpoint ,因为是wal模式,因此需要checkPoint。前提是检查数据库是否打开。
- (BOOL)_dbExecute:(NSString *)sql {
if (sql.length == 0) return NO;
if (![self _dbCheck]) return NO;
char *error = NULL;
int result = sqlite3_exec(_db, sql.UTF8String, NULL, NULL, &error);
if (error) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite exec error (%d): %s", __FUNCTION__, __LINE__, result, error);
sqlite3_free(error);
}
return result == SQLITE_OK;
}
执行一条sql 语句。不过每次执行语句都要check下数据库是否打开。
- (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql {
if (![self _dbCheck] || sql.length == 0 || !_dbStmtCache) return NULL;
sqlite3_stmt *stmt = (sqlite3_stmt *)CFDictionaryGetValue(_dbStmtCache, (__bridge const void *)(sql));
if (!stmt) {
int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL);
if (result != SQLITE_OK) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NULL;
}
CFDictionarySetValue(_dbStmtCache, (__bridge const void *)(sql), stmt);
} else {
sqlite3_reset(stmt);
}
return stmt;
}
这里其实就是讲sql语句字符串转换成sqlite3_stmt (相当于一条sqlite 语句),并且将这条语句保存到缓存中。
- (NSString *)_dbJoinedKeys:(NSArray *)keys {
NSMutableString *string = [NSMutableString new];
for (NSUInteger i = 0,max = keys.count; i < max; i++) {
[string appendString:@"?"];
if (i + 1 != max) {
[string appendString:@","];
}
}
return string;
}
根据数组中的数据获取字符串,格式是“?,?,?,?”
- (void)_dbBindJoinedKeys:(NSArray *)keys stmt:(sqlite3_stmt *)stmt fromIndex:(int)index{
for (int i = 0, max = (int)keys.count; i < max; i++) {
NSString *key = keys[i];
sqlite3_bind_text(stmt, index + i, key.UTF8String, -1, NULL);
}
}
stmt代表sqlite语句,将语句替换的部分绑定响应的key
- (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_text(stmt, 1, key.UTF8String, -1, NULL);
sqlite3_bind_text(stmt, 2, fileName.UTF8String, -1, NULL);
sqlite3_bind_int(stmt, 3, (int)value.length);
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);
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;
}
函数的作用是保存数据到数据库表 manifest 。
1获取sqlite语句
2.将数据绑定sqlite语句
3.保存
其实很简单的,这里不过有个逻辑是,当有文件名的时候,我们不用保存,value值
- (BOOL)_dbUpdateAccessTimeWithKey:(NSString *)key {
NSString *sql = @"update manifest set last_access_time = ?1 where key = ?2;";
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
if (!stmt) return NO;
sqlite3_bind_int(stmt, 1, (int)time(NULL));
sqlite3_bind_text(stmt, 2, key.UTF8String, -1, NULL);
int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite update error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
return YES;
}
更新一条语句的last_access_time 时间
- (BOOL)_dbUpdateAccessTimeWithKeys:(NSArray *)keys {
if (![self _dbCheck]) return NO;
int t = (int)time(NULL);
NSString *sql = [NSString stringWithFormat:@"update manifest set last_access_time = %d where key in (%@);", t, [self _dbJoinedKeys:keys]];
sqlite3_stmt *stmt = NULL;
int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL);
if (result != SQLITE_OK) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
[self _dbBindJoinedKeys:keys stmt:stmt fromIndex:1];
result = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite update error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
return YES;
}
1.检查是否打开数据库
2.获取sql语句,假设keys中有两个数据 sql语句的结果是update manifest set last_access_time = %d where key in (?,?);
3.获取sqlite语句sqlite3_stmt
4.绑定sqlite 语句与值。
5.执行sqlite 语句。
6.销毁语句
- (BOOL)_dbDeleteItemWithKey:(NSString *)key {
NSString *sql = @"delete from manifest where key = ?1;";
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
if (!stmt) return NO;
sqlite3_bind_text(stmt, 1, key.UTF8String, -1, NULL);
int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d db delete error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
return YES;
}
删除key对应的sqlite的一条数据
- (BOOL)_dbDeleteItemWithKeys:(NSArray *)keys {
if (![self _dbCheck]) return NO;
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);
if (result != SQLITE_OK) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
[self _dbBindJoinedKeys:keys stmt:stmt fromIndex:1];
result = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (result == SQLITE_ERROR) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite delete error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
return YES;
}
这是删除数组中对应key的sqlite 数据
1检查数据库
2.获取sqlite 语句
3绑定语句,
4执行语句
5释放语句。
其实发现只要传入的函数是数组,就采用这种模式。作者把没条语句封装到函数中了。代码有重复,其实可以用block块来封装下。
- (BOOL)_dbDeleteItemsWithSizeLargerThan:(int)size {
NSString *sql = @"delete from manifest where size > ?1;";
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
if (!stmt) return NO;
sqlite3_bind_int(stmt, 1, size);
int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite delete error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
return YES;
}
删除超过指定大小的数据
- (BOOL)_dbDeleteItemsWithTimeEarlierThan:(int)time {
NSString *sql = @"delete from manifest where last_access_time < ?1;";
sqlite3_stmt *stmt = [self _dbPrepareStmt:sql];
if (!stmt) return NO;
sqlite3_bind_int(stmt, 1, time);
int result = sqlite3_step(stmt);
if (result != SQLITE_DONE) {
if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite delete error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
return NO;
}
return YES;
}
删除超过指定时间的数据
- (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++);
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 *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;
}
sqlite3_stmt 中保存的一条数据,这个只是将数据提取出来
- (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) {
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;
}
查询数据,将数据保存到YYKVStorageItem 中
- (NSMutableArray *)_dbGetItemWithKeys:(NSArray *)keys excludeInlineData:(BOOL)excludeInlineData
获取keys 所对应的数据
- (NSData *)_dbGetValueWithKey:(NSString *)key
获取key 对应的一条数据中的inline_data 数据
- (NSString *)_dbGetFilenameWithKey:(NSString *)key
获取key对应的fileName
- (NSMutableArray *)_dbGetFilenameWithKeys:(NSArray *)keys
获取keys 数据对应的fileName数组
- (NSMutableArray *)_dbGetFilenamesWithSizeLargerThan:(int)size
获取超过某一大小所有文件
- (NSMutableArray *)_dbGetFilenamesWithTimeEarlierThan:(int)time
获取超过某一个时间的所有文件
- (NSMutableArray *)_dbGetItemSizeInfoOrderByTimeAscWithLimit:(int)count
排序,超过某个时间排序
- (int)_dbGetItemCountWithKey:(NSString *)key
获取某个key的条数,因为key是主键,所以这里查询只有0条或者1条
- (int)_dbGetTotalItemSize
获取所有文件累加大小
- (int)_dbGetTotalItemCount
获取数据库有多少条数据
该类操作数据库或者文件都是在同步操作。
唯一异步操作的地方是删除所有文件。
这里数据库是wal模式
YYDiskCache
我们看看硬盘上Disk的结构
我们看看初始化
- (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];
}
默认初始化,抛出异常。必须要有path
- (instancetype)initWithPath:(NSString *)path {
return [self initWithPath:path inlineThreshold:1024 * 20]; // 20KB
}
- (instancetype)initWithPath:(NSString *)path
inlineThreshold:(NSUInteger)threshold {
self = [super init];
if (!self) return nil;
YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
if (globalCache) return globalCache;
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];
_YYDiskCacheSetGlobal(self);
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appWillBeTerminated) name:UIApplicationWillTerminateNotification object:nil];
return self;
}
我们看初始化,参数threshold 默认是1024*20
1.调用_YYDiskCacheGetGlobal 方法检查是否含有YYDiskCache ,有就返回
2.根据参数threshold 区分当前YYKVStorageType 类型
3.初始化YYKVStorage 对象
4.self 保存 YYKVStorage ,path
5.初始化锁,queue,等相关参数
6.调用-_trimRecursively 函数,这个和memory 一样的实现
7.调用_YYDiskCacheSetGlobal 方法
8.增加通知
YYKVStorage 咋说
这个看简单,这里面主要有个_YYDiskCacheSetGlobal 方法和_YYDiskCacheGetGlobal 方法。我们看看
/// weak reference for all instances
static NSMapTable *_globalInstances;
static dispatch_semaphore_t _globalInstancesLock;
static void _YYDiskCacheInitGlobal() {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_globalInstancesLock = dispatch_semaphore_create(1);
_globalInstances = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0];
});
}
static YYDiskCache *_YYDiskCacheGetGlobal(NSString *path) {
if (path.length == 0) return nil;
_YYDiskCacheInitGlobal();
dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
id cache = [_globalInstances objectForKey:path];
dispatch_semaphore_signal(_globalInstancesLock);
return cache;
}
static void _YYDiskCacheSetGlobal(YYDiskCache *cache) {
if (cache.path.length == 0) return;
_YYDiskCacheInitGlobal();
dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
[_globalInstances setObject:cache forKey:cache.path];
dispatch_semaphore_signal(_globalInstancesLock);
}
1.从这一段来看,我们知道,磁盘缓存是个单例,全局的。
2.初始化信号量和NSMapTable
3.NSMapTable 保存key是path ,而值是 YYDiskCache
- (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];
});
}
这里也是递归,默认是5s
- (void)_trimInBackground {
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
if (!self) return;
Lock();
[self _trimToCost:self.costLimit];
[self _trimToCount:self.countLimit];
[self _trimToAge:self.ageLimit];
[self _trimToFreeDiskSpace:self.freeDiskSpaceLimit];
Unlock();
});
}
这里是每间隔5s自动计算数据,这里和YYMemoryCache 一样,这里新增加一种5秒检测- (void)_trimToFreeDiskSpace:(NSUInteger)targetFreeDiskSpace ,其他的实现具体不做讲解。
- (void)_trimToFreeDiskSpace:(NSUInteger)targetFreeDiskSpace {
if (targetFreeDiskSpace == 0) return;
int64_t totalBytes = [_kv getItemsSize];
if (totalBytes <= 0) return;
int64_t diskFreeBytes = _YYDiskSpaceFree();
if (diskFreeBytes < 0) return;
int64_t needTrimBytes = targetFreeDiskSpace - diskFreeBytes;
if (needTrimBytes <= 0) return;
int64_t costLimit = totalBytes - needTrimBytes;
if (costLimit < 0) costLimit = 0;
[self _trimToCost:(int)costLimit];
}
这个函数其实就是让磁盘空间最少保留targetFreeDiskSpace 大小,要是检测的磁盘剩余空间不足targetFreeDiskSpace大小,那么就删除所有本地文件。
我们看看该类的增删改查,我们知道往磁盘写文件最好都是异步写入这样不会阻碍当前线程。因此这里的增删改查都有异步和同步两种写法,自由选择。
.增-同步方法
- (void)setObject:(id)object forKey:(NSString *)key {
if (!key) return;
if (!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();
}
1 检测key 和object ,无效就删除指定的key对应数据
2.这里有个关联对象,检测改对象是否绑定一个数据
3.要是可以外界传入archive方法,那就用外界方法archive。否则自己archive
4.要是_kv.type 不是插入数据库类型,并且大于指定大小,那么就生成一个文件名字
5.将数据插入到数据库中
不过这里直接设置是同步的。在当前线程操作,文件小是可以这样执行的。
这里看看如何生成文件名字的
- (NSString *)_filenameForKey:(NSString *)key {
NSString *filename = nil;
if (_customFileNameBlock) filename = _customFileNameBlock(key);
if (!filename) filename = key.md5String;
return filename;
}
这里就是把文件名字md5下
+ (void)setExtendedData:(NSData *)extendedData toObject:(id)object {
if (!object) return;
objc_setAssociatedObject(object, &extended_data_key, extendedData, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
给数据绑定一个关联数据
.增-异步方法
- (void)setObject:(id)object forKey:(NSString *)key withBlock:(void(^)(void))block {
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
[self setObject:object forKey:key];
if (block) block();
});
}
只是将同步方法放入了一个指定的队列中执行,这个队列是串行队列。执行完毕后,有个回调方法而已。
.删-同步方法
- (void)removeObjectForKey:(NSString *)key {
if (!key) return;
Lock();
[_kv removeItemForKey:key];
Unlock();
}
删除key对应的数据
- (void)removeAllObjects {
Lock();
[_kv removeAllItems];
Unlock();
}
删除所有数据
- (void)trimToCount:(NSUInteger)count {
Lock();
[self _trimToCount:count];
Unlock();
}
删除到指定条数
- (void)trimToCost:(NSUInteger)cost {
Lock();
[self _trimToCost:cost];
Unlock();
}
删除到指定大小
- (void)trimToAge:(NSTimeInterval)age {
Lock();
[self _trimToAge:age];
Unlock();
}
删除到指定时间
.删-异步方法
- (void)removeObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key))block {
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
[self removeObjectForKey:key];
if (block) block(key);
});
}
只是将删除操作放入异步线程中
- (void)removeAllObjectsWithBlock:(void(^)(void))block {
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
[self removeAllObjects];
if (block) block();
});
}
异步删除所有数据
- (void)removeAllObjectsWithProgressBlock:(void(^)(int removedCount, int totalCount))progress
endBlock:(void(^)(BOOL error))end {
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
if (!self) {
if (end) end(YES);
return;
}
Lock();
[_kv removeAllItemsWithProgressBlock:progress endBlock:end];
Unlock();
});
}
异步删除数据,不过有删除数据的进度
- (void)trimToCount:(NSUInteger)count withBlock:(void(^)(void))block {
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
[self trimToCount:count];
if (block) block();
});
}
异步删除到指定条数
- (void)trimToCost:(NSUInteger)cost withBlock:(void(^)(void))block {
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
[self trimToCost:cost];
if (block) block();
});
}
异步删除到指定大小
- (void)trimToAge:(NSTimeInterval)age withBlock:(void(^)(void))block {
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
[self trimToAge:age];
if (block) block();
});
}
异步删除到指定时间
.查-同步
- (NSInteger)totalCount {
Lock();
int count = [_kv getItemsCount];
Unlock();
return count;
}
获取总条数
- (NSInteger)totalCost {
Lock();
int count = [_kv getItemsSize];
Unlock();
return count;
}
获取所有文件大小
- (BOOL)containsObjectForKey:(NSString *)key {
if (!key) return NO;
Lock();
BOOL contains = [_kv itemExistsForKey:key];
Unlock();
return contains;
}
检查key 是否有磁盘数据
- (id)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;
}
获取key 对应的数据
1.从数据库获取key指定的数据
2.检查数据是否需要外界arcieve还是内部arcieve
3.要是有延展数据,那么将该数据关联绑定到对象上
.查-异步
- (void)totalCountWithBlock:(void(^)(NSInteger totalCount))block {
if (!block) return;
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
NSInteger totalCount = [self totalCount];
block(totalCount);
});
}
异步获取总条数
- (void)totalCostWithBlock:(void(^)(NSInteger totalCost))block {
if (!block) return;
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
NSInteger totalCost = [self totalCost];
block(totalCost);
});
}
异步获取所有文件大小
+ (NSData *)getExtendedDataFromObject:(id)object {
if (!object) return nil;
return (NSData *)objc_getAssociatedObject(object, &extended_data_key);
}
获取对象关联的指定数据,对应的sqlite中的最后的一个字段
- (void)objectForKey:(NSString *)key withBlock:(void(^)(NSString *key, idobject))block { if (!block) return; __weak typeof(self) _self = self; dispatch_async(_queue, ^{ __strong typeof(_self) self = _self; id object = [self objectForKey:key];
block(key, object);
});
}
异步方法获取对象
- (void)containsObjectForKey:(NSString *)key withBlock:(void(^)(NSString *key, BOOL contains))block {
if (!block) return;
__weak typeof(self) _self = self;
dispatch_async(_queue, ^{
__strong typeof(_self) self = _self;
BOOL contains = [self containsObjectForKey:key];
block(key, contains);
});
}
异步方法获取数据
其他方法
- (NSString *)description {
if (_name) return [NSString stringWithFormat:@"<%@: %p> (%@:%@)", self.class, self, _name, _path];
else return [NSString stringWithFormat:@"<%@: %p> (%@)", self.class, self, _path];
}
重写description方法
- (BOOL)errorLogsEnabled {
Lock();
BOOL enabled = _kv.errorLogsEnabled;
Unlock();
return enabled;
}
错误日志是否打开
- (void)setErrorLogsEnabled:(BOOL)errorLogsEnabled {
Lock();
_kv.errorLogsEnabled = errorLogsEnabled;
Unlock();
}
设置错误日志是否打开
到这里yyCache数据介绍完毕。
最终花个增删查数据的流程图
增加数据
查数据
删除数据