從 YYCache 源碼 Get 到如何設(shè)計(jì)一個(gè)優(yōu)秀的緩存
來(lái)源:Lision
前言
iOS 開(kāi)發(fā)中總會(huì)用到各種緩存译仗,但是各位有沒(méi)有考慮過(guò)什么樣的緩存才能被叫做優(yōu)秀的緩存,或者說(shuō)優(yōu)秀的緩存應(yīng)該具備哪些特質(zhì)柬采?
閉上眼睛户侥,想一想如果面試官讓你設(shè)計(jì)一個(gè)緩存你會(huì)怎么回答?
本文將結(jié)合 YYCache 的源碼逐步帶大家找到答案赌厅。
YYCache 是一個(gè)線(xiàn)程安全的高性能鍵值緩存(該項(xiàng)目是 YYKit 組件之一)。YYKit 是在 2015 年發(fā)布到 Github 的,由于其代碼質(zhì)量很高杉辙,在短時(shí)間內(nèi)就收獲了大量的 Star(目前已經(jīng) 1w+ Star 了),而且在 iOS 各大社區(qū)反響廣泛捶朵,Google 一下也是漫天贊嘆蜘矢。
YYKit 作者是 @ibireme狂男,原名郭曜源(猜測(cè) YY 前綴來(lái)源于曜源?)品腹,是我個(gè)人非常喜歡的國(guó)人開(kāi)發(fā)者(何止喜歡岖食,簡(jiǎn)直是迷弟)。
YYCache 的代碼邏輯清晰舞吭,注釋詳盡泡垃,加上自身不算太大的代碼量使得其閱讀非常簡(jiǎn)單,更加難能可貴的是它的性能還非常高羡鸥。
我對(duì)它的評(píng)價(jià)是小而美蔑穴,這種小而美的緩存源碼對(duì)于我們今天的主題太合適不過(guò)了(本文中 YYCache 源碼版本為 v1.0.4)。
索引
YYCache 簡(jiǎn)介
YYMemoryCache 細(xì)節(jié)剖析
YYDiskCache 細(xì)節(jié)剖析
優(yōu)秀的緩存應(yīng)該具備哪些特質(zhì)
總結(jié)
YYCache 簡(jiǎn)介
簡(jiǎn)單把 YYCache 從頭到尾擼了一遍惧浴,最大的感觸就是代碼風(fēng)格干凈整潔存和,代碼思路清晰明了。
由于代碼整體閱讀難度不是非常大衷旅,本文不會(huì)去逐字逐句的解讀源碼捐腿,而是提煉 YYCache 作為一個(gè)小而美的緩存實(shí)現(xiàn)了哪些緩存該具備的特質(zhì),并且分析實(shí)現(xiàn)細(xì)節(jié)柿顶。
我們先來(lái)簡(jiǎn)單看一下 YYCache 的代碼結(jié)構(gòu)叙量,YYCache 是由 YYMemoryCache 與 YYDiskCache 兩部分組成的,其中 YYMemoryCache 作為高速內(nèi)存緩存九串,而 YYDiskCache 則作為低速磁盤(pán)緩存绞佩。
通常一個(gè)緩存是由內(nèi)存緩存和磁盤(pán)緩存組成,內(nèi)存緩存提供容量小但高速的存取功能猪钮,磁盤(pán)緩存提供大容量但低速的持久化存儲(chǔ)品山。
@``interface
YYCache : NSObject
@property (copy, readonly) NSString *name;
@property (strong, readonly) YYMemoryCache *memoryCache;
@property (strong, readonly) YYDiskCache *diskCache;
- (BOOL)containsObjectForKey:(NSString *)key;
- (nullable id)objectForKey:(NSString *)key;
- (``void``)setObject:(nullable id)object forKey:(NSString *)key;
- (``void``)removeObjectForKey:(NSString *)key;
@end
|
上面的代碼我做了簡(jiǎn)化,只保留了最基本的代碼(我認(rèn)為作者在最初設(shè)計(jì) YYCache 雛形時(shí)很可能也只是提供了這些基本的接口)烤低,其他的接口只是通過(guò)調(diào)用基本的接口再附加對(duì)應(yīng)處理代碼而成肘交。
Note: 其實(shí)源碼中作者用了一些技巧性的宏,例如 NS_ASSUME_NONNULL_BEGIN 與 NS_ASSUME_NONNULL_END 來(lái)通過(guò)編譯器層檢測(cè)入?yún)⑹欠駷榭詹⒔o予警告扑馁,參見(jiàn) Nullability and Objective-C涯呻。
類(lèi)似上述的編碼技巧還有很多,我并非不想與大家分享我 get 到的這些編碼技巧腻要,只是覺(jué)得它與本文的主題似乎不太相符复罐。我準(zhǔn)備在之后專(zhuān)門(mén)寫(xiě)一篇文章來(lái)與大家分享我在閱讀各大源碼庫(kù)過(guò)程中 get 到的編碼技巧(感興趣的話(huà)可以 關(guān)注我)。
從代碼中我們可以看到 YYCache 中持有 YYMemoryCache 與 YYDiskCache雄家,并且對(duì)外提供了一些接口效诅。這些接口基本都是基于 Key 和 Value 設(shè)計(jì)的,類(lèi)似于 iOS 原生的字典類(lèi)接口(增刪改查)。
YYMemoryCache 細(xì)節(jié)剖析
YYMemoryCache 是一個(gè)高速的內(nèi)存緩存乱投,用于存儲(chǔ)鍵值對(duì)咽笼。它與 NSDictionary 相反,Key 被保留并且不復(fù)制戚炫。API 和性能類(lèi)似于 NSCache剑刑,所有方法都是線(xiàn)程安全的。
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))抖苦。
@``interface
YYMemoryCache : NSObject
#pragma mark - Attribute
@property (nullable, copy) NSString *name; ``// 緩存名稱(chēng),默認(rèn)為 nil
@property (readonly) NSUInteger totalCount; ``// 緩存對(duì)象總數(shù)
@property (readonly) NSUInteger totalCost; ``// 緩存對(duì)象總開(kāi)銷(xiāo)
#pragma mark - Limit
@property NSUInteger countLimit; ``// 緩存對(duì)象數(shù)量限制米死,默認(rèn)無(wú)限制锌历,超過(guò)限制則會(huì)在后臺(tái)逐出一些對(duì)象以滿(mǎn)足限制
@property NSUInteger costLimit; ``// 緩存開(kāi)銷(xiāo)數(shù)量限制,默認(rèn)無(wú)限制峦筒,超過(guò)限制則會(huì)在后臺(tái)逐出一些對(duì)象以滿(mǎn)足限制
@property NSTimeInterval ageLimit; ``// 緩存時(shí)間限制究西,默認(rèn)無(wú)限制,超過(guò)限制則會(huì)在后臺(tái)逐出一些對(duì)象以滿(mǎn)足限制
@property NSTimeInterval autoTrimInterval; ``// 緩存自動(dòng)清理時(shí)間間隔物喷,默認(rèn) 5s
@property BOOL shouldRemoveAllObjectsOnMemoryWarning; ``// 是否應(yīng)該在收到內(nèi)存警告時(shí)刪除所有緩存內(nèi)對(duì)象
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground; ``// 是否應(yīng)該在 App 進(jìn)入后臺(tái)時(shí)刪除所有緩存內(nèi)對(duì)象
@property (nullable, copy) ``void``(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache); ``// 我認(rèn)為這是一個(gè) hook卤材,便于我們?cè)谑盏絻?nèi)存警告時(shí)自定義處理緩存
@property (nullable, copy) ``void``(^didEnterBackgroundBlock)(YYMemoryCache *cache); ``// 我認(rèn)為這是一個(gè) hook,便于我們?cè)谑盏?App 進(jìn)入后臺(tái)時(shí)自定義處理緩存
@property BOOL releaseOnMainThread; ``// 是否在主線(xiàn)程釋放對(duì)象峦失,默認(rèn) NO扇丛,有些對(duì)象(例如 UIView/CALayer)應(yīng)該在主線(xiàn)程釋放
@property BOOL releaseAsynchronously; ``// 是否異步釋放對(duì)象,默認(rèn) YES
- (BOOL)containsObjectForKey:(id)key;
- (nullable id)objectForKey:(id)key;
- (``void``)setObject:(nullable id)object forKey:(id)key;
- (``void``)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
- (``void``)removeObjectForKey:(id)key;
- (``void``)removeAllObjects;
#pragma mark - Trim
- (``void``)trimToCount:(NSUInteger)count; ``// 用 LRU 算法刪除對(duì)象尉辑,直到 totalCount <= count
- (``void``)trimToCost:(NSUInteger)cost; ``// 用 LRU 算法刪除對(duì)象帆精,直到 totalCost <= cost
- (``void``)trimToAge:(NSTimeInterval)age; ``// 用 LRU 算法刪除對(duì)象,直到所有到期對(duì)象全部被刪除
@end
|
YYMemoryCache 的定義代碼比較簡(jiǎn)單~ 該有的注釋我已經(jīng)加到了上面隧魄,這里 LRU 算法的實(shí)現(xiàn)我準(zhǔn)備單獨(dú)拎出來(lái)放到后面和(_YYLinkedMapNode 與 _YYLinkedMap)一起講卓练。我們這里只需要再關(guān)注一下 YYMemoryCache 是如何做到線(xiàn)程安全的。
YYMemoryCache 是如何做到線(xiàn)程安全的
@implementation YYMemoryCache {
pthread_mutex_t _lock; ``// 線(xiàn)程鎖购啄,旨在保證 YYMemoryCache 線(xiàn)程安全
_YYLinkedMap *_lru; ``// _YYLinkedMap襟企,YYMemoryCache 通過(guò)它間接操作緩存對(duì)象
dispatch_queue_t _queue; ``// 串行隊(duì)列,用于 YYMemoryCache 的 trim 操作
}
|
沒(méi)錯(cuò)闸溃,這里 ibireme 選擇使用 pthread_mutex 線(xiàn)程鎖來(lái)確保 YYMemoryCache 的線(xiàn)程安全整吆。
有趣的是拱撵,這里 ibireme 使用 pthread_mutex 是有一段小故事的辉川。在最初 YYMemoryCache 這里使用的鎖是 OSSpinLock 自旋鎖(詳見(jiàn) YYCache 設(shè)計(jì)思路 備注-關(guān)于鎖)表蝙,后面有人在 Github 向作者提 issue 反饋 OSSpinLock 不安全,經(jīng)過(guò)作者的確認(rèn)(詳見(jiàn) 不再安全的 OSSpinLock)最后選擇用 pthread_mutex 替代 OSSpinLock乓旗。
上面是 ibireme 在確認(rèn) OSSpinLock 不再安全之后為了尋找替代方案做的簡(jiǎn)單性能測(cè)試府蛇,對(duì)比了一下幾種能夠替代 OSSpinLock 鎖的性能。在 不再安全的 OSSpinLock 文末的評(píng)論中屿愚,我找到了作者使用 pthread_mutex 的原因汇跨。
ibireme: 蘋(píng)果員工說(shuō) libobjc 里 spinlock 是用了一些私有方法 (mach_thread_switch),貢獻(xiàn)出了高線(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
上文介紹了 YYMemoryCache,其實(shí) YYMemoryCache 并不直接操作緩存對(duì)象结啼,而是通過(guò)內(nèi)部的 _YYLinkedMapNode 與 _YYLinkedMap 來(lái)間接的操作緩存對(duì)象掠剑。這兩個(gè)類(lèi)對(duì)于上文中提到的 LRU 緩存算法的理解至關(guān)重要,所以我把他們倆單獨(dú)拎出來(lái)放在這里詳細(xì)解讀一下郊愧。
/**
_YYLinkedMap 中的一個(gè)節(jié)點(diǎn)朴译。
通常情況下我們不應(yīng)該使用這個(gè)類(lèi)。
*/
@``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; ``// 記錄開(kāi)銷(xiāo),對(duì)應(yīng) YYMemoryCache 提供的 cost 控制
NSTimeInterval _time; ``// 記錄時(shí)間红选,對(duì)應(yīng) YYMemoryCache 提供的 age 控制
}
@end
/**
YYMemoryCache 內(nèi)的一個(gè)鏈表澜公。
_YYLinkedMap 不是一個(gè)線(xiàn)程安全的類(lèi),而且它也不對(duì)參數(shù)做校驗(yàn)喇肋。
通常情況下我們不應(yīng)該使用這個(gè)類(lèi)坟乾。
*/
@``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
}
// 鏈表操作甚侣,看接口名稱(chēng)應(yīng)該不需要注釋吧~
- (``void``)insertNodeAtHead:(_YYLinkedMapNode *)node;
- (``void``)bringNodeToHead:(_YYLinkedMapNode *)node;
- (``void``)removeNode:(_YYLinkedMapNode *)node;
- (_YYLinkedMapNode *)removeTailNode;
- (``void``)removeAll;
@end
|
為了方便大家閱讀,我標(biāo)注了必要的中文注釋间学。其實(shí)對(duì)數(shù)據(jù)結(jié)構(gòu)與算法不陌生的同學(xué)應(yīng)該一眼就看的出來(lái) _YYLinkedMapNode 與 _YYLinkedMap 這倆貨的本質(zhì)殷费。沒(méi)錯(cuò)印荔,丫就是雙向鏈表節(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 快速定位用戶(hù)要訪(fǎng)問(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)
上文我們認(rèn)清了 _YYLinkedMap 與 _YYLinkedMapNode 本質(zhì)上就是雙向鏈表和鏈表節(jié)點(diǎn),這里我們簡(jiǎn)單講一下 YYMemoryCache 是如何利用雙向鏈表實(shí)現(xiàn) LRU(least-recently-used) 算法的葡粒。
緩存替換策略
首先 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
是不是被唬到了?不要擔(dān)心塔鳍,我這里會(huì)表述的盡量易懂伯铣。
緩存命中率
為什么有這么多緩存替換策略,或者說(shuō)搞這么多名堂究竟是為了什么呢轮纫?
答案是提高緩存命中率腔寡,那么何謂緩存命中率呢?
Google 一下自然是有不少解釋?zhuān)贿^(guò)很多都是 web 相關(guān)的掌唾,而且不說(shuō)人話(huà)(很難理解)放前,我個(gè)人非常討厭各種不說(shuō)人話(huà)的“高深”抽象概念。
這里抖了好幾抖膽才敢談一下我對(duì)于緩存命中率的理解(限于 YYCache 和 iOS 開(kāi)發(fā))糯彬。
緩存命中 = 用戶(hù)要訪(fǎng)問(wèn)的緩存對(duì)象在高速緩存中凭语,我們直接在高速緩存中通過(guò) Hash 將其找到并返回給用戶(hù)。
緩存命中率 = 用戶(hù)要訪(fǎng)問(wèn)的緩存對(duì)象在高速緩存中被我們?cè)L問(wèn)到的概率撩扒。
既然談到了自己的理解似扔,我索性說(shuō)個(gè)夠。
- 緩存丟失 = 由于高速緩存數(shù)量有限(占據(jù)內(nèi)存等原因)搓谆,所以用戶(hù)要訪(fǎng)問(wèn)的緩存對(duì)象很有可能被我們從有限的高速緩存中淘汰掉了炒辉,我們可能會(huì)將其存儲(chǔ)于低速的磁盤(pán)緩存中(如果磁盤(pán)緩存還有資源的話(huà)),那么就要從磁盤(pán)緩存中獲取該緩存對(duì)象以返回給用戶(hù)泉手,這種情況我理解為(高速)緩存未命中黔寇,即緩存丟失(并不是真的被我們丟掉了,但肯定是被我們從高速緩存淘汰掉了)斩萌。
緩存命中是 cache-hit缝裤,那么如果你玩游戲屏轰,可以理解為這次 hit miss 了(笑,有人找我開(kāi)黑嗎)憋飞。
LRU
首先來(lái)講一下 LRU 的概念讓大家有一個(gè)基本的認(rèn)識(shí)霎苗。LRU(least-recently-used) 翻譯過(guò)來(lái)是“最近使用”,顧名思義這種緩存替換策略是基于用戶(hù)最近訪(fǎng)問(wèn)過(guò)的緩存對(duì)象而建立搀崭。
我認(rèn)為 LRU 緩存替換策略的核心思想在于:LRU 認(rèn)為用戶(hù)最新使用(訪(fǎng)問(wèn))過(guò)的緩存對(duì)象為高頻緩存對(duì)象叨粘,即用戶(hù)很可能還會(huì)再次使用(訪(fǎng)問(wèn))該緩存對(duì)象猾编;而反之瘤睹,用戶(hù)很久之前使用(訪(fǎng)問(wèn))過(guò)的緩存對(duì)象(期間一直沒(méi)有再次訪(fǎng)問(wèn))為低頻緩存對(duì)象,即用戶(hù)很可能不會(huì)再去使用(訪(fǎng)問(wèn))該緩存對(duì)象答倡,通常在資源不足時(shí)會(huì)先去釋放低頻緩存對(duì)象轰传。
_YYLinkedMapNode 與 _YYLinkedMap 實(shí)現(xiàn) LRU
YYCache 作者通過(guò) _YYLinkedMapNode 與 _YYLinkedMap 雙向鏈表實(shí)現(xiàn) LRU 緩存替換策略的思路其實(shí)很簡(jiǎn)捷清晰,我們一步一步來(lái)看瘪撇。
雙向鏈表中有頭結(jié)點(diǎn)和尾節(jié)點(diǎn):
頭結(jié)點(diǎn) = 鏈表中用戶(hù)最近一次使用(訪(fǎng)問(wèn))的緩存對(duì)象節(jié)點(diǎn)获茬,MRU。
尾節(jié)點(diǎn) = 鏈表中用戶(hù)已經(jīng)很久沒(méi)有再次使用(訪(fǎng)問(wèn))的緩存對(duì)象節(jié)點(diǎn)倔既,LRU恕曲。
如何讓頭結(jié)點(diǎn)和尾節(jié)點(diǎn)指向我們想指向的緩存對(duì)象節(jié)點(diǎn)?我們結(jié)合代碼來(lái)看:
- 在用戶(hù)使用(訪(fǎng)問(wèn))時(shí)更新緩存節(jié)點(diǎn)信息渤涌,并將其移動(dòng)至雙向鏈表頭結(jié)點(diǎn)佩谣。
- (id)objectForKey:(id)key {
// 判斷入?yún)?/code>
if
(!key) ``return
nil;
pthread_mutex_lock(&_lock);
// 找到對(duì)應(yīng)緩存節(jié)點(diǎn)
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge ``const
void
*)(key));
if
(node) {
// 更新緩存節(jié)點(diǎn)時(shí)間,并將其移動(dòng)至雙向鏈表頭結(jié)點(diǎn)
node->_time = CACurrentMediaTime();
[_lru bringNodeToHead:node];
}
pthread_mutex_unlock(&_lock);
// 返回找到的緩存節(jié)點(diǎn) value
return
node ? node->_value : nil;
}
- 在用戶(hù)設(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)并插入鏈表表頭安皱。
- (``void``)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
// 判斷入?yún)⒌鼢蓿÷?/code>
...
pthread_mutex_lock(&_lock);
// 判斷入?yún)?key 對(duì)應(yīng)的緩存對(duì)象節(jié)點(diǎn)是否存在
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge ``const
void
*)(key));
NSTimeInterval now = CACurrentMediaTime();
if
(node) {
// 存在則更新緩存對(duì)象節(jié)點(diǎn)并將節(jié)點(diǎn)移動(dòng)至鏈表頭結(jié)點(diǎn)
_lru->_totalCost -= node->_cost;
_lru->_totalCost += cost;
node->_cost = cost;
node->_time = now;
node->_value = object;
[_lru bringNodeToHead:node];
} ``else
{
// 不存在則根據(jù)入?yún)⑸尚碌木彺鎸?duì)象節(jié)點(diǎn)并插入鏈表表頭
node = [_YYLinkedMapNode ``new``];
node->_cost = cost;
node->_time = now;
node->_key = key;
node->_value = object;
[_lru insertNodeAtHead:node];
}
// 判斷插入、更新節(jié)點(diǎn)之后是否超過(guò)了限制 cost酌伊、count腾窝,如果超過(guò)則 trim,省略
...
pthread_mutex_unlock(&_lock);
}
|
- 在資源不足時(shí)居砖,從雙線(xiàn)鏈表的尾節(jié)點(diǎn)(LRU)開(kāi)始清理緩存虹脯,釋放資源。
// 這里拿 count 資源舉例悯蝉,cost归形、age 自己舉一反三
- (``void``)_trimToCount:(NSUInteger)countLimit {
// 判斷 countLimit 為 0,則全部清空緩存鼻由,省略
// 判斷 _lru->_totalCount <= countLimit暇榴,沒(méi)有超出資源限制則不作處理厚棵,省略
...
NSMutableArray *holder = [NSMutableArray ``new``];
while
(!finish) {
if
(pthread_mutex_trylock(&_lock) == ``0``) {
if
(_lru->_totalCount > countLimit) {
// 從雙線(xiàn)鏈表的尾節(jié)點(diǎn)(LRU)開(kāi)始清理緩存,釋放資源
_YYLinkedMapNode *node = [_lru removeTailNode];
if
(node) [holder addObject:node];
} ``else
{
finish = YES;
}
pthread_mutex_unlock(&_lock);
} ``else
{
// 使用 usleep 以微秒為單位掛起線(xiàn)程蔼紧,在短時(shí)間間隔掛起線(xiàn)程
// 對(duì)比 sleep 用 usleep 能更好的利用 CPU 時(shí)間
usleep(``10
* ``1000``); ``//10 ms
}
}
// 判斷是否需要在主線(xiàn)程釋放婆硬,采取釋放緩存對(duì)象操作
if
(holder.count) {
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
// 異步釋放,我們單獨(dú)拎出來(lái)講
[holder count]; ``// release in queue
});
}
}
嘛~ 是不是感覺(jué)敲簡(jiǎn)單奸例?上面代碼去掉了可能會(huì)分散大家注意力的代碼彬犯,我們這里僅僅討論 LRU 的實(shí)現(xiàn),其余部分的具體實(shí)現(xiàn)源碼也非常簡(jiǎn)單查吊,我覺(jué)得沒(méi)必要貼出來(lái)單獨(dú)講解谐区,感興趣的同學(xué)可以自己去 YYCache 下載源碼查閱。
異步釋放技巧
關(guān)于上面的異步釋放緩存對(duì)象的代碼逻卖,我覺(jué)得還是有必要單獨(dú)拎出來(lái)講一下的:
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
dispatch_async(queue, ^{
// 異步釋放宋列,我們單獨(dú)拎出來(lái)講
[holder count]; ``// release in queue
});
這個(gè)技巧 ibireme 在他的另一篇文章 iOS 保持界面流暢的技巧 中有提及:
Note: 對(duì)象的銷(xiāo)毀雖然消耗資源不多,但累積起來(lái)也是不容忽視的。通常當(dāng)容器類(lèi)持有大量對(duì)象時(shí),其銷(xiāo)毀時(shí)的資源消耗就非常明顯蜗字。同樣的,如果對(duì)象可以放到后臺(tái)線(xiàn)程去釋放坤邪,那就挪到后臺(tái)線(xiàn)程去。這里有個(gè)小 Tip:把對(duì)象捕獲到 block 中罚缕,然后扔到后臺(tái)隊(duì)列去隨便發(fā)送個(gè)消息以避免編譯器警告艇纺,就可以讓對(duì)象在后臺(tái)線(xiàn)程銷(xiāo)毀了。
而上面代碼中的 YYMemoryCacheGetReleaseQueue 這個(gè)隊(duì)列源碼為:
// 靜態(tài)內(nèi)聯(lián) dispatch_queue_t
static
inline dispatch_queue_t YYMemoryCacheGetReleaseQueue() {
return
dispatch_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è)線(xiàn)程安全的磁盤(pán)緩存,用于存儲(chǔ)由 SQLite 和文件系統(tǒng)支持的鍵值對(duì)(類(lèi)似于 NSURLCache 的磁盤(pán)緩存)肠鲫。
YYDiskCache 具有以下功能:
它使用 LRU(least-recently-used) 來(lái)刪除對(duì)象员帮。
支持按 cost,count 和 age 進(jìn)行控制导饲。
它可以被配置為當(dāng)沒(méi)有可用的磁盤(pán)空間時(shí)自動(dòng)驅(qū)逐緩存對(duì)象捞高。
它可以自動(dòng)抉擇每個(gè)緩存對(duì)象的存儲(chǔ)類(lèi)型(sqlite/file)以便提供更好的性能表現(xiàn)。
Note: 您可以編譯最新版本的 sqlite 并忽略 iOS 系統(tǒng)中的 libsqlite3.dylib 來(lái)獲得 2x?4x 的速度提升渣锦。
@``interface
YYDiskCache : NSObject
#pragma mark - Attribute
@property (nullable, copy) NSString *name; ``// 緩存名稱(chēng)硝岗,默認(rèn)為 nil
@property (readonly) NSString *path; ``// 緩存路徑
@property (readonly) NSUInteger inlineThreshold; ``// 閾值,大于閾值則存儲(chǔ)類(lèi)型為 file袋毙;否則存儲(chǔ)類(lèi)型為 sqlite
@property (nullable, copy) NSData *(^customArchiveBlock)(id object); ``// 用來(lái)替換 NSKeyedArchiver型檀,你可以使用該代碼塊以支持沒(méi)有 conform
NSCoding協(xié)議的對(duì)象
@property (nullable, copy) id (^customUnarchiveBlock)(NSData *data); ``// 用來(lái)替換 NSKeyedUnarchiver,你可以使用該代碼塊以支持沒(méi)有 conform
NSCoding協(xié)議的對(duì)象
@property (nullable, copy) NSString *(^customFileNameBlock)(NSString *key); ``// 當(dāng)一個(gè)對(duì)象將以 file 的形式保存時(shí)听盖,該代碼塊用來(lái)生成指定文件名胀溺。如果為 nil裂七,則默認(rèn)使用 md5(key) 作為文件名
#pragma mark - Limit
@property NSUInteger countLimit; ``// 緩存對(duì)象數(shù)量限制,默認(rèn)無(wú)限制仓坞,超過(guò)限制則會(huì)在后臺(tái)逐出一些對(duì)象以滿(mǎn)足限制
@property NSUInteger costLimit; ``// 緩存開(kāi)銷(xiāo)數(shù)量限制背零,默認(rèn)無(wú)限制,超過(guò)限制則會(huì)在后臺(tái)逐出一些對(duì)象以滿(mǎn)足限制
@property NSTimeInterval ageLimit; ``// 緩存時(shí)間限制无埃,默認(rèn)無(wú)限制徙瓶,超過(guò)限制則會(huì)在后臺(tái)逐出一些對(duì)象以滿(mǎn)足限制
@property NSUInteger freeDiskSpaceLimit; ``// 緩存應(yīng)該保留的最小可用磁盤(pán)空間(以字節(jié)為單位),默認(rèn)無(wú)限制嫉称,超過(guò)限制則會(huì)在后臺(tái)逐出一些對(duì)象以滿(mǎn)足限制
@property NSTimeInterval autoTrimInterval; ``// 緩存自動(dòng)清理時(shí)間間隔侦镇,默認(rèn) 60s
@property BOOL errorLogsEnabled; ``// 是否開(kāi)啟錯(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 結(jié)構(gòu)與 YYMemoryCache 類(lèi)似,由于很多接口都是基于基本的接口做了擴(kuò)展所得澎埠,這里貼的代碼省略了一些接口虽缕。代碼還是一如既往的干凈簡(jiǎn)潔,相信各位都能看懂蒲稳。
YYDiskCache 是基于 sqlite 和 file 來(lái)做的磁盤(pán)緩存,我們的緩存對(duì)象可以自由的選擇存儲(chǔ)類(lèi)型伍派,下面簡(jiǎn)單對(duì)比一下:
sqlite: 對(duì)于小數(shù)據(jù)(例如 NSNumber)的存取效率明顯高于 file江耀。
file: 對(duì)于較大數(shù)據(jù)(例如高質(zhì)量圖片)的存取效率優(yōu)于 sqlite。
所以 YYDiskCache 使用兩者配合诉植,靈活的存儲(chǔ)以提高性能祥国。
NSMapTable
NSMapTable 是類(lèi)似于字典的集合,但具有更廣泛的可用內(nèi)存語(yǔ)義晾腔。NSMapTable 是 iOS6 之后引入的類(lèi)舌稀,它基于 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ú)法正確初始化。
更多信息詳見(jiàn) NSMapTable 官方文檔疫赎。
需要特殊說(shuō)明的是盛撑,YYDiskCache 內(nèi)部是基于一個(gè)單例 NSMapTable 管理的,這點(diǎn)有別于 YYMemoryCache捧搞。
static
NSMapTable *_globalInstances; ``// 引用管理所有的 YYDiskCache 實(shí)例
static
dispatch_semaphore_t _globalInstancesLock; ``// YYDiskCache 使用 dispatch_semaphore 保障 NSMapTable 線(xiàn)程安全
static
void
_YYDiskCacheInitGlobal() {
static
dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_globalInstancesLock = dispatch_semaphore_create(``1``);
_globalInstances = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:``0``];
});
}
static
YYDiskCache *_YYDiskCacheGetGlobal(NSString *path) {
if
(path.length == ``0``) ``return
nil;
_YYDiskCacheInitGlobal();
dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
id cache = [_globalInstances objectForKey:path];
dispatch_semaphore_signal(_globalInstancesLock);
return
cache;
}
static
void
_YYDiskCacheSetGlobal(YYDiskCache *cache) {
if
(cache.path.length == ``0``) ``return``;
_YYDiskCacheInitGlobal();
dispatch_semaphore_wait(_globalInstancesLock, DISPATCH_TIME_FOREVER);
[_globalInstances setObject:cache forKey:cache.path];
dispatch_semaphore_signal(_globalInstancesLock);
}
每當(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;
// 沒(méi)有獲取到則初始化一個(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)省略
...
// 開(kāi)啟遞歸清理爵憎,會(huì)根據(jù) _autoTrimInterval 對(duì) YYDiskCache trim
[self _trimRecursively];
// 向 NSMapTable 單例注冊(cè)新生成的 YYDiskCache 實(shí)例
_YYDiskCacheSetGlobal(self);
// App 生命周期通知相關(guān)代碼慨亲,省略
...
return
self;
}
我在 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ì)磁盤(pán)緩存來(lái)說(shuō)碍舍,它比較合適。
YYKVStorageItem 與 YYKVStorage
剛才在 YYDiskCache 的初始化源碼中邑雅,我們不難發(fā)現(xiàn)一個(gè)類(lèi) YYKVStorage片橡。與 YYMemoryCache 相對(duì)應(yīng)的,YYDiskCache 也不會(huì)直接操作緩存對(duì)象(sqlite/file)蒂阱,而是通過(guò) YYKVStorage 來(lái)間接的操作緩存對(duì)象锻全。
從這一點(diǎn)上不難發(fā)現(xiàn),YYKVStorage 等價(jià)于 YYMemoryCache 中的雙向鏈表 _YYLinkedMap录煤,而對(duì)應(yīng)于 _YYLinkedMap 中的節(jié)點(diǎn) _YYLinkedMapNode鳄厌,YYKVStorage 中也有一個(gè)類(lèi) YYKVStorageItem 充當(dāng)著與緩存對(duì)象一對(duì)一的角色。
// YYKVStorageItem 是 YYKVStorage 中用來(lái)存儲(chǔ)鍵值對(duì)和元數(shù)據(jù)的類(lèi)
// 通常情況下妈踊,我們不應(yīng)該直接使用這個(gè)類(lèi)
@``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 和文件系統(tǒng)的鍵值存儲(chǔ)了嚎。
通常情況下,我們不應(yīng)該直接使用這個(gè)類(lèi)。
@warning
這個(gè)類(lèi)的實(shí)例是 *非* 線(xiàn)程安全的歪泳,你需要確保
只有一個(gè)線(xiàn)程可以同時(shí)訪(fǎng)問(wèn)該實(shí)例萝勤。如果你真的
需要在多線(xiàn)程中處理大量的數(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 類(lèi)型
@property (nonatomic) BOOL errorLogsEnabled; ``/// 是否開(kāi)啟錯(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
代碼美哭了有木有5凶俊?這種代碼根本不需要翻譯伶氢,我覺(jué)得相比于逐行的翻譯趟径,直接看代碼更舒服。這里我們只需要看一下 YYKVStorageType 這個(gè)枚舉癣防,他決定著 YYKVStorage 的存儲(chǔ)類(lèi)型蜗巧。
YYKVStorageType
/**
存儲(chǔ)類(lèi)型,指示“YYKVStorageItem.value”存儲(chǔ)在哪里蕾盯。
@discussion
通常幕屹,將數(shù)據(jù)寫(xiě)入 sqlite 比外部文件更快,但是
讀取性能取決于數(shù)據(jù)大小级遭。在我的測(cè)試(環(huán)境 iPhone 6 64G)望拖,
當(dāng)數(shù)據(jù)較大(超過(guò) 20KB)時(shí)從外部文件讀取數(shù)據(jù)比 sqlite 更快。
*/
typedef NS_ENUM(NSUInteger, YYKVStorageType) {
YYKVStorageTypeFile = ``0``, ``// value 以文件的形式存儲(chǔ)于文件系統(tǒng)
YYKVStorageTypeSQLite = ``1``, ``// value 以二進(jìn)制形式存儲(chǔ)于 sqlite
YYKVStorageTypeMixed = ``2``, ``// value 將根據(jù)你的選擇基于上面兩種形式混合存儲(chǔ)
};
在 YYKVStorageType 的注釋中標(biāo)記了作者寫(xiě) YYCache 時(shí)做出的測(cè)試結(jié)論装畅,大家也可以基于自己的環(huán)境去測(cè)試驗(yàn)證作者的說(shuō)法(這一點(diǎn)是可以討論的靠娱,我們可以根據(jù)自己的測(cè)試來(lái)設(shè)置 YYDiskCache 中的 inlineThreshold 閾值)。
如果想要了解更多的信息可以點(diǎn)擊 Internal Versus External BLOBs in SQLite 查閱 SQLite 官方文檔掠兄。
YYKVStorage 性能優(yōu)化細(xì)節(jié)
上文說(shuō)到 YYKVStorage 可以基于 SQLite 和文件系統(tǒng)做磁盤(pán)存儲(chǔ),這里再提一些我閱讀源碼發(fā)現(xiàn)到的有趣細(xì)節(jié):
@implementation YYKVStorage {
...
CFMutableDictionaryRef _dbStmtCache; ``// 焦點(diǎn)集中在這里
...
}
可以看到 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) {
// 如果沒(méi)有緩存再?gòu)男律梢粋€(gè) sqlite3_stmt
int
result = sqlite3_prepare_v2(_db, sql.UTF8String, -``1``, &stmt, NULL);
// 生成結(jié)果異常則根據(jù)錯(cuò)誤日志開(kāi)啟標(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 的開(kāi)銷(xiāo)。
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ì)
嘛~ 我們回到文章最初提到的問(wèn)題,優(yōu)秀的緩存應(yīng)該具備哪些特質(zhì)惩歉?
如果跟著文章一步步讀到這里等脂,相信很容易舉出以下幾點(diǎn):
內(nèi)存緩存和磁盤(pán)緩存
線(xiàn)程安全
緩存控制
緩存替換策略
緩存命中率
性能
我們簡(jiǎn)單的總結(jié)一下 YYCache 源碼中是如何體現(xiàn)這些特質(zhì)的。
內(nèi)存緩存和磁盤(pán)緩存
YYCache 是由內(nèi)存緩存 YYMemoryCache 與磁盤(pán)緩存 YYDiskCache 相互配合組成的撑蚌,內(nèi)存緩存提供容量小但高速的存取功能上遥,磁盤(pán)緩存提供大容量但低速的持久化存儲(chǔ)。這樣的設(shè)計(jì)支持用戶(hù)在緩存不同對(duì)象時(shí)都能夠有很好的體驗(yàn)争涌。
在 YYCache 中使用接口訪(fǎng)問(wèn)緩存對(duì)象時(shí)粉楚,會(huì)先去嘗試從內(nèi)存緩存 YYMemoryCache 中訪(fǎng)問(wèn),如果訪(fǎng)問(wèn)不到(沒(méi)有使用該 key 緩存過(guò)對(duì)象或者該對(duì)象已經(jīng)從容量有限的 YYMemoryCache 中淘汰掉)才會(huì)去從 YYDiskCache 訪(fǎng)問(wèn),如果訪(fǎng)問(wèn)到(表示之前確實(shí)使用該 key 緩存過(guò)對(duì)象模软,該對(duì)象已經(jīng)從容量有限的 YYMemoryCache 中淘汰掉成立)會(huì)先在 YYMemoryCache 中更新一次該緩存對(duì)象的訪(fǎng)問(wèn)信息之后才返回給接口伟骨。
線(xiàn)程安全
如果說(shuō) YYCache 這個(gè)類(lèi)是一個(gè)純邏輯層的緩存類(lèi)(指 YYCache 的接口實(shí)現(xiàn)全部是調(diào)用其他類(lèi)完成),那么 YYMemoryCache 與 YYDiskCache 還是做了一些事情的(并沒(méi)有 YYCache 當(dāng)甩手掌柜那么輕松)燃异,其中最顯而易見(jiàn)的就是 YYMemoryCache 與 YYDiskCache 為 YYCache 保證了線(xiàn)程安全携狭。
YYMemoryCache 使用了 pthread_mutex 線(xiàn)程鎖來(lái)確保線(xiàn)程安全,而 YYDiskCache 則選擇了更適合它的 dispatch_semaphore回俐,上文已經(jīng)給出了作者選擇這些鎖的原因逛腿。
緩存控制
YYCache 提供了三種控制維度,分別是:cost鲫剿、count鳄逾、age。這已經(jīng)滿(mǎn)足了絕大多數(shù)開(kāi)發(fā)者的需求灵莲,我們?cè)谧约涸O(shè)計(jì)緩存時(shí)也可以根據(jù)自己的使用環(huán)境提供合適的控制方式雕凹。
緩存替換策略
在上文解析 YYCache 源碼的時(shí)候,介紹了緩存替換策略的概念并且列舉了很多經(jīng)典的策略政冻。YYCache 使用了雙向鏈表(_YYLinkedMapNode 與 _YYLinkedMap)實(shí)現(xiàn)了 LRU(least-recently-used) 策略枚抵,旨在提高 YYCache 的緩存命中率。
緩存命中率
這一概念是在上文解析 _YYLinkedMapNode 與 _YYLinkedMap 小節(jié)介紹的明场,我們?cè)谧约涸O(shè)計(jì)緩存時(shí)不一定非要使用 LRU 策略汽摹,可以根據(jù)我們的實(shí)際使用環(huán)境選擇最適合我們自己的緩存替換策略。
性能
其實(shí)性能這個(gè)東西是隱而不見(jiàn)的苦锨,又是到處可見(jiàn)的(笑)逼泣。它從我們最開(kāi)始設(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取微乎其微的性能提升
看到這里是不是恍然大悟,性能是怎么來(lái)的秃励?就是這樣對(duì)于每一個(gè)細(xì)節(jié)的極致追求一點(diǎn)一滴積少成多摳出來(lái)的氏仗。
來(lái)自:
https://lision.me/yycache/