YYCache深入學習

邦Ben發表於2018-01-05

深知,原始碼還是一點點讀,加點讀書筆記,才可以深入挖掘,因此還是覺得每次讀原始碼都記錄一番,無論好壞,如有寫錯,請斧正

簡介

YYCahce 是作為 ibireme 大神開源的一個YYkit元件庫中的一部分,YYCache提供了記憶體快取,和永續性的硬碟快取。

一個合理快取應該有的設計

  • 合理的增刪改查介面
  • 快取記憶體,提高常用快取的返回效能和效率
  • 低速快取,磁碟大檔案快取
  • 良好快取限制策略
  • 高效能,執行緒安全

基本設計思路

YYCache 提供對外的整合介面,YYMemoryCache 提供記憶體儲存快取,通過lru演算法進行處理,YYDiskCache提供file和sqlite3的兩種持久化儲存方式

YYMemoryCache

YYMemoryCahce 作為一個記憶體快取,提供快取記憶體,並且因為記憶體有限,需要進行一定的限制

  1. 執行緒安全

    在頻率高的併發資料操作中,必須保證執行緒安全,不然拿到的資料不符合預期,或者導致讀寫衝突 在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 是安全的。

  2. LRU

最近使用優先,也就是認為,最近使用的,最大可能性會再次用到 裡面實現用到一個雙向連結串列的結構進行處理,使用到的就會被移動到表頭 在刪除釋放的時候,就會從隊尾進行刪除釋放

  1. 鍵值對儲存操作

通過建立連結串列節點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
複製程式碼
  1. 限制策略
// 限制條件有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。

  1. 某些小技巧,用於提供效能

會在一段時間,自行檢查是否超過限制策略,超過則迴圈釋放。 YYMemoryCache預設在進入後臺的時候會進行檢查。 因為一次釋放太多,會導致資源消耗過大,因此,通過dispatch_aync block的方式,放到後臺執行緒釋放


YYDiskCache

  1. 執行緒安全

disk中使用dispatch_semaphore訊號量進行處理

dispatch_semaphore 是訊號量,但當訊號總量設為 1 時也可以當作鎖來。在沒有等待情況出現時,它的效能比 pthread_mutex 還要高,但一旦有等待情況出現時,效能就會下降許多。相對於 OSSpinLock 來說,它的優勢在於等待時不會消耗 CPU 資源。對磁碟快取來說,它比較合適。

dispatch_semaphore是屬於閒等待,CPU不會消耗,因此,在做磁碟快取的時候,用時較長,需要等待的話,會比較節省資源

  • OSSpinLock 自旋鎖,不安全,所以使用了dispatch_semaphore進行處理,因為大檔案,需要等待的情況較多
  1. 大檔案處理
  • 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/

相關文章