深知,原始碼還是一點點讀,加點讀書筆記,才可以深入挖掘,因此還是覺得每次讀原始碼都記錄一番,無論好壞,如有寫錯,請斧正
簡介
YYCahce 是作為 ibireme 大神開源的一個YYkit元件庫中的一部分,YYCache提供了記憶體快取,和永續性的硬碟快取。
一個合理快取應該有的設計
- 合理的增刪改查介面
- 快取記憶體,提高常用快取的返回效能和效率
- 低速快取,磁碟大檔案快取
- 良好快取限制策略
- 高效能,執行緒安全
基本設計思路
YYCache 提供對外的整合介面,YYMemoryCache 提供記憶體儲存快取,通過lru演算法進行處理,YYDiskCache提供file和sqlite3的兩種持久化儲存方式
YYMemoryCache
YYMemoryCahce 作為一個記憶體快取,提供快取記憶體,並且因為記憶體有限,需要進行一定的限制
-
執行緒安全
在頻率高的併發資料操作中,必須保證執行緒安全,不然拿到的資料不符合預期,或者導致讀寫衝突 在YYMemoryCache中,採用的pthread_mutex加鎖
pthread_mutex 定義了一組跨平臺的執行緒相關的 API,pthread_mutex 表示互斥鎖。互斥鎖的實現原理與訊號量非常相似,不是使用忙等,而是阻塞執行緒並睡眠,需要進行上下文切換。
// 主要使用api pthread_mutex_lock(&mutex); // 申請鎖 // 臨界區 pthread_mutex_unlock(&mutex); // 釋放鎖 複製程式碼
pthread_mutex支援遞迴鎖,遞迴鎖,就是在內部加鎖呼叫的時候,遞迴了自身,這樣子,會導致死鎖。
-
pthread_mutex則支援遞迴處理 attr PTHREAD_MUTEX_RECURSIVE
-
memory中使用pthread,在迴圈釋放的時候,通過pthread_mutex_trylock實現簡單的自旋鎖
原文:OSSpinLock 和 dispatch_semaphore 都不會產生特別明顯的死鎖,所以我也無法確定用 dispatch_semaphore 代替 OSSpinLock 是否正確。能夠肯定的是,用 pthread_mutex 是安全的。
-
-
LRU
最近使用優先,也就是認為,最近使用的,最大可能性會再次用到 裡面實現用到一個雙向連結串列的結構進行處理,使用到的就會被移動到表頭 在刪除釋放的時候,就會從隊尾進行刪除釋放
- 鍵值對儲存操作
通過建立連結串列節點node,進行間接運算元據
#pragma mark - _YYLinkedMapNode
@interface _YYLinkedMapNode : NSObject {
// 類似C中的private_extern,使用@private的話,限制太大,@package在類的映象外進行引用就會報錯
// 使用@public @protect等的話,就沒什麼限制的
// 目的是,限制在本檔案中使用
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // 通過dic進行持有
__unsafe_unretained _YYLinkedMapNode *_next; // 通過dic進行持有
id _key;
id _value;
NSUInteger _cost;
NSTimeInterval _time;
}
@end
複製程式碼
- 限制策略
// 限制條件有3個,age cost count ,一般情況下預設都不做限制
/// 個數限制
@property NSUInteger countLimit;
/// 過期時間
@property NSTimeInterval ageLimit;
/// 儲存消耗限制,setObject的時候時候把記憶體大小的存進去
@property NSUInteger costLimit;
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground; // 進入後臺檢查限制刪除快取,預設YES
@property BOOL shouldRemoveAllObjectsOnMemoryWarning; // 收到記憶體警告的時候
複製程式碼
MemoryCache中使用的LRU的的演算法進行刪除快取,當超過一定的限制的時候,會進行迴圈清理,也就是說,memory裡面的東西並不是存進去就會在app的生命週期中一直存在的,可能會被釋放掉。因此外部在使用的時候,需要判空處理的,如果為nil,則認為快取已經失效,需要重新更新。
因此,YYMemoryCahce並不是資料在app的生命週期會被一直保留的,所以,在使用YYCahce最外層介面的時候,YYCache是會通過YYMemoryCache提供快取記憶體,同時存入到低速快取中。
獲取的時候,YYMemoryCache獲取不到,則會詢問YYDiskCache。
- 某些小技巧,用於提供效能
會在一段時間,自行檢查是否超過限制策略,超過則迴圈釋放。 YYMemoryCache預設在進入後臺的時候會進行檢查。 因為一次釋放太多,會導致資源消耗過大,因此,通過dispatch_aync block的方式,放到後臺執行緒釋放
YYDiskCache
- 執行緒安全
disk中使用dispatch_semaphore訊號量進行處理
dispatch_semaphore 是訊號量,但當訊號總量設為 1 時也可以當作鎖來。在沒有等待情況出現時,它的效能比 pthread_mutex 還要高,但一旦有等待情況出現時,效能就會下降許多。相對於 OSSpinLock 來說,它的優勢在於等待時不會消耗 CPU 資源。對磁碟快取來說,它比較合適。
dispatch_semaphore是屬於閒等待,CPU不會消耗,因此,在做磁碟快取的時候,用時較長,需要等待的話,會比較節省資源
- OSSpinLock 自旋鎖,不安全,所以使用了dispatch_semaphore進行處理,因為大檔案,需要等待的情況較多
- 大檔案處理
-
YYKVStorage 通過key value的一個封裝,實現增刪改查
-
YYKVStorage 實現細節:
- 提供對應的sqlite3的db增刪改查的介面,在操作的過程都做了防禦式的操作
- (BOOL)_dbClose { // 關閉資料庫 if (!_db) { return YES; } int result = 0; BOOL retry = NO; BOOL stmtFinalized = NO; if (_dbStmtCache) { _dbStmtCache = NULL; } do { retry = NO; result = sqlite3_close(_db); if (result == SQLITE_BUSY || result == SQLITE_LOCKED) { // 如果sqlite被佔用,那進行stmt的析構操作,讓其釋放資源 // 在重新試一次 if (!stmtFinalized) { stmtFinalized = YES; //析構 sqlite3_stmt *stmt; // sqlite3_stmt 是一種輔助資料結構,用於操作二進位制資料 // 迴圈釋放所有的sqlite stmt while ((stmt = sqlite3_next_stmt(_db, nil))) { sqlite3_finalize(stmt); //析構 所有的sqlite stmt的輔助 retry = YES; // 然後重試, } } } else if (result != SQLITE_OK) { // 關閉失敗 // 輸出 錯誤日誌 NSLog(@"關閉失敗"); } } 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; // 正常 } 複製程式碼
- 對sqlite的stmt也做了快取,加速其效能
// 獲取快取中的stmt,用sql做key - (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) { // 如果stmt為空的話 int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL); if (result != SQLITE_OK) { // 如果不成功 NSLog(@"建立stmt失敗"); return NULL; // 返回空 } CFDictionarySetValue(_dbStmtCache, sql.UTF8String, stmt); // 快取起來,都是同一個stmt,每次建立的話,會大量消耗資源,因此這裡做了快取 } else { sqlite3_reset(stmt); } return stmt; } 複製程式碼
-
儲存方式有三個模式
- 檔案
- sqlite3
- 混合 // 混合的情況有個閾值,大於閾值,則存檔案
-
db sqlite3的使用細節
- sqlite3_stmt 運算元據的輔助資料介面,用於執行sql,並且返回結果
- stmt 也進行做了快取,因為這個sql會重複不定期使用
-
優化: 可以升級最新版本的sqlite3,以此提高效率
YYCache
提供了增刪改查的API,底下呼叫的YYMemoryCache和YYDiskCache,封裝了一些基本邏輯。在save的時候,會儲存到YYMemoryCache和YYDiskCache,讀取的時候,會優先讀取YYMemoryCache實現快取記憶體,再讀取低速快取。
參考
實際上在看的時候,下面這位大佬寫得更加清晰的,之所以我按自己的理解再寫一次,也是為了讓自己更好的研究,根據所有學習技巧來說,最重要的還是應用,因此有了這篇文章,希望之後能逐漸寫出一些更好blog
從 YYCache 原始碼 Get 到如何設計一個優秀的快取 - https://juejin.im/post/59f6e3b051882534af253d4a
ibireme blog - http://blog.ibireme.com/category/tec/ios-tec/