YYKit 源码分析(8)-YYCache

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数据介绍完毕。

最终花个增删查数据的流程图

增加数据



查数据



删除数据


©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容