前言
日常的iOS開發(fā)過程中,經(jīng)常會(huì)用到緩存,但是什么樣的緩存才能被叫做優(yōu)秀的緩存,或者說優(yōu)秀的緩存應(yīng)該具備哪些特質(zhì)?YYCache我認(rèn)為是一個(gè)比較優(yōu)秀的緩存,代碼邏輯清晰,注釋詳盡,加上自身不算太大的代碼量使得其閱讀非常簡(jiǎn)單,更可貴的是它的性能還很高。
YYCache簡(jiǎn)介

我們先來簡(jiǎn)單看一下 YYCache 的代碼結(jié)構(gòu),YYCache 是由 YYMemoryCache 與 YYDiskCache 兩部分組成的,其中 YYMemoryCache 作為高速內(nèi)存緩存,而 YYDiskCache 則作為低速磁盤緩存。
通常一個(gè)緩存是由內(nèi)存緩存和磁盤緩存組成,內(nèi)存緩存提供容量小但高速的存取功能,磁盤緩存提供大容量但低速的持久化存儲(chǔ)。
@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值對(duì)應(yīng)的對(duì)象 會(huì)阻塞調(diào)用的進(jìn)程*/
- (nullable id)objectForKey:(NSString *)key;
/** 獲取key值對(duì)應(yīng)的對(duì)象,并執(zhí)行block*/
- (void)objectForKey:(NSString *)key withBlock:(nullable?void(^)(NSString *key, id object))block;
/** 對(duì)某個(gè)key設(shè)置對(duì)象,阻塞線程*/
- (void)setObject:(nullable id)object forKey:(NSString *)key;
/** 設(shè)置key的對(duì)象,線程會(huì)立即返回,設(shè)置成功后回調(diào)block*/
- (void)setObject:(nullable id)object forKey:(NSString *)key withBlock:(nullable?void(^)(void))block;
/**刪除key對(duì)應(yīng)的對(duì)象 阻塞線程 */
- (void)removeObjectForKey:(NSString *)key;
/**刪除key對(duì)應(yīng)的object 線程會(huì)立即返回,刪除成功后回調(diào)block*/
- (void)removeObjectForKey:(NSString *)key withBlock:(nullable?void(^)(NSString *key))block;
/**清空緩存*/
- (void)removeAllObjects;
/** 清空緩存, 線程會(huì)立即返回,清空成功后回調(diào)block */
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
/**清空緩存, 線程會(huì)立即返回,后臺(tái)線程執(zhí)行block*/
- (void)removeAllObjectsWithProgressBlock:(nullable?void(^)(int?removedCount,?int?totalCount))progress ?endBlock:(nullable?void(^)(BOOL?error))end;
上邊整理了幾個(gè)常用的方法,做了簡(jiǎn)單的中文注釋,從代碼中我們可以看到 YYCache 中持有 YYMemoryCache 與 YYDiskCache,并且對(duì)外提供了一些接口。這些接口基本都是基于 Key 和 Value 設(shè)計(jì)的,類似于 iOS 原生的字典類接口(增刪改查)
YYMemoryCache
YYMemoryCache 是一個(gè)高速的內(nèi)存緩存,用于存儲(chǔ)鍵值對(duì)。它與 NSDictionary 相反,Key 被保留并且不復(fù)制。API 和性能類似于 NSCache,所有方法都是線程安全的。
YYMemoryCache 使用 LRU(least-recently-used) 算法來驅(qū)逐對(duì)象。介紹一下LRU:
LRU(Least?recently?used,最近最少使用)算法根據(jù)數(shù)據(jù)的歷史訪問記錄來進(jìn)行淘汰數(shù)據(jù),其核心思想是“如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也更高”。最常見的實(shí)現(xiàn)是使用一個(gè)鏈表保存緩存數(shù)據(jù),詳細(xì)算法實(shí)現(xiàn)如下:
? ? ? ? ? ?1. 新數(shù)據(jù)插入到鏈表頭部;
? ? ? ? ? ?2.?每當(dāng)緩存命中(即緩存數(shù)據(jù)被訪問),則將數(shù)據(jù)移到鏈表頭部;
? ? ? ? ? ?3.?當(dāng)鏈表滿的時(shí)候,將鏈表尾部的數(shù)據(jù)丟棄。
? ? ?分析
? ? ? ?【命中率】
? ? ? ? ? ? 當(dāng)存在熱點(diǎn)數(shù)據(jù)時(shí),LRU的效率很好,但偶發(fā)性的、周期性的批量操作會(huì)導(dǎo)致LRU命中率急劇下降,緩存污染情況比較嚴(yán)重。
? ? ? ?【復(fù)雜度】
? ? ? ? ? ? 實(shí)現(xiàn)簡(jiǎn)單。
? ? ? 【代價(jià)】?
? ? ? ? ? ? 命中時(shí)需要遍歷鏈表,找到命中的數(shù)據(jù)塊索引,然后需要將數(shù)據(jù)移到頭部。
YYMemoryCache是線程安全的
@implementation YYMemoryCache {
????pthread_mutex_t _lock; // 線程鎖,旨在保證 YYMemoryCache 線程安全
????_YYLinkedMap *_lru; // _YYLinkedMap,YYMemoryCache 通過它間接操作緩存對(duì)象
????dispatch_queue_t _queue; // 串行隊(duì)列,用于 YYMemoryCache 的 trim 操作
}
? 沒錯(cuò),YYMemoryCache使用?pthread_mutex線程鎖來確保線程安全。最初YYMemoryCache 這里使用的鎖是?OSSpinLock?自旋鎖,后面有人在 Github 向作者提?issue?反饋?OSSpinLock?不安全,經(jīng)過作者的確認(rèn)(詳見?不再安全的 OSSpinLock)最后選擇用?pthread_mutex?替代?OSSpinLock。
具體來說,如果一個(gè)低優(yōu)先級(jí)的線程獲得鎖并訪問共享資源,這時(shí)一個(gè)高優(yōu)先級(jí)的線程也嘗試獲得這個(gè)鎖,它會(huì)處于 spin lock 的忙等狀態(tài)從而占用大量 CPU。此時(shí)低優(yōu)先級(jí)線程無(wú)法與高優(yōu)先級(jí)線程爭(zhēng)奪 CPU 時(shí)間,從而導(dǎo)致任務(wù)遲遲完不成、無(wú)法釋放 lock。這并不只是理論上的問題,libobjc 已經(jīng)遇到了很多次這個(gè)問題了,于是蘋果的工程師停用了 OSSpinLock。
_YYLinkedMap 與 _LinkedMapNode
YYMemoryCache 無(wú)法直接操作緩存,而是通過內(nèi)部的?_YYLinkedMapNode?與?_YYLinkedMap?來的操作緩存對(duì)象。這兩個(gè)類對(duì)于上文中提到的 LRU 緩存算法的理解至關(guān)重要。
@interface _YYLinkedMapNode : NSObject {
????@package
????__unsafe_unretained _YYLinkedMapNode *_prev; // __unsafe_unretained 是為了性能優(yōu)化,節(jié)點(diǎn)被 _YYLinkedMap 的 _dic 強(qiáng)引用
????__unsafe_unretained _YYLinkedMapNode *_next; // __unsafe_unretained 是為了性能優(yōu)化,節(jié)點(diǎn)被 _YYLinkedMap 的 _dic 強(qiáng)引用
????id _key;
????id _value;
????NSUInteger _cost;? // 記錄開銷,對(duì)應(yīng) YYMemoryCache 提供的 cost 控制
????NSTimeInterval _time;// 記錄時(shí)間,對(duì)應(yīng) YYMemoryCache 提供的 age 控制
}
@end
@interface _YYLinkedMap : NSObject {
????@package
????CFMutableDictionaryRef _dic; // // 不要直接設(shè)置該對(duì)象
????NSUInteger _totalCost;
????NSUInteger _totalCount;
????_YYLinkedMapNode *_head; // MRU, 最常用節(jié)點(diǎn),不要直接修改它
????_YYLinkedMapNode *_tail; // LRU, 最常用節(jié)點(diǎn),不要直接修改它
????BOOL _releaseOnMainThread; // 對(duì)應(yīng) YYMemoryCache 的 releaseOnMainThread
????BOOL _releaseAsynchronously; // 對(duì)應(yīng) YYMemoryCache 的 releaseAsynchronously
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
- (void)removeNode:(_YYLinkedMapNode *)node;
- (_YYLinkedMapNode *)removeTailNode;
- (void)removeAll;
}
對(duì)數(shù)據(jù)結(jié)構(gòu)與算法不陌生的同學(xué),應(yīng)該一眼就看的出來?_YYLinkedMapNode?與?_YYLinkedMap?這的本質(zhì)。其實(shí)就是雙向鏈表節(jié)點(diǎn)和雙向鏈表。
? _YYLinkedMapNode?作為雙向鏈表節(jié)點(diǎn),除了基本的?_prev、_next,還有鍵值緩存基本的?_key?與?_value,我們可以把?_YYLinkedMapNode?理解為 YYMemoryCache 中的一個(gè)緩存對(duì)象。_YYLinkedMap?作為由?_YYLinkedMapNode?節(jié)點(diǎn)組成的雙向鏈表,使用?CFMutableDictionaryRef _dic?字典存儲(chǔ)?_YYLinkedMapNode。這樣在確保?_YYLinkedMapNode?被強(qiáng)引用的同時(shí),能夠利用字典的 Hash 快速定位用戶要訪問的緩存對(duì)象,這樣既符合了鍵值緩存的概念又省去了自己實(shí)現(xiàn)的麻煩。總得來說 YYMemoryCache 是通過使用?_YYLinkedMap雙向鏈表來操作?_YYLinkedMapNode?緩存對(duì)象節(jié)點(diǎn)的。
YYDiskCache簡(jiǎn)介
YYDiskCache 是一個(gè)線程安全的磁盤緩存,用于存儲(chǔ)由 SQLite 和文件系統(tǒng)支持的鍵值對(duì)(類似于 NSURLCache 的磁盤緩存)。
YYDiskCache 具有以下功能:
通過 LRU 算法來刪除對(duì)象。
它可以被配置為當(dāng)沒有可用的磁盤空間時(shí)自動(dòng)驅(qū)逐緩存對(duì)象。
它可以自動(dòng)抉擇每個(gè)緩存對(duì)象的存儲(chǔ)類型(sqlite/file)以便提供更好的性能表現(xiàn)。
@interface YYDiskCache : NSObject
#pragma mark - Attribute
@property (nullable, copy) NSString *name; // 緩存名稱,默認(rèn)為 nil
@property (readonly) NSString *path; // 緩存路徑
@property (readonly) NSUInteger inlineThreshold; // 閾值,大于閾值則存儲(chǔ)類型為 file;否則存儲(chǔ)類型為 sqlite
@property (nullable, copy) NSData *(^customArchiveBlock)(id object); // 用來替換 NSKeyedArchiver,你可以使用該代碼塊以支持沒有 conform `NSCoding` 協(xié)議的對(duì)象
@property (nullable, copy) id (^customUnarchiveBlock)(NSData *data); // 用來替換 NSKeyedUnarchiver,你可以使用該代碼塊以支持沒有 conform `NSCoding` 協(xié)議的對(duì)象
@property (nullable, copy) NSString *(^customFileNameBlock)(NSString *key); // 當(dāng)一個(gè)對(duì)象將以 file 的形式保存時(shí),該代碼塊用來生成指定文件名。如果為 nil,則默認(rèn)使用 md5(key) 作為文件名
#pragma mark - Limit
@property NSUInteger countLimit; // 緩存對(duì)象數(shù)量限制,默認(rèn)無(wú)限制,超過限制則會(huì)在后臺(tái)逐出一些對(duì)象以滿足限制
@property NSUInteger costLimit; // 緩存開銷數(shù)量限制,默認(rèn)無(wú)限制,超過限制則會(huì)在后臺(tái)逐出一些對(duì)象以滿足限制
@property NSTimeInterval ageLimit; // 緩存時(shí)間限制,默認(rèn)無(wú)限制,超過限制則會(huì)在后臺(tái)逐出一些對(duì)象以滿足限制
@property NSUInteger freeDiskSpaceLimit; // 緩存應(yīng)該保留的最小可用磁盤空間(以字節(jié)為單位),默認(rèn)無(wú)限制,超過限制則會(huì)在后臺(tái)逐出一些對(duì)象以滿足限制
@property NSTimeInterval autoTrimInterval; // 緩存自動(dòng)清理時(shí)間間隔,默認(rèn) 60s
@property BOOL errorLogsEnabled; // 是否開啟錯(cuò)誤日志
#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 來做的磁盤緩存,我們的緩存對(duì)象可以自由的選擇存儲(chǔ)類型,下面簡(jiǎn)單對(duì)比一下:
sqlite: 對(duì)于小數(shù)據(jù)(例如 NSNumber)的存取效率明顯高于 file。
file: 對(duì)于較大數(shù)據(jù)(例如高質(zhì)量圖片)的存取效率優(yōu)于 sqlite。
所以 YYDiskCache 使用兩者配合,靈活的存儲(chǔ)以提高性能。
YYDiskCache 內(nèi)部是基于一個(gè)單例 NSMapTable 管理,
NSMapTable 是類似于字典的集合,但具有更廣泛的可用內(nèi)存語(yǔ)義。NSMapTable 是 iOS6 之后引入的類,它基于 NSDictionary 建模,但是具有以下差異:
鍵/值可以選擇 “weakly” 持有,以便于在回收其中一個(gè)對(duì)象時(shí)刪除對(duì)應(yīng)條目。
它可以包含任意指針(其內(nèi)容不被約束為對(duì)象)。
您可以將 NSMapTable 實(shí)例配置為對(duì)任意指針進(jìn)行操作,而不僅僅是對(duì)象
每當(dāng)一個(gè) YYDiskCache 被初始化時(shí),其實(shí)會(huì)先到 NSMapTable 中獲取對(duì)應(yīng) path 的 YYDiskCache 實(shí)例,如果獲取不到才會(huì)去真正的初始化一個(gè) YYDiskCache 實(shí)例,并且將其引用在 NSMapTable 中,這樣做也會(huì)提升不少性能。
- (instancetype)initWithPath:(NSString *)path
?????????????inlineThreshold:(NSUInteger)threshold {
????//初始化判斷忽略
????// 先從 NSMapTable 單例中根據(jù) path 獲取 YYDiskCache 實(shí)例,如果獲取到就直接返回該實(shí)例
????YYDiskCache *globalCache = _YYDiskCacheGetGlobal(path);
????if (globalCache) return globalCache;
????// 沒有獲取到則初始化一個(gè) YYDiskCache 實(shí)例
????// 要想初始化一個(gè) YYDiskCache 首先要初始化一個(gè) YYKVStorage
????YYKVStorage *kv = [[YYKVStorage alloc] initWithPath:path type:type];
????if (!kv) return nil;
????// 根據(jù)剛才得到的 kv 和 path 入?yún)⒊跏蓟粋€(gè) YYDiskCache 實(shí)例,代碼太長(zhǎng)省略
????...
? ? // 開啟遞歸清理,會(huì)根據(jù) _autoTrimInterval 對(duì) YYDiskCache trim
????[self _trimRecursively];
????// 向 NSMapTable 單例注冊(cè)新生成的 YYDiskCache 實(shí)例
????_YYDiskCacheSetGlobal(self);
????// App 生命周期通知相關(guān)代碼,省略
????...
????return self;
}
dispatch_semaphore 是信號(hào)量,但當(dāng)信號(hào)總量設(shè)為 1 時(shí)也可以當(dāng)作鎖來。在沒有等待情況出現(xiàn)時(shí),它的性能比 pthread_mutex 還要高,但一旦有等待情況出現(xiàn)時(shí),性能就會(huì)下降許多。相對(duì)于 OSSpinLock 來說,它的優(yōu)勢(shì)在于等待時(shí)不會(huì)消耗 CPU 資源。對(duì)磁盤緩存來說,它比較合適。
YYKVStorageItem 與 YYKVStorage
在上邊的代碼中,我們看到了YYKVStorage,YYDiskCache是通過YYKVStorage來操作緩存對(duì)象(sqlite/file),YYKVStorage 和 YYMemoryCache 中的雙向鏈表?_YYLinkedMap扮演的角色是一樣的,而對(duì)應(yīng)于?_YYLinkedMap?中的節(jié)點(diǎn)?_YYLinkedMapNode,YYKVStorage 中也有一個(gè)類 YYKVStorageItem 充當(dāng)著與緩存對(duì)象的角色。
/**
?用于YYStorage存儲(chǔ)鍵值對(duì)和屬性信息
?通常情況下,我們不應(yīng)該直接使用這個(gè)類。
?*/
@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的鍵值存儲(chǔ)。
?通常情況下,我們不應(yīng)該直接使用這個(gè)類。
?@warning?
??這個(gè)類的實(shí)例是 *非* 線程安全的,你需要確保
??只有一個(gè)線程可以同時(shí)訪問該實(shí)例。如果你真的
??需要在多線程中處理大量的數(shù)據(jù),應(yīng)該分割數(shù)據(jù)
??到多個(gè) KVStorage 實(shí)例(分片)。
?*/
@interface YYKVStorage : NSObject
#pragma mark - Attribute
@property (nonatomic, readonly) NSString *path;??????? /// storage 路徑
@property (nonatomic, readonly) YYKVStorageType type;? /// storage 類型
@property (nonatomic) BOOL errorLogsEnabled;?????????? /// 是否開啟錯(cuò)誤日志
#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,這個(gè)枚舉決定著 YYKVStorage 的存儲(chǔ)類型
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代碼的同時(shí),發(fā)現(xiàn)一個(gè)細(xì)節(jié)
? ??CFMutableDictionaryRef _dbStmtCache;
是 YYKVStorage 中的私有成員,它是一個(gè)可變字典充當(dāng)著 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) {
????????// 如果沒有緩存再?gòu)男律梢粋€(gè) sqlite3_stmt
????????int result = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &stmt, NULL);
????????// 生成結(jié)果異常則根據(jù)錯(cuò)誤日志開啟標(biāo)識(shí)打印日志
????????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 的開銷。