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數據介紹完畢。
最終花個增刪查數據的流程圖
增加數據
查數據
刪除數據