前言
日常的iOS開發(fā)過程中,經(jīng)常會用到緩存,但是什么樣的緩存才能被叫做優(yōu)秀的緩存属划,或者說優(yōu)秀的緩存應(yīng)該具備哪些特質(zhì)?YYCache我認為是一個比較優(yōu)秀的緩存候生,代碼邏輯清晰同眯,注釋詳盡,加上自身不算太大的代碼量使得其閱讀非常簡單唯鸭,更可貴的是它的性能還很高须蜗。
YYCache簡介
我們先來簡單看一下 YYCache 的代碼結(jié)構(gòu),YYCache 是由 YYMemoryCache 與 YYDiskCache 兩部分組成的,其中 YYMemoryCache 作為高速內(nèi)存緩存明肮,而 YYDiskCache 則作為低速磁盤緩存菱农。
通常一個緩存是由內(nèi)存緩存和磁盤緩存組成,內(nèi)存緩存提供容量小但高速的存取功能晤愧,磁盤緩存提供大容量但低速的持久化存儲大莫。
@interface YYCache : NSObject
/** 緩存名稱 */
@property (copy, readonly) NSString *name;
/** memoryCache*/
@property (strong, readonly) YYMemoryCache *memoryCache;
/** diskCache*/
@property (strong, readonly) YYDiskCache *diskCache;
/**判斷key是否存在*/
- (BOOL)containsObjectForKey:(NSString *)key;
/**判斷key是否存在,并執(zhí)行block*/
- (void)containsObjectForKey:(NSString *)key withBlock:(nullable?void(^)(NSString *key,?BOOL?contains))block;
/**獲取key值對應(yīng)的對象 會阻塞調(diào)用的進程*/
- (nullable id)objectForKey:(NSString *)key;
/** 獲取key值對應(yīng)的對象官份,并執(zhí)行block*/
- (void)objectForKey:(NSString *)key withBlock:(nullable?void(^)(NSString *key, id object))block;
/** 對某個key設(shè)置對象只厘,阻塞線程*/
- (void)setObject:(nullable id)object forKey:(NSString *)key;
/** 設(shè)置key的對象,線程會立即返回舅巷,設(shè)置成功后回調(diào)block*/
- (void)setObject:(nullable id)object forKey:(NSString *)key withBlock:(nullable?void(^)(void))block;
/**刪除key對應(yīng)的對象 阻塞線程 */
- (void)removeObjectForKey:(NSString *)key;
/**刪除key對應(yīng)的object 線程會立即返回羔味,刪除成功后回調(diào)block*/
- (void)removeObjectForKey:(NSString *)key withBlock:(nullable?void(^)(NSString *key))block;
/**清空緩存*/
- (void)removeAllObjects;
/** 清空緩存, 線程會立即返回钠右,清空成功后回調(diào)block */
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
/**清空緩存赋元, 線程會立即返回,后臺線程執(zhí)行block*/
- (void)removeAllObjectsWithProgressBlock:(nullable?void(^)(int?removedCount,?int?totalCount))progress ?endBlock:(nullable?void(^)(BOOL?error))end;
上邊整理了幾個常用的方法飒房,做了簡單的中文注釋搁凸,從代碼中我們可以看到 YYCache 中持有 YYMemoryCache 與 YYDiskCache,并且對外提供了一些接口狠毯。這些接口基本都是基于 Key 和 Value 設(shè)計的护糖,類似于 iOS 原生的字典類接口(增刪改查)
YYMemoryCache
YYMemoryCache 是一個高速的內(nèi)存緩存,用于存儲鍵值對嚼松。它與 NSDictionary 相反嫡良,Key 被保留并且不復(fù)制。API 和性能類似于 NSCache献酗,所有方法都是線程安全的寝受。
YYMemoryCache 使用 LRU(least-recently-used) 算法來驅(qū)逐對象。介紹一下LRU:
LRU(Least?recently?used罕偎,最近最少使用)算法根據(jù)數(shù)據(jù)的歷史訪問記錄來進行淘汰數(shù)據(jù)很澄,其核心思想是“如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也更高”颜及。最常見的實現(xiàn)是使用一個鏈表保存緩存數(shù)據(jù)痴怨,詳細算法實現(xiàn)如下:
? ? ? ? ? ?1. 新數(shù)據(jù)插入到鏈表頭部;
? ? ? ? ? ?2.?每當緩存命中(即緩存數(shù)據(jù)被訪問)器予,則將數(shù)據(jù)移到鏈表頭部;
? ? ? ? ? ?3.?當鏈表滿的時候捐迫,將鏈表尾部的數(shù)據(jù)丟棄乾翔。
? ? ?分析
? ? ? ?【命中率】
? ? ? ? ? ? 當存在熱點數(shù)據(jù)時,LRU的效率很好,但偶發(fā)性的反浓、周期性的批量操作會導(dǎo)致LRU命中率急劇下降萌丈,緩存污染情況比較嚴重。
? ? ? ?【復(fù)雜度】
? ? ? ? ? ? 實現(xiàn)簡單雷则。
? ? ? 【代價】?
? ? ? ? ? ? 命中時需要遍歷鏈表辆雾,找到命中的數(shù)據(jù)塊索引,然后需要將數(shù)據(jù)移到頭部月劈。
YYMemoryCache是線程安全的
@implementation YYMemoryCache {
????pthread_mutex_t _lock; // 線程鎖度迂,旨在保證 YYMemoryCache 線程安全
????_YYLinkedMap *_lru; // _YYLinkedMap,YYMemoryCache 通過它間接操作緩存對象
????dispatch_queue_t _queue; // 串行隊列猜揪,用于 YYMemoryCache 的 trim 操作
}
? 沒錯惭墓,YYMemoryCache使用?pthread_mutex線程鎖來確保線程安全。最初YYMemoryCache 這里使用的鎖是?OSSpinLock?自旋鎖而姐,后面有人在 Github 向作者提?issue?反饋?OSSpinLock?不安全腊凶,經(jīng)過作者的確認(詳見?不再安全的 OSSpinLock)最后選擇用?pthread_mutex?替代?OSSpinLock。
具體來說拴念,如果一個低優(yōu)先級的線程獲得鎖并訪問共享資源钧萍,這時一個高優(yōu)先級的線程也嘗試獲得這個鎖,它會處于 spin lock 的忙等狀態(tài)從而占用大量 CPU政鼠。此時低優(yōu)先級線程無法與高優(yōu)先級線程爭奪 CPU 時間风瘦,從而導(dǎo)致任務(wù)遲遲完不成、無法釋放 lock缔俄。這并不只是理論上的問題弛秋,libobjc 已經(jīng)遇到了很多次這個問題了,于是蘋果的工程師停用了 OSSpinLock俐载。
_YYLinkedMap 與 _LinkedMapNode
YYMemoryCache 無法直接操作緩存蟹略,而是通過內(nèi)部的?_YYLinkedMapNode?與?_YYLinkedMap?來的操作緩存對象。這兩個類對于上文中提到的 LRU 緩存算法的理解至關(guān)重要遏佣。
@interface _YYLinkedMapNode : NSObject {
????@package
????__unsafe_unretained _YYLinkedMapNode *_prev; // __unsafe_unretained 是為了性能優(yōu)化挖炬,節(jié)點被 _YYLinkedMap 的 _dic 強引用
????__unsafe_unretained _YYLinkedMapNode *_next; // __unsafe_unretained 是為了性能優(yōu)化,節(jié)點被 _YYLinkedMap 的 _dic 強引用
????id _key;
????id _value;
????NSUInteger _cost;? // 記錄開銷状婶,對應(yīng) YYMemoryCache 提供的 cost 控制
????NSTimeInterval _time;// 記錄時間意敛,對應(yīng) YYMemoryCache 提供的 age 控制
}
@end
@interface _YYLinkedMap : NSObject {
????@package
????CFMutableDictionaryRef _dic; // // 不要直接設(shè)置該對象
????NSUInteger _totalCost;
????NSUInteger _totalCount;
????_YYLinkedMapNode *_head; // MRU, 最常用節(jié)點,不要直接修改它
????_YYLinkedMapNode *_tail; // LRU, 最常用節(jié)點膛虫,不要直接修改它
????BOOL _releaseOnMainThread; // 對應(yīng) YYMemoryCache 的 releaseOnMainThread
????BOOL _releaseAsynchronously; // 對應(yīng) YYMemoryCache 的 releaseAsynchronously
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
- (void)removeNode:(_YYLinkedMapNode *)node;
- (_YYLinkedMapNode *)removeTailNode;
- (void)removeAll;
}
對數(shù)據(jù)結(jié)構(gòu)與算法不陌生的同學(xué)草姻,應(yīng)該一眼就看的出來?_YYLinkedMapNode?與?_YYLinkedMap?這的本質(zhì)。其實就是雙向鏈表節(jié)點和雙向鏈表稍刀。
? _YYLinkedMapNode?作為雙向鏈表節(jié)點撩独,除了基本的?_prev敞曹、_next,還有鍵值緩存基本的?_key?與?_value综膀,我們可以把?_YYLinkedMapNode?理解為 YYMemoryCache 中的一個緩存對象澳迫。_YYLinkedMap?作為由?_YYLinkedMapNode?節(jié)點組成的雙向鏈表,使用?CFMutableDictionaryRef _dic?字典存儲?_YYLinkedMapNode剧劝。這樣在確保?_YYLinkedMapNode?被強引用的同時橄登,能夠利用字典的 Hash 快速定位用戶要訪問的緩存對象,這樣既符合了鍵值緩存的概念又省去了自己實現(xiàn)的麻煩讥此÷G拢總得來說 YYMemoryCache 是通過使用?_YYLinkedMap雙向鏈表來操作?_YYLinkedMapNode?緩存對象節(jié)點的。
YYDiskCache簡介
YYDiskCache 是一個線程安全的磁盤緩存暂论,用于存儲由 SQLite 和文件系統(tǒng)支持的鍵值對(類似于 NSURLCache 的磁盤緩存)面褐。
YYDiskCache 具有以下功能:
通過 LRU 算法來刪除對象。
它可以被配置為當沒有可用的磁盤空間時自動驅(qū)逐緩存對象取胎。
它可以自動抉擇每個緩存對象的存儲類型(sqlite/file)以便提供更好的性能表現(xiàn)展哭。
@interface YYDiskCache : NSObject
#pragma mark - Attribute
@property (nullable, copy) NSString *name; // 緩存名稱,默認為 nil
@property (readonly) NSString *path; // 緩存路徑
@property (readonly) NSUInteger inlineThreshold; // 閾值闻蛀,大于閾值則存儲類型為 file匪傍;否則存儲類型為 sqlite
@property (nullable, copy) NSData *(^customArchiveBlock)(id object); // 用來替換 NSKeyedArchiver,你可以使用該代碼塊以支持沒有 conform `NSCoding` 協(xié)議的對象
@property (nullable, copy) id (^customUnarchiveBlock)(NSData *data); // 用來替換 NSKeyedUnarchiver觉痛,你可以使用該代碼塊以支持沒有 conform `NSCoding` 協(xié)議的對象
@property (nullable, copy) NSString *(^customFileNameBlock)(NSString *key); // 當一個對象將以 file 的形式保存時役衡,該代碼塊用來生成指定文件名。如果為 nil薪棒,則默認使用 md5(key) 作為文件名
#pragma mark - Limit
@property NSUInteger countLimit; // 緩存對象數(shù)量限制手蝎,默認無限制,超過限制則會在后臺逐出一些對象以滿足限制
@property NSUInteger costLimit; // 緩存開銷數(shù)量限制俐芯,默認無限制棵介,超過限制則會在后臺逐出一些對象以滿足限制
@property NSTimeInterval ageLimit; // 緩存時間限制,默認無限制吧史,超過限制則會在后臺逐出一些對象以滿足限制
@property NSUInteger freeDiskSpaceLimit; // 緩存應(yīng)該保留的最小可用磁盤空間(以字節(jié)為單位)邮辽,默認無限制,超過限制則會在后臺逐出一些對象以滿足限制
@property NSTimeInterval autoTrimInterval; // 緩存自動清理時間間隔贸营,默認 60s
@property BOOL errorLogsEnabled; // 是否開啟錯誤日志
#pragma mark - Initializer
- (nullable instancetype)initWithPath:(NSString *)path
??????????????????????inlineThreshold:(NSUInteger)threshold NS_DESIGNATED_INITIALIZER;
- (BOOL)containsObjectForKey:(NSString *)key;
- (nullable id)objectForKey:(NSString *)key;
- (void)setObject:(nullable id)object forKey:(NSString *)key;
- (void)removeObjectForKey:(NSString *)key;
- (void)removeAllObjects;
- (NSInteger)totalCount;
- (NSInteger)totalCost;
#pragma mark - Trim
- (void)trimToCount:(NSUInteger)count;
- (void)trimToCost:(NSUInteger)cost;
- (void)trimToAge:(NSTimeInterval)age;
#pragma mark - Extended Data
+ (nullable NSData *)getExtendedDataFromObject:(id)object;
+ (void)setExtendedData:(nullable NSData *)extendedData toObject:(id)object;
@end
YYDiskCache 是基于 sqlite 和 file 來做的磁盤緩存吨述,我們的緩存對象可以自由的選擇存儲類型,下面簡單對比一下:
sqlite: 對于小數(shù)據(jù)(例如 NSNumber)的存取效率明顯高于 file钞脂。
file: 對于較大數(shù)據(jù)(例如高質(zhì)量圖片)的存取效率優(yōu)于 sqlite揣云。
所以 YYDiskCache 使用兩者配合,靈活的存儲以提高性能冰啃。
YYDiskCache 內(nèi)部是基于一個單例 NSMapTable 管理灵再,
NSMapTable 是類似于字典的集合肋层,但具有更廣泛的可用內(nèi)存語義。NSMapTable 是 iOS6 之后引入的類翎迁,它基于 NSDictionary 建模,但是具有以下差異:
鍵/值可以選擇 “weakly” 持有净薛,以便于在回收其中一個對象時刪除對應(yīng)條目汪榔。
它可以包含任意指針(其內(nèi)容不被約束為對象)。
您可以將 NSMapTable 實例配置為對任意指針進行操作肃拜,而不僅僅是對象
每當一個 YYDiskCache 被初始化時痴腌,其實會先到 NSMapTable 中獲取對應(yīng) path 的 YYDiskCache 實例,如果獲取不到才會去真正的初始化一個 YYDiskCache 實例燃领,并且將其引用在 NSMapTable 中士聪,這樣做也會提升不少性能。
- (instancetype)initWithPath:(NSString *)path
?????????????inlineThreshold:(NSUInteger)threshold {
????//初始化判斷忽略
????// 先從 NSMapTable 單例中根據(jù) path 獲取 YYDiskCache 實例猛蔽,如果獲取到就直接返回該實例
????YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
????if (globalCache) return globalCache;
????// 沒有獲取到則初始化一個 YYDiskCache 實例
????// 要想初始化一個 YYDiskCache 首先要初始化一個 YYKVStorage
????YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
????if (!kv) return nil;
????// 根據(jù)剛才得到的 kv 和 path 入?yún)⒊跏蓟粋€ YYDiskCache 實例剥悟,代碼太長省略
????...
? ? // 開啟遞歸清理,會根據(jù) _autoTrimInterval 對 YYDiskCache trim
????[self _trimRecursively];
????// 向 NSMapTable 單例注冊新生成的 YYDiskCache 實例
????_YYDiskCacheSetGlobal(self);
????// App 生命周期通知相關(guān)代碼曼库,省略
????...
????return self;
}
dispatch_semaphore 是信號量区岗,但當信號總量設(shè)為 1 時也可以當作鎖來。在沒有等待情況出現(xiàn)時毁枯,它的性能比 pthread_mutex 還要高慈缔,但一旦有等待情況出現(xiàn)時,性能就會下降許多种玛。相對于 OSSpinLock 來說藐鹤,它的優(yōu)勢在于等待時不會消耗 CPU 資源。對磁盤緩存來說赂韵,它比較合適娱节。
YYKVStorageItem 與 YYKVStorage
在上邊的代碼中,我們看到了YYKVStorage右锨,YYDiskCache是通過YYKVStorage來操作緩存對象(sqlite/file)括堤,YYKVStorage 和 YYMemoryCache 中的雙向鏈表?_YYLinkedMap扮演的角色是一樣的,而對應(yīng)于?_YYLinkedMap?中的節(jié)點?_YYLinkedMapNode绍移,YYKVStorage 中也有一個類 YYKVStorageItem 充當著與緩存對象的角色悄窃。
/**
?用于YYStorage存儲鍵值對和屬性信息
?通常情況下,我們不應(yīng)該直接使用這個類蹂窖。
?*/
@interface YYKVStorageItem : NSObject
@property (nonatomic, strong) NSString *key;??????????????? ///< key?
@property (nonatomic, strong) NSData *value;??????????????? ///< value?
@property (nullable, nonatomic, strong) NSString *filename; ///< filename (nil if inline)
@property (nonatomic) int size;???????????????????????????? ///< value's size in bytes?
@property (nonatomic) int modTime;????????????????????????? ///< modification unix timestamp
@property (nonatomic) int accessTime;?????????????????????? ///< last access unix timestamp
@property (nullable, nonatomic, strong) NSData *extendedData; ///< extended data (nil if no extended data)
@end
/**
?YYKVStorage 是基于 sqlite 和file的鍵值存儲轧抗。
?通常情況下,我們不應(yīng)該直接使用這個類瞬测。
?@warning?
??這個類的實例是 *非* 線程安全的横媚,你需要確保
??只有一個線程可以同時訪問該實例纠炮。如果你真的
??需要在多線程中處理大量的數(shù)據(jù),應(yīng)該分割數(shù)據(jù)
??到多個 KVStorage 實例(分片)灯蝴。
?*/
@interface YYKVStorage : NSObject
#pragma mark - Attribute
@property (nonatomic, readonly) NSString *path;??????? /// storage 路徑
@property (nonatomic, readonly) YYKVStorageType type;? /// storage 類型
@property (nonatomic) BOOL errorLogsEnabled;?????????? /// 是否開啟錯誤日志
#pragma mark - Initializer
- (nullable instancetype)initWithPath:(NSString *)path type:(YYKVStorageType)type NS_DESIGNATED_INITIALIZER;
#pragma mark - Save Items
- (BOOL)saveItem:(YYKVStorageItem *)item;
...
#pragma mark - Remove Items
- (BOOL)removeItemForKey:(NSString *)key;
...
#pragma mark - Get Items
- (nullable YYKVStorageItem *)getItemForKey:(NSString *)key;
...
#pragma mark - Get Storage Status
- (BOOL)itemExistsForKey:(NSString *)key;
- (int)getItemsCount;
- (int)getItemsSize;
@end
這里我們看一下YYKVStorageType恢口,這個枚舉決定著 YYKVStorage 的存儲類型
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
????/// The `value` is stored as a file in file system.
????YYKVStorageTypeFile = 0,
????/// The `value` is stored in sqlite with blob type.
????YYKVStorageTypeSQLite = 1,
????/// The `value` is stored in file system or sqlite based on your choice.
????YYKVStorageTypeMixed = 2,
};
再看YYKVStorage代碼的同時,發(fā)現(xiàn)一個細節(jié)
? ??CFMutableDictionaryRef _dbStmtCache;
是 YYKVStorage 中的私有成員穷躁,它是一個可變字典充當著 sqlite3_stmt 緩存的角色耕肩。
- (sqlite3_stmt *)_dbPrepareStmt:(NSString *)sql {
????if (![self _dbCheck] || sql.length == 0 || !_dbStmtCache) return NULL;
????// 先嘗試從 _dbStmtCache 根據(jù)入?yún)?sql 取出已緩存 sqlite3_stmt
????sqlite3_stmt *stmt = (sqlite3_stmt *)CFDictionaryGetValue(_dbStmtCache, (__bridge const void *)(sql));
????if (!stmt) {
????????// 如果沒有緩存再從新生成一個 sqlite3_stmt
????????int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL);
????????// 生成結(jié)果異常則根據(jù)錯誤日志開啟標識打印日志
????????if (result != SQLITE_OK) {
????????????if (_errorLogsEnabled) NSLog(@"%s line:%d sqlite stmt prepare error (%d): %s", __FUNCTION__, __LINE__, result, sqlite3_errmsg(_db));
????????????return NULL;
????????}
????????// 生成成功則放入 _dbStmtCache 緩存
????????CFDictionarySetValue(_dbStmtCache, (__bridge const void *)(sql), stmt);
????} else {
????????sqlite3_reset(stmt);
????}
????return stmt;
}
這樣就可以省去一些重復(fù)生成 sqlite3_stmt 的開銷。