YYCache 原始碼學習(二):YYDiskCache
整體思路
從作者的《YYCache 設計思路》一文中可以看出,作者在設計YYDiskCache之前做了充分的測試:iPhone 6 64G 下,SQLite 寫入效能比直接寫檔案要高,但讀取效能取決於資料大小:當單條資料小於 20K 時,資料越小 SQLite 讀取效能越高;單條資料大於 20K 時,直接寫為檔案速度會更快一些。
YYDiskCache的磁碟快取結合使用了檔案儲存和資料庫儲存。
個人理解:在進行磁碟快取的時候,會判斷要儲存資料的大小,如果資料小於20K,則直接存入資料庫(資料儲存到inline_data欄位,此時filename為空)。如果資料大於20K,先把資料以檔案形式進行儲存,然後再在資料庫中儲存對應的檔名(此時inline_data為NULL,filename為檔案地址),具體的可以結合下文中提到的磁碟快取的檔案結構來看。
磁碟快取的核心類是YYKVStorage
,他主要封裝了檔案儲存操作和SQLite資料庫的操作。YYDiskCache
是對YYKVStorage
的封裝,丟擲的API和記憶體快取相似,都有資料讀寫和修剪記憶體。
磁碟快取的檔案結構
/* File: /path/ /manifest.sqlite /manifest.sqlite-shm /manifest.sqlite-wal /data/ /e10adc3949ba59abbe56e057f20f883e /e10adc3949ba59abbe56e057f20f883e /trash/ /unused_file_or_folder SQL: create table if not exists manifest ( keytext, filenametext, sizeinteger, inline_datablob, modification_timeinteger, last_access_timeinteger, extended_datablob, primary key(key) ); create index if not exists last_access_time_idx on manifest(last_access_time); */
這個結構我們不需要多說什麼,只提一個小點,作者在path路徑下面設計了一個/data/和一個/trash/。刪除檔案是一個比較耗時的操作,在刪除檔案的時候,先進行檔案的移動,然後在一個子執行緒中處理要刪掉的檔案,提高了整體的效率。
實現 LRU
磁碟快取對快取淘汰演算法的實現就比較簡單了,因為每次儲存都有對應的資料庫記錄,而且表中設計了last_access_time這個欄位,我們可以直接使用資料庫的排序語句就可以找到最不常用的檔案了。
程式碼分析
1.
- (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語句的控制代碼,可以理解成提前把sql語句編譯成位元組碼留給後面的執行函式(當前不執行)。同時,作者使用
_dbStmtCache
對語句進行快取,下次使用時可以更快度的加載出來。
2.
- (BOOL)_dbClose { if (!_db) return YES; intresult = 0; BOOL retry = NO; BOOL stmtFinalized = NO; if (_dbStmtCache) CFRelease(_dbStmtCache); _dbStmtCache = NULL; do { retry = NO; result = sqlite3_close(_db); // 狀態為busy或者lock if (result == SQLITE_BUSY || result == SQLITE_LOCKED) { if (!stmtFinalized) { stmtFinalized = YES; sqlite3_stmt *stmt; //sqlite3_stmt *sqlite3_next_stmt(sqlite3 *pDb, sqlite3_stmt *pStmt); //表示從資料庫pDb中對應的pStmt語句開始一個個往下找出相應prepared語句,如果pStmt為nil,那麼就從pDb的第一個prepared語句開始。 while ((stmt = sqlite3_next_stmt(_db, nil)) != 0) { //釋放資料庫中的prepared語句資源 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; }
這個是關閉資料庫的方法,_dbStmtCache
中快取了我們使用的控制代碼,所以首先要釋放掉了_dbStmtCache
。
在真正關閉資料庫的程式碼中使用了do-while迴圈,因為一次訪問資料庫並不一定成功,資料庫可能是busy或者lock的狀態,所以要使用一個迴圈來多次訪問。
如果為能關閉資料庫,作者使用了sqlite3_next_stmt
一個個的找出prepared語句,並使用sqlite3_finalize
釋放了prepared資源(防止記憶體洩露)。
其他的就沒什麼好說的了,主要就是一些sql語句的用法,這些大家看一下,碰到陌生的api谷歌一下就有了 ~ 具體的檔案的操作,比較常用,看起來就容易很多。