YYCache原始碼筆記1

Yang1492955186752發表於2017-12-13

這一篇主要講YYMemoryCache。總的來說,YYMemoryCache不同於一般的第三方Cache的key-value儲存方式,而是採用的CFDictionary和雙向連結串列相結合的方式進行記憶體快取,這種做法的好處就是能夠滿足LRU(Least Recently Used)淘汰演算法。它的層級結構是YYMemoryCache ->_YYLinkedMap ->_YYLinkedMapNode。

_YYLinkedMapNode的結構如下

_YYLinkedMapNode.png

@package 是為了不讓framework外部對其進行操作 下面兩個是連結串列每一個節點的前後指標,因為每個node都會被CFDictionary進行retain操作,所以這裡使用的是__unsafe_unretained(PS:猜猜為什麼不用weak)來弱引用node _key對應外界傳進來的key _value就是需要儲存的值 _cost儲存這個value的記憶體消耗 _time訪問這個node的絕對時間

通過以上6個成員變數,就能完成時間,空間,數量的淘汰演算法了。

接下來我們看看是如何操作node來進行Cache的增刪查改的。

#增

通過外部呼叫- (void)setObject:(id)object forKey:(id)key或者- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost將物件存入MemoryCache中,具體實現如下

set.png

當object不存在時,呼叫removeObjectForKey 進行刪除。目前暫時不討論node存在的情況,後面會講。首先先建立一個新的node,然後通過->來對對於的成員變數進行賦值(不用點語法就是為了節省呼叫方法的時間,提升效能,還有OC中物件進行 ->是訪問成員變數,由於之前採用了 @package操作,所以不能在外部通過->來訪問成員變數,和結構體的->類似),然後用_lru(_YYLinkedMap的例項)將node插入到連結串列頭部。然後檢查是否超過數量和大小的限制,以進行尾部的node刪除,然後在後臺執行緒釋放。還有一點要說明的是這裡的原始碼為了保證執行緒安全使用的是OSSpinLock,它的好處是效能好,但是等待時間過長時會消耗大量CPU資源,所以很適合做記憶體快取的鎖。不過後來YY大神把原始碼裡所有的OSSpinLock全部替換為了pthread_mutex_t,這是因為OSSpinLock有潛在的優先順序反轉問題,具體可以檢視YY大神部落格。(更新:蘋果又新增了一個安全的自旋鎖:os_unfair_lock,,顧名思義能夠保證不同優先順序的執行緒申請鎖的時候不會發生優先順序反轉問題,預計隨Xcode8推出而更新) 這裡再提一個問題:為什麼將物件捕獲到block中後,隨便呼叫一個方法就能使其釋放?

#刪 具體實現如下

刪.png

根據key取出對於的node,然後操作_lru進行remove如下:

removeNode.png

這裡主要是根據這個node的_prev和_next所指向的node進行前後指標重新引用,和普通連結串列刪除節點是一樣的操作。

#查 程式碼如下:

查

查到相關node後,更新node的絕對時間,並且通過bringNodeToHead:將node放在連結串列頭部,具體程式碼為:

bringNodeToHead.png
流程前半部分和上文刪除節點的操作是一樣的,只不過刪除完節點之後需要將_head指向該節點,表示該node成為了連結串列的頭部節點。

#改 這部分程式碼和增是在同一個方法內的,首先判斷key-value pair是否存在,存在則將新的value存進去並更新絕對時間和_cost,然後將node插入連結串列頭結點。程式碼如下

set2.png

最後我們看看是如何實現淘汰演算法的: 1.首先當我們初始化一個MemoryCache例項之後,這個例項就會自建立成功後遞迴呼叫- (void)_trimRecursively如圖:

recursively.png
再提一個小問題,為什麼要在GCD內部進行strongSelf操作?

利用dispatch_after根據外部設定的_autoTrimInterval(預設5s)進行遞迴操作。分別輪詢costLimit,countLimit以及ageLimit。我們只講講costLimit,因為其他兩個操作是類似的。程式碼如下:

trim.png

首先判斷外部設定的costLimit是否為0,是則將MemoryCache全部清除程式碼如下:

removeAll.png

可以選擇是否非同步釋放和指定執行緒釋放。

然後判斷costCount是否大於costLimit,若大於,則嘗試加鎖進行尾部節點的移除。為了避免多次釋放導致的效能開銷,這裡是將所有的尾節點放於一個陣列中,集中釋放。

小結: 1.看了關於其他第三方的快取方案,YYCache暴露出來的costLimit我理解為是快取檔案的總記憶體限制,但是除非使用者自己手動呼叫YYMemory- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost;這個方法,不然直接通過最上YYCache這個類呼叫- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key;這個屬性就毫無疑義了,因為預設所有檔案的cost都是0。 2.拜讀真正大神的原始碼感覺比看很多標題黨部落格效果好得多。 3.通過這部分的原始碼拜讀,對於block的變數捕獲,加鎖解鎖機制,效能優化的方法以及自己造輪子程式碼的結構和抽象以及底層的C函式呼叫和連結串列的操作有了更深的認識。移動端也可以見識到各種資料結構以及演算法,感覺很棒~ 4.對於GCD的同步非同步處理以及線性併發佇列的選擇有了更明確的觀念。 5.未完待續。

相關文章