YYCache簡(jiǎn)介

YYCache由YYMemoryCache(高速內(nèi)存緩存)和YYDiskCache(低速磁盤緩存)兩部分組成;
通常一個(gè)緩存是由內(nèi)存緩存和磁盤緩存組成,內(nèi)存緩存提供容量小但高速的存取功能,磁盤緩存提供大容量但低速的持久化存儲(chǔ)
Note: 其實(shí)源碼中作者用了一些技巧性的宏,例如NS_ASSUME_NONNULL_BEGIN與NS_ASSUME_NONNULL_END來(lái)通過(guò)編譯器層檢測(cè)入?yún)⑹欠駷榭詹⒔o予警告

從代碼中我們可以看到 YYCache 中持有 YYMemoryCache 與 YYDiskCache,并且對(duì)外提供了一些接口。這些接口基本都是基于 Key 和 Value 設(shè)計(jì)的,類似于 iOS 原生的字典類接口(增刪改查)。
YYMemoryCache的解讀
YYMemoryCache 是一個(gè)高速的內(nèi)存緩存,用于存儲(chǔ)鍵值對(duì)。它與 NSDictionary 相反,Key 被保留并且不復(fù)制。API 和性能類似于 NSCache,所有方法都是線程安全的。
YYMemoryCache 對(duì)象與 NSCache 的不同之處在于:
YYMemoryCache 使用 LRU(least-recently-used) 算法來(lái)驅(qū)逐對(duì)象;NSCache 的驅(qū)逐方式是非確定性的。
YYMemoryCache 提供 age、cost、count 三種方式控制緩存;NSCache 的控制方式是不精確的。
YYMemoryCache 可以配置為在收到內(nèi)存警告或者 App 進(jìn)入后臺(tái)時(shí)自動(dòng)逐出對(duì)象。
Note: YYMemoryCache 中的Access Methods消耗時(shí)長(zhǎng)通常是穩(wěn)定的(O(1))。
貼出幾個(gè)我在讀代碼的時(shí)候不太弄清屬性和方法
@property(readonly) NSUInteger totalCount;// 緩存對(duì)象總數(shù)
@property(readonly) NSUInteger totalCost;// 緩存對(duì)象總開銷
@property NSTimeInterval autoTrimInterval; // 緩存自動(dòng)清理時(shí)間間隔,默認(rèn) 5s
@property BOOL releaseOnMainThread; // 是否在主線程釋放對(duì)象,默認(rèn) NO,有些對(duì)象(例如 UIView/CALayer)應(yīng)該在主線程釋放
@property BOOL releaseAsynchronously; // 是否異步釋放對(duì)象,默認(rèn) YES
YYMemoryCache 線程安全的做法
使用pthread_mutex線程鎖來(lái)確保 YYMemoryCache 的線程安全。
@implementationYYMemoryCache{
pthread_mutex_t _lock;// 線程鎖,旨在保證 YYMemoryCache 線程安全
_YYLinkedMap *_lru;// _YYLinkedMap,YYMemoryCache 通過(guò)它間接操作緩存對(duì)象
dispatch_queue_t_queue;// 串行隊(duì)列,用于 YYMemoryCache 的 trim 操作}
注:在最初 YYMemoryCache 這里使用的鎖是OSSpinLock自旋鎖(詳見YYCache 設(shè)計(jì)思路備注-關(guān)于鎖),后面有人在 Github 向作者提issue反饋OSSpinLock不安全,經(jīng)過(guò)作者的確認(rèn)(詳見不再安全的 OSSpinLock)最后選擇用pthread_mutex替代OSSpinLock。

上面是 ibireme 在確認(rèn)OSSpinLock不再安全之后為了尋找替代方案做的簡(jiǎn)單性能測(cè)試,對(duì)比了一下幾種能夠替代OSSpinLock鎖的性能。在不再安全的 OSSpinLock文末的評(píng)論中,我找到了作者使用pthread_mutex的原因。
ibireme: 蘋果員工說(shuō) libobjc 里spinlock是用了一些私有方法 (mach_thread_switch),貢獻(xiàn)出了高線程的優(yōu)先來(lái)避免優(yōu)先級(jí)反轉(zhuǎn)的問(wèn)題,但是我翻了下 libdispatch 的源碼倒是沒(méi)發(fā)現(xiàn)相關(guān)邏輯,也可能是我忽略了什么。在我的一些測(cè)試中,OSSpinLock和dispatch_semaphore都不會(huì)產(chǎn)生特別明顯的死鎖,所以我也無(wú)法確定用dispatch_semaphore代替OSSpinLock是否正確。能夠肯定的是,用pthread_mutex是安全的。
_YYLinkedMapNode與_YYLinkedMap
通過(guò)內(nèi)部的_YYLinkedMapNode與_YYLinkedMap來(lái)間接的操作緩存對(duì)象。
_YYLinkedMapNode是_YYLinkedMap 中的一個(gè)節(jié)點(diǎn)。通常情況下我們不應(yīng)該使用這個(gè)類
_YYLinkedMapNode屬性:
__unsafe_unretained _YYLinkedMapNode *_prev; // __unsafe_unretained 是為了性能優(yōu)化,節(jié)點(diǎn)被 _YYLinkedMap 的 _dic 強(qiáng)引用
NSUInteger_cost;// 記錄開銷,對(duì)應(yīng) YYMemoryCache 提供的 cost 控制NSTimeInterval_time;// 記錄時(shí)間,對(duì)應(yīng) YYMemoryCache 提供的 age 控制
_YYLinkedMap是YYMemoryCache 內(nèi)的一個(gè)鏈表。_YYLinkedMap 不是一個(gè)線程安全的類,而且它也不對(duì)參數(shù)做校驗(yàn)。通常情況下我們不應(yīng)該使用這個(gè)類。
_YYLinkedMapNode與_YYLinkedMap是雙向鏈表節(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 快速定位用戶要訪問(wèn)的緩存對(duì)象,這樣既符合了鍵值緩存的概念又省去了自己實(shí)現(xiàn)的麻煩
總得來(lái)說(shuō) YYMemoryCache 是通過(guò)使用_YYLinkedMap雙向鏈表來(lái)操作_YYLinkedMapNode緩存對(duì)象節(jié)點(diǎn)的
LRU(least-recently-used) 算法的實(shí)現(xiàn)
緩存替換策略
首先 LRU 是緩存替換策略(Cache replacement policies)的一種,還有很多緩存替換策略諸如:
First In First Out (FIFO)
Last In First Out (LIFO)
Time aware Least Recently Used (TLRU)
Most Recently Used (MRU)
Pseudo-LRU (PLRU)
Random Replacement (RR)
Segmented LRU (SLRU)
Least-Frequently Used (LFU)
Least Frequent Recently Used (LFRU)
LFU with Dynamic Aging (LFUDA)
Low Inter-reference Recency Set (LIRS)
Adaptive Replacement Cache (ARC)
Clock with Adaptive Replacement (CAR)
Multi Queue (MQ) caching algorithm|Multi Queue (MQ)
Pannier: Container-based caching algorithm for compound objects
緩存替換策略是為了提高緩存命中率
緩存命中 = 用戶要訪問(wèn)的緩存對(duì)象在高速緩存中,我們直接在高速緩存中通過(guò)Hash將其找到并返回給用戶
緩存命中率 =?用戶要訪問(wèn)的緩存對(duì)象在高速緩存中被我們?cè)L問(wèn)到的概率。
緩存丟失 = 由于高速緩存數(shù)量有限(占據(jù)內(nèi)存等原因),所以用戶要訪問(wèn)的緩存對(duì)象很有可能被我們從有限的高速緩存中淘汰掉了,我們可能會(huì)將其存儲(chǔ)于低速的磁盤緩存中(如果磁盤緩存還有資源的話),那么就要從磁盤緩存中獲取該緩存對(duì)象以返回給用戶,這種情況我理解為(高速)緩存未命中,即緩存丟失(并不是真的被我們丟掉了,但肯定是被我們從高速緩存淘汰掉了)。
LRU
LRU(least-recently-used) 翻譯過(guò)來(lái)是“最近使用”,顧名思義這種緩存替換策略是基于用戶最近訪問(wèn)過(guò)的緩存對(duì)象而建立。 LRU 緩存替換策略的核心思想在于:LRU 認(rèn)為用戶最新使用(訪問(wèn))過(guò)的緩存對(duì)象為高頻緩存對(duì)象,即用戶很可能還會(huì)再次使用(訪問(wèn))該緩存對(duì)象;而反之,用戶很久之前使用(訪問(wèn))過(guò)的緩存對(duì)象(期間一直沒(méi)有再次訪問(wèn))為低頻緩存對(duì)象,即用戶很可能不會(huì)再去使用(訪問(wèn))該緩存對(duì)象,通常在資源不足時(shí)會(huì)先去釋放低頻緩存對(duì)象。
_YYLinkedMapNode與_YYLinkedMap實(shí)現(xiàn) LRU
YYCache 作者通過(guò)_YYLinkedMapNode與_YYLinkedMap雙向鏈表實(shí)現(xiàn) LRU 緩存替換策略的思路:
雙向鏈表中有頭結(jié)點(diǎn)和尾節(jié)點(diǎn):
頭結(jié)點(diǎn) = 鏈表中用戶最近一次使用(訪問(wèn))的緩存對(duì)象節(jié)點(diǎn),MRU。(more-recently-used)
尾節(jié)點(diǎn) = 鏈表中用戶已經(jīng)很久沒(méi)有再次使用(訪問(wèn))的緩存對(duì)象節(jié)點(diǎn),LRU。(least-recently-used)
如何讓頭結(jié)點(diǎn)和尾節(jié)點(diǎn)指向我們想指向的緩存對(duì)象節(jié)點(diǎn)?我們結(jié)合代碼來(lái)看:
1>在用戶使用(訪問(wèn))時(shí)更新緩存節(jié)點(diǎn)信息,并將其移動(dòng)至雙向鏈表頭結(jié)點(diǎn)。

2>在用戶設(shè)置緩存對(duì)象時(shí),判斷入?yún)?key 對(duì)應(yīng)的緩存對(duì)象節(jié)點(diǎn)是否存在?存在則更新緩存對(duì)象節(jié)點(diǎn)并將節(jié)點(diǎn)移動(dòng)至鏈表頭結(jié)點(diǎn);不存在則根據(jù)入?yún)⑸尚碌木彺鎸?duì)象節(jié)點(diǎn)并插入鏈表表頭。

3>在資源不足時(shí),從雙線鏈表的尾節(jié)點(diǎn)(LRU)開始清理緩存,釋放資源。

上面3>涉及到的異步釋放技巧的解釋:
這個(gè)技巧 ibireme 在他的另一篇文章iOS 保持界面流暢的技巧中有提及:
Note: 對(duì)象的銷毀雖然消耗資源不多,但累積起來(lái)也是不容忽視的。通常當(dāng)容器類持有大量對(duì)象時(shí),其銷毀時(shí)的資源消耗就非常明顯。同樣的,如果對(duì)象可以放到后臺(tái)線程去釋放,那就挪到后臺(tái)線程去。這里有個(gè)小 Tip:把對(duì)象捕獲到 block 中,然后扔到后臺(tái)隊(duì)列去隨便發(fā)送個(gè)消息以避免編譯器警告,就可以讓對(duì)象在后臺(tái)線程銷毀了。
// 靜態(tài)內(nèi)聯(lián) dispatch_queue_tstaticinlinedispatch_queue_tYYMemoryCacheGetReleaseQueue() {returndispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW,0);}
在源碼中可以看到 YYMemoryCacheGetReleaseQueue 是一個(gè)低優(yōu)先級(jí)DISPATCH_QUEUE_PRIORITY_LOW隊(duì)列,猜測(cè)這樣設(shè)計(jì)的原因是可以讓 iOS 在系統(tǒng)相對(duì)空閑時(shí)再來(lái)異步釋放緩存對(duì)象。
YYDiskCache 細(xì)節(jié)剖析
YYDiskCache 是一個(gè)線程安全的磁盤緩存,用于存儲(chǔ)由 SQLite 和文件系統(tǒng)支持的鍵值對(duì)(類似于 NSURLCache 的磁盤緩存)。
YYDiskCache 具有以下功能:
它使用 LRU(least-recently-used) 來(lái)刪除對(duì)象。
支持按 cost,count 和 age 進(jìn)行控制。
它可以被配置為當(dāng)沒(méi)有可用的磁盤空間時(shí)自動(dòng)驅(qū)逐緩存對(duì)象。
它可以自動(dòng)抉擇每個(gè)緩存對(duì)象的存儲(chǔ)類型(sqlite/file)以便提供更好的性能表現(xiàn)。
YYDiskCache 是基于 sqlite 和 file 來(lái)做的磁盤緩存,我們的緩存對(duì)象可以自由的選擇存儲(chǔ)類型,下面簡(jiǎn)單對(duì)比一下:
sqlite: 對(duì)于小數(shù)據(jù)(例如 NSNumber)的存取效率明顯高于 file。
file: 對(duì)于較大數(shù)據(jù)(例如高質(zhì)量圖片)的存取效率優(yōu)于 sqlite。
所以 YYDiskCache 使用兩者配合,靈活的存儲(chǔ)以提高性能。
NSMapTable
NSMapTable 是類似于字典的集合,但具有更廣泛的可用內(nèi)存語(yǔ)義。NSMapTable 是 iOS6 之后引入的類,它基于 NSDictionary 建模,但是具有以下差異:
鍵/值可以選擇 “weakly” 持有,以便于在回收其中一個(gè)對(duì)象時(shí)刪除對(duì)應(yīng)條目。
它可以包含任意指針(其內(nèi)容不被約束為對(duì)象)。
您可以將 NSMapTable 實(shí)例配置為對(duì)任意指針進(jìn)行操作,而不僅僅是對(duì)象。
Note: 配置映射表時(shí),請(qǐng)注意,只有 NSMapTableOptions 中列出的選項(xiàng)才能保證其余的 API 能夠正常工作,包括復(fù)制,歸檔和快速枚舉。 雖然其他 NSPointerFunctions 選項(xiàng)用于某些配置,例如持有任意指針,但并不是所有選項(xiàng)的組合都有效。使用某些組合,NSMapTableOptions 可能無(wú)法正常工作,甚至可能無(wú)法正確初始化。
更多信息詳見NSMapTable 官方文檔。
YYDiskCache 使用 dispatch_semaphore 保障 NSMapTable 線程安全
在YYCache 設(shè)計(jì)思路中找到了作者使用 dispatch_semaphore 作為 YYDiskCache 鎖的原因:
dispatch_semaphore 是信號(hào)量,但當(dāng)信號(hào)總量設(shè)為 1 時(shí)也可以當(dāng)作鎖來(lái)。在沒(méi)有等待情況出現(xiàn)時(shí),它的性能比 pthread_mutex 還要高,但一旦有等待情況出現(xiàn)時(shí),性能就會(huì)下降許多。相對(duì)于 OSSpinLock 來(lái)說(shuō),它的優(yōu)勢(shì)在于等待時(shí)不會(huì)消耗 CPU 資源。對(duì)磁盤緩存來(lái)說(shuō),它比較合適。
與 YYMemoryCache 相對(duì)應(yīng)的,YYDiskCache 也不會(huì)直接操作緩存對(duì)象(sqlite/file),而是通過(guò) YYKVStorage 來(lái)間接的操作緩存對(duì)象。
YYKVStorage 性能優(yōu)化細(xì)節(jié)
上文說(shuō)到 YYKVStorage 可以基于 SQLite 和文件系統(tǒng)做磁盤存儲(chǔ),這里再提一些我閱讀源碼發(fā)現(xiàn)到的有趣細(xì)節(jié):
@implementationYYKVStorage{
...CFMutableDictionaryRef_dbStmtCache;// 焦點(diǎn)集中在這里...
}
可以看到CFMutableDictionaryRef _dbStmtCache;是 YYKVStorage 中的私有成員,它是一個(gè)可變字典充當(dāng)著 sqlite3_stmt 緩存的角色。
- (sqlite3_stmt *)_dbPrepareStmt:(NSString*)sql 該方法可以省去一些重復(fù)生成 sqlite3_stmt 的開銷。
sqlite3_stmt: 該對(duì)象的實(shí)例表示已經(jīng)編譯成二進(jìn)制形式并準(zhǔn)備執(zhí)行的單個(gè) SQL 語(yǔ)句
更多關(guān)于 SQLite 的信息請(qǐng)點(diǎn)擊SQLite 官方文檔查閱。
優(yōu)秀的緩存應(yīng)該具備哪些特質(zhì)
內(nèi)存緩存和磁盤緩存
線程安全
緩存控制
緩存替換策略
緩存命中率
性能
YYCache 源碼中是如何體現(xiàn)這些特質(zhì)的。
1>內(nèi)存緩存和磁盤緩存
YYCache 是由內(nèi)存緩存 YYMemoryCache 與磁盤緩存 YYDiskCache 相互配合組成的,內(nèi)存緩存提供容量小但高速的存取功能,磁盤緩存提供大容量但低速的持久化存儲(chǔ)。這樣的設(shè)計(jì)支持用戶在緩存不同對(duì)象時(shí)都能夠有很好的體驗(yàn)。
在 YYCache 中使用接口訪問(wèn)緩存對(duì)象時(shí),會(huì)先去嘗試從內(nèi)存緩存 YYMemoryCache 中訪問(wèn),如果訪問(wèn)不到(沒(méi)有使用該 key 緩存過(guò)對(duì)象或者該對(duì)象已經(jīng)從容量有限的 YYMemoryCache 中淘汰掉)才會(huì)去從 YYDiskCache 訪問(wèn),如果訪問(wèn)到(表示之前確實(shí)使用該 key 緩存過(guò)對(duì)象,該對(duì)象已經(jīng)從容量有限的 YYMemoryCache 中淘汰掉成立)會(huì)先在 YYMemoryCache 中更新一次該緩存對(duì)象的訪問(wèn)信息之后才返回給接口。
2>線程安全
如果說(shuō) YYCache 這個(gè)類是一個(gè)純邏輯層的緩存類(指 YYCache 的接口實(shí)現(xiàn)全部是調(diào)用其他類完成),那么 YYMemoryCache 與 YYDiskCache 還是做了一些事情的(并沒(méi)有 YYCache 當(dāng)甩手掌柜那么輕松),其中最顯而易見的就是 YYMemoryCache 與 YYDiskCache 為 YYCache 保證了線程安全。
YYMemoryCache 使用了pthread_mutex線程鎖來(lái)確保線程安全,而 YYDiskCache 則選擇了更適合它的dispatch_semaphore,上文已經(jīng)給出了作者選擇這些鎖的原因。
3>緩存控制
YYCache 提供了三種控制維度,分別是:cost、count、age。這已經(jīng)滿足了絕大多數(shù)開發(fā)者的需求,我們?cè)谧约涸O(shè)計(jì)緩存時(shí)也可以根據(jù)自己的使用環(huán)境提供合適的控制方式。
4>緩存替換策略
在上文解析 YYCache 源碼的時(shí)候,介紹了緩存替換策略的概念并且列舉了很多經(jīng)典的策略。YYCache 使用了雙向鏈表(_YYLinkedMapNode與_YYLinkedMap)實(shí)現(xiàn)了 LRU(least-recently-used) 策略,旨在提高 YYCache 的緩存命中率。
5>緩存命中率
這一概念是在上文解析_YYLinkedMapNode與_YYLinkedMap小節(jié)介紹的,我們?cè)谧约涸O(shè)計(jì)緩存時(shí)不一定非要使用 LRU 策略,可以根據(jù)我們的實(shí)際使用環(huán)境選擇最適合我們自己的緩存替換策略。
6>性能
其實(shí)性能這個(gè)東西是隱而不見的,又是到處可見的(笑)。它從我們最開始設(shè)計(jì)一個(gè)緩存架構(gòu)時(shí)就被帶入,一直到我們具體的實(shí)現(xiàn)細(xì)節(jié)中慢慢成形,最后成為了我們?cè)O(shè)計(jì)出來(lái)的緩存優(yōu)秀與否的決定性因素。
上文中剖析了太多 YYCache 中對(duì)于性能提升的實(shí)現(xiàn)細(xì)節(jié):
異步釋放緩存對(duì)象
鎖的選擇
使用 NSMapTable 單例管理的 YYDiskCache
YYKVStorage 中的_dbStmtCache
甚至使用 CoreFoundation 來(lái)?yè)Q取微乎其微的性能提升
讀完YYCache源碼看到這個(gè)還不錯(cuò),就摘下來(lái)了