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數據介紹完畢。

最終花個增刪查數據的流程圖

增加數據



查數據



刪除數據


?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容