YYCache源碼總結(jié)

YYCache簡(jiǎn)介


YYCache的結(jié)構(gòu)

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予警告


簡(jiǎn)化版YYCache的代碼

從代碼中我們可以看到 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。


性能測(cè)試結(jié)果

上面是 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)了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容