YYCache

從 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)單,更加難能可貴的是它的性能還非常高羡鸥。

image
image

我對(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)介

image

簡(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é)剖析

image

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乓旗。

image

上面是 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ì)表述的盡量易懂伯铣。

緩存命中率

image

為什么有這么多緩存替換策略,或者說(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é)剖析

image

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)有 conformNSCoding協(xié)議的對(duì)象

@property (nullable, copy) id (^customUnarchiveBlock)(NSData *data); ``// 用來(lái)替換 NSKeyedUnarchiver,你可以使用該代碼塊以支持沒(méi)有 conformNSCoding協(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ì)

image

嘛~ 我們回到文章最初提到的問(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/

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市夺鲜,隨后出現(xiàn)的幾起案子皆尔,更是在濱河造成了極大的恐慌,老刑警劉巖币励,帶你破解...
    沈念sama閱讀 206,723評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慷蠕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡榄审,警方通過(guò)查閱死者的電腦和手機(jī)砌们,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,485評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人浪感,你說(shuō)我怎么就攤上這事昔头。” “怎么了影兽?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,998評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵揭斧,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我峻堰,道長(zhǎng)讹开,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,323評(píng)論 1 279
  • 正文 為了忘掉前任捐名,我火速辦了婚禮旦万,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘镶蹋。我一直安慰自己成艘,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,355評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布贺归。 她就那樣靜靜地躺著淆两,像睡著了一般。 火紅的嫁衣襯著肌膚如雪拂酣。 梳的紋絲不亂的頭發(fā)上秋冰,一...
    開(kāi)封第一講書(shū)人閱讀 49,079評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音婶熬,去河邊找鬼剑勾。 笑死,一個(gè)胖子當(dāng)著我的面吹牛赵颅,可吹牛的內(nèi)容都是我干的甥材。 我是一名探鬼主播,決...
    沈念sama閱讀 38,389評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼性含,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了鸳惯?” 一聲冷哼從身側(cè)響起商蕴,我...
    開(kāi)封第一講書(shū)人閱讀 37,019評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎芝发,沒(méi)想到半個(gè)月后绪商,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,519評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辅鲸,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,971評(píng)論 2 325
  • 正文 我和宋清朗相戀三年格郁,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,100評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡例书,死狀恐怖锣尉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情决采,我是刑警寧澤自沧,帶...
    沈念sama閱讀 33,738評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站树瞭,受9級(jí)特大地震影響拇厢,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜晒喷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,293評(píng)論 3 307
  • 文/蒙蒙 一孝偎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧凉敲,春花似錦衣盾、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,289評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至废赞,卻和暖如春徽龟,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背唉地。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,517評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工据悔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人耘沼。 一個(gè)月前我還...
    沈念sama閱讀 45,547評(píng)論 2 354
  • 正文 我出身青樓极颓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親群嗤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子菠隆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,834評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容