YYCache 原始碼解析(一):使用方法、架構與記憶體快取的設計
YYCache 是國內開發者 ibireme 開源的一個執行緒安全的高效能快取元件,程式碼風格簡潔清晰,閱讀它的原始碼有助於建立比較完整的快取設計的思路,同時也能鞏固一下雙向連結串列,執行緒鎖,資料庫操作相關的知識。
如果你還沒有看過 YYCache 的原始碼,那麼恭喜你,閱讀此文會對理解 YYCache 的原始碼有比較大的幫助。
YYCache 在架構上包含兩個層級的快取,一個是記憶體快取,另一個是磁碟快取,而且由於原文比較長,筆者將他們分別放在兩個文章裡面講解,即分為兩個公眾號文章來發布:
-
YYCache 原始碼解析(一):使用方法,架構與記憶體快取的設計
-
YYCache 原始碼解析(二):磁碟快取的設計與快取元件設計思路
本篇為第一篇,講解的是:
-
基本使用方法
-
架構與成員職責劃分
-
YYCache 的介面記憶體快取的設計
一. 基本使用方法
舉一個快取使用者姓名的例子來看一下YYCache的幾個API:
//需要快取的物件 NSString *userName = @"Jack"; //需要快取的物件在快取裡對應的鍵 NSString *key = @"user_name"; //建立一個YYCache例項:userInfoCache YYCache *userInfoCache = [YYCache cacheWithName:@"userInfo"]; //存入鍵值對 [userInfoCache setObject:userName forKey:key withBlock:^{ NSLog(@"caching object succeed"); }]; //判斷快取是否存在 [userInfoCache containsObjectForKey:key withBlock:^(NSString * _Nonnull key, BOOL contains) { if (contains){ NSLog(@"object exists"); } }]; //根據key讀取資料 [userInfoCache objectForKey:key withBlock:^(NSString * _Nonnull key, id<NSCoding>_Nonnull object) { NSLog(@"user name : %@",object); }]; //根據key移除快取 [userInfoCache removeObjectForKey:key withBlock:^(NSString * _Nonnull key) { NSLog(@"remove user name %@",key); }]; //移除所有快取 [userInfoCache removeAllObjectsWithBlock:^{ NSLog(@"removing all cache succeed"); }]; //移除所有快取帶進度 [userInfoCache removeAllObjectsWithProgressBlock:^(int removedCount, int totalCount) { NSLog(@"remove all cache objects: removedCount :%dtotalCount : %d",removedCount,totalCount); } endBlock:^(BOOL error) { if(!error){ NSLog(@"remove all cache objects: succeed"); }else{ NSLog(@"remove all cache objects: failed"); } }];
總體來看這些API與 NSCache
是差不多的。
再來看一下框架的架構圖與成員職責劃分。
二. 架構與成員職責劃分
架構圖

成員職責劃分
從架構圖上來看,該元件裡面的成員並不多:
-
YYCache:提供了最外層的介面,呼叫了YYMemoryCache與YYDiskCache的相關方法。
-
YYMemoryCache:負責處理容量小,相對高速的記憶體快取。執行緒安全,支援自動和手動清理快取等功能。
-
_YYLinkedMap:YYMemoryCache使用的雙向連結串列類。
-
_YYLinkedMapNode:是_YYLinkedMap使用的節點類。
-
YYDiskCache:負責處理容量大,相對低速的磁碟快取。執行緒安全,支援非同步操作,自動和手動清理快取等功能。
-
YYKVStorage:YYDiskCache的底層實現類,用於管理磁碟快取。
-
YYKVStorageItem:內建在YYKVStorage中,是YYKVStorage內部用於封裝某個快取的類。
三. YYCache的介面與記憶體快取的設計
知道了YYCache的架構圖與成員職責劃分以後,現在結合程式碼開始正式講解。
本章的講解分為下面2個部分:
-
YYCache
-
YYMemoryCache
YYCache
YYCache給使用者提供所有最外層的快取操作介面,而這些介面的內部內部實際上是呼叫了YYMemoryCache和YYDiskCache物件的相關方法。
我們來看一下YYCache的屬性和介面:
YYCache的屬性和介面
@interface YYCache : NSObject @property (copy, readonly) NSString *name;//快取名稱 @property (strong, readonly) YYMemoryCache *memoryCache;//記憶體快取 @property (strong, readonly) YYDiskCache *diskCache;//磁碟快取 //是否包含某快取,無回撥 - (BOOL)containsObjectForKey:(NSString *)key; //是否包含某快取,有回撥 - (void)containsObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, BOOL contains))block; //獲取快取物件,無回撥 - (nullable id<NSCoding>)objectForKey:(NSString *)key; //獲取快取物件,有回撥 - (void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id<NSCoding> object))block; //寫入快取物件,無回撥 - (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key; //寫入快取物件,有回撥 - (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(nullable void(^)(void))block; //移除某快取,無回撥 - (void)removeObjectForKey:(NSString *)key; //移除某快取,有回撥 - (void)removeObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key))block; //移除所有快取,無回撥 - (void)removeAllObjects; //移除所有快取,有回撥 - (void)removeAllObjectsWithBlock:(void(^)(void))block; //移除所有快取,有進度和完成的回撥 - (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress endBlock:(nullable void(^)(BOOL error))end; @end
從上面的介面可以看出YYCache的介面和NSCache很相近,而且在介面上都區分了有無回撥的功能。
下面結合程式碼看一下這些介面是如何實現的:
YYCache的介面實現
下面省略了帶有回撥的介面,因為與無回撥的介面非常接近。
- (BOOL)containsObjectForKey:(NSString *)key { //先檢查記憶體快取是否存在,再檢查磁碟快取是否存在 return [_memoryCache containsObjectForKey:key] || [_diskCache containsObjectForKey:key]; } - (id<NSCoding>)objectForKey:(NSString *)key { //首先嚐試獲取記憶體快取,然後獲取磁碟快取 id<NSCoding> object = [_memoryCache objectForKey:key]; //如果記憶體快取不存在,就會去磁碟快取裡面找:如果找到了,則再次寫入記憶體快取中;如果沒找到,就返回nil if (!object) { object = [_diskCache objectForKey:key]; if (object) { [_memoryCache setObject:object forKey:key]; } } return object; } - (void)setObject:(id<NSCoding>)object forKey:(NSString *)key { //先寫入記憶體快取,後寫入磁碟快取 [_memoryCache setObject:object forKey:key]; [_diskCache setObject:object forKey:key]; } - (void)removeObjectForKey:(NSString *)key { //先移除記憶體快取,後移除磁碟快取 [_memoryCache removeObjectForKey:key]; [_diskCache removeObjectForKey:key]; } - (void)removeAllObjects { //先全部移除記憶體快取,後全部移除磁碟快取 [_memoryCache removeAllObjects]; [_diskCache removeAllObjects]; }
從上面的介面實現可以看出:在YYCache中,永遠都是先訪問記憶體快取,然後再訪問磁碟快取(包括了寫入,讀取,查詢,刪除快取的操作)。而且關於記憶體快取(_memoryCache)的操作,是不存在block回撥的。
值得一提的是: 在讀取快取的操作中,如果在記憶體快取中無法獲取對應的快取,則會去磁碟快取中尋找。如果在磁碟快取中找到了對應的快取,則會將該物件再次寫入記憶體快取中,保證在下一次嘗試獲取同一快取時能夠在記憶體中就能返回,提高速度 。
OK,現在瞭解了YYCache的介面以及實現,下面我分別講解一下YYMemoryCache(記憶體快取)和YYDiskCache(磁碟快取)這兩個類。
YYMemoryCache
YYMemoryCache負責處理容量小,相對高速的記憶體快取:它將需要快取的物件與傳入的key關聯起來,操作類似於NSCache。
但是與NSCache不同的是,YYMemoryCache的內部有:
-
快取淘汰演算法:使用LRU(least-recently-used) 演算法來淘汰(清理)使用頻率較低的快取。
-
快取清理策略:使用三個維度來標記,分別是count(快取數量),cost(開銷),age(距上一次的訪問時間)。YYMemoryCache提供了分別針對這三個維度的清理快取的介面。使用者可以根據不同的需求(策略)來清理在某一維度超標的快取。
一個是淘汰演算法,另一個是清理維度,乍一看可能沒什麼太大區別。我在這裡先簡單區分一下:
快取淘汰演算法的目的在於區分出使用頻率高和使用頻率低的快取,當快取數量達到一定限制的時候會優先清理那些使用頻率低的快取。 因為使用頻率已經比較低的快取在將來的使用頻率也很有可能會低 。
快取清理維度是給每個快取新增的標記:
-
如果使用者需要刪除age(距上一次的訪問時間)超過1天的快取,在YYMemoryCache內部,就會從使用頻率最低的那個快取開始查詢,直到所有距上一次的訪問時間超過1天的快取都清理掉為止。
-
如果使用者需要將快取總開銷清理到總開銷小於或等於某個值,在YYMemoryCache內部,就會從使用頻率最低的那個快取開始清理,直到總開銷小於或等於這個值。
-
如果使用者需要將快取總數清理到總開銷小於或等於某個值,在YYMemoryCache內部,就會從使用頻率最低的那個快取開始清理,直到總開銷小於或等於這個值。
可以看出,無論是以哪個維度來清理快取,都是從快取使用頻率最低的那個快取開始清理。而YYMemoryCache保留的所有快取的使用頻率的高低,是由LRU這個演算法決定的。
現在知道了這二者的區別,下面來具體講解一下快取淘汰演算法和快取清理策略:
YYMemoryCache的快取淘汰演算法
在詳細講解這個演算法之前我覺得有必要先說一下該演算法的核心:
我個人認為LRU快取替換策略的核心在於 如果某個快取訪問的頻率越高,就認定使用者在將來越有可能訪問這個快取 。
所以在這個演算法中,將那些最新訪問(寫入),最多次被訪問的快取移到最前面,然後那些很早之前寫入,不經常訪問的快取就被自動放在了後面。這樣一來,在保留的快取個數一定的情況下,留下的快取都是訪問頻率比較高的,這樣一來也就提升了快取的命中率。誰都不想留著一些很難被使用者再次訪問的快取,畢竟快取本身也佔有一定的資源不是麼?
其實這個道理和一些商城類app的商品推薦邏輯是一樣的:
如果首頁只能展示10個商品,對於一個程式員使用者來說,可能推薦的是於那些他最近購買商品類似的機械鍵盤滑鼠,技術書籍或者顯示屏之類的商品,而不是一些洋娃娃或是鋼筆之類的商品。
那麼LRU演算法具體是怎麼做的呢?
在YYMemoryCache中,使用了雙向連結串列這個資料結構來儲存這些快取:
-
當寫入一個新的快取時,要把這個快取節點放在連結串列頭部,並且並且原連結串列頭部的快取節點要變成現在連結串列的第二個快取節點。
-
當訪問一個已有的快取時,要把這個快取節點移動到連結串列頭部,原位置兩側的快取要接上,並且原連結串列頭部的快取節點要變成現在連結串列的第二個快取節點。
-
(根據清理維度)自動清理快取時,要從連結串列的最後端逐個清理。
這樣一來,就可以保證連結串列前端的快取是最近寫入過和經常訪問過的。而且該演算法總是從連結串列的最後端刪除快取,這也就保證了留下的都是一些“比較新鮮的”快取。
下面結合程式碼來講解一下這個演算法的實現:
YYMemoryCache 用一個連結串列節點類來儲存某個單獨的記憶體快取的資訊(鍵,值,快取時間等),然後用一個雙向連結串列類來儲存和管理這些節點 。這兩個類的名稱分別是:
-
_YYLinkedMapNode:連結串列內的節點類,可以看做是對某個單獨記憶體快取的封裝。
-
_YYLinkedMap:雙向連結串列類,用於儲存和管理所有記憶體快取(節點)
_YYLinkedMapNode
_YYLinkedMapNode可以被看做是對某個快取的封裝:它包含了該節點上一個和下一個節點的指標,以及快取的key和對應的值(物件),還有該快取的開銷和訪問時間。
@interface _YYLinkedMapNode : NSObject { @package __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic id _key;//快取key id _value;//key對應值 NSUInteger _cost;//快取開銷 NSTimeInterval _time;//訪問時間 } @end @implementation _YYLinkedMapNode @end
下面看一下雙向連結串列類:
_YYLinkedMap
@interface _YYLinkedMap : NSObject { @package CFMutableDictionaryRef _dic;// 用於存放節點 NSUInteger _totalCost;//總開銷 NSUInteger _totalCount;//節點總數 _YYLinkedMapNode *_head;// 連結串列的頭部結點 _YYLinkedMapNode *_tail;// 連結串列的尾部節點 BOOL _releaseOnMainThread;//是否在主執行緒釋放,預設為NO BOOL _releaseAsynchronously;//是否在子執行緒釋放,預設為YES } //在連結串列頭部插入某節點 - (void)insertNodeAtHead:(_YYLinkedMapNode *)node; //將連結串列內部的某個節點移到連結串列頭部 - (void)bringNodeToHead:(_YYLinkedMapNode *)node; //移除某個節點 - (void)removeNode:(_YYLinkedMapNode *)node; //移除連結串列的尾部節點並返回它 - (_YYLinkedMapNode *)removeTailNode; //移除所有節點(預設在子執行緒操作) - (void)removeAll; @end
從連結串列類的屬性上看:連結串列類內建了CFMutableDictionaryRef,用於儲存節點的鍵值對,它還持有了連結串列內節點的總開銷,總數量,頭尾節點等資料。
可以參考下面這張圖來看一下二者的關係:

看一下_YYLinkedMap的介面的實現:
將節點插入到連結串列頭部:
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node { //設定該node的值 CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node)); //增加開銷和總快取數量 _totalCost += node->_cost; _totalCount++; if (_head) { //如果連結串列內已經存在頭節點,則將這個頭節點賦給當前節點的尾指標(原第一個節點變成了現第二個節點) node->_next = _head; //將該節點賦給現第二個節點的頭指標(此時_head指向的節點是先第二個節點) _head->_prev = node; //將該節點賦給連結串列的頭結點指標(該節點變成了現第一個節點) _head = node; } else { //如果連結串列內沒有頭結點,說明是空連結串列。說明是第一次插入,則將連結串列的頭尾節點都設定為當前節點 _head = _tail = node; } }
要看懂節點操作的程式碼只要瞭解雙向連結串列的特性即可。在雙向連結串列中:
-
每個節點都有兩個分別指向前後節點的指標。所以說每個節點都知道它前一個節點和後一個節點是誰。
-
連結串列的頭部節點指向它前面節點的指標為空;連結串列尾部節點指向它後側節點的指標也為空。
為了便於理解,我們可以把這個抽象概念類比於幼兒園手拉手的小朋友們:
每個小朋友的左手都拉著前面小朋友的右手;每個小朋友的右手都拉著後面小朋友的左手;
而且最前面的小朋友的左手和最後面的小朋友的右手都沒有拉任何一個小朋友。
將某個節點移動到連結串列頭部:
- (void)bringNodeToHead:(_YYLinkedMapNode *)node { //如果該節點已經是連結串列頭部節點,則立即返回,不做任何操作 if (_head == node) return; if (_tail == node) { //如果該節點是連結串列的尾部節點 //1. 將該節點的頭指標指向的節點變成連結串列的尾節點(將倒數第二個節點變成倒數第一個節點,即尾部節點) _tail = node->_prev; //2. 將新的尾部節點的尾部指標置空 _tail->_next = nil; } else { //如果該節點是連結串列頭部和尾部以外的節點(中間節點) //1. 將該node的頭指標指向的節點賦給其尾指標指向的節點的頭指標 node->_next->_prev = node->_prev; //2. 將該node的尾指標指向的節點賦給其頭指標指向的節點的尾指標 node->_prev->_next = node->_next; } //將原頭節點賦給該節點的尾指標(原第一個節點變成了現第二個節點) node->_next = _head; //將當前節點的頭節點置空 node->_prev = nil; //將現第二個節點的頭結點指向當前節點(此時_head指向的節點是現第二個節點) _head->_prev = node; //將該節點設定為連結串列的頭節點 _head = node; }
第一次看上面的程式碼我自己是懵逼的,不過如果結合上面小朋友拉手的例子就可以快一點理解。
如果要其中一個小朋友放在隊伍的最前面,需要
-
將原來這個小朋友前後的小朋友的手拉上。
-
然後將這個小朋友的右手和原來排在第一位的小朋友的左手拉上。
上面說的比較簡略,但是相信對大家理解整個過程會有幫助。
也可以再結合連結串列的圖解來看一下:

讀者同樣可以利用這種思考方式理解下面這段程式碼:
移除連結串列中的某個節點:
- (void)removeNode:(_YYLinkedMapNode *)node { //除去該node的鍵對應的值 CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key)); //減去開銷和總快取數量 _totalCost -= node->_cost; _totalCount--; //節點操作 //1. 將該node的頭指標指向的節點賦給其尾指標指向的節點的頭指標 if (node->_next) node->_next->_prev = node->_prev; //2. 將該node的尾指標指向的節點賦給其頭指標指向的節點的尾指標 if (node->_prev) node->_prev->_next = node->_next; //3. 如果該node就是連結串列的頭結點,則將該node的尾部指標指向的節點賦給連結串列的頭節點(第二變成了第一) if (_head == node) _head = node->_next; //4. 如果該node就是連結串列的尾節點,則將該node的頭部指標指向的節點賦給連結串列的尾節點(倒數第二變成了倒數第一) if (_tail == node) _tail = node->_prev; }
移除並返回尾部的node:
- (_YYLinkedMapNode *)removeTailNode { //如果不存在尾節點,則返回nil if (!_tail) return nil; _YYLinkedMapNode *tail = _tail; //移除尾部節點對應的值 CFDictionaryRemoveValue(_dic, (__bridge const void *)(_tail->_key)); //減少開銷和總快取數量 _totalCost -= _tail->_cost; _totalCount--; if (_head == _tail) { //如果連結串列的頭尾節點相同,說明連結串列只有一個節點。將其置空 _head = _tail = nil; } else { //將連結串列的尾節指標指向的指標賦給連結串列的尾指標(倒數第二變成了倒數第一) _tail = _tail->_prev; //將新的尾節點的尾指標置空 _tail->_next = nil; } return tail; }
OK,現在瞭解了YYMemoryCache底層的節點操作的程式碼。現在來看一下YYMemoryCache是如何使用它們的。
YYMemoryCache的屬性和介面
//YYMemoryCache.h @interface YYMemoryCache : NSObject #pragma mark - Attribute //快取名稱,預設為nil @property (nullable, copy) NSString *name; //快取總數量 @property (readonly) NSUInteger totalCount; //快取總開銷 @property (readonly) NSUInteger totalCost; #pragma mark - Limit //數量上限,預設為NSUIntegerMax,也就是無上限 @property NSUInteger countLimit; //開銷上限,預設為NSUIntegerMax,也就是無上限 @property NSUInteger costLimit; //快取時間上限,預設為DBL_MAX,也就是無上限 @property NSTimeInterval ageLimit; //清理超出上限之外的快取的操作間隔時間,預設為5s @property NSTimeInterval autoTrimInterval; //收到記憶體警告時是否清理所有快取,預設為YES @property BOOL shouldRemoveAllObjectsOnMemoryWarning; //app進入後臺是是否清理所有快取,預設為YES @property BOOL shouldRemoveAllObjectsWhenEnteringBackground; //收到記憶體警告的回撥block @property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache); //進入後臺的回撥block @property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache); //快取清理是否在後臺進行,預設為NO @property BOOL releaseOnMainThread; //快取清理是否非同步執行,預設為YES @property BOOL releaseAsynchronously; #pragma mark - Access Methods //是否包含某個快取 - (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; //清理快取到指定開銷 - (void)trimToCost:(NSUInteger)cost; //清理快取時間小於指定時間的快取 - (void)trimToAge:(NSTimeInterval)age;
YYMemoryCache的介面實現
在YYMemoryCache的初始化方法裡,會例項化一個_YYLinkedMap的例項來賦給_lru這個成員變數。
- (instancetype)init{ .... _lru = [_YYLinkedMap new]; ... }
然後所有的關於快取的操作,都要用到_lru這個成員變數,因為它才是在底層持有這些快取(節點)的雙向連結串列類。下面我們來看一下這些快取操作介面的實現:
//是否包含某個快取物件 - (BOOL)containsObjectForKey:(id)key { //嘗試從內建的字典中獲得快取物件 if (!key) return NO; pthread_mutex_lock(&_lock); BOOL contains = CFDictionaryContainsKey(_lru->_dic, (__bridge const void *)(key)); pthread_mutex_unlock(&_lock); return contains; } //獲取某個快取物件 - (id)objectForKey:(id)key { if (!key) return nil; pthread_mutex_lock(&_lock); _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); if (node) { //如果節點存在,則更新它的時間資訊(最後一次訪問的時間) node->_time = CACurrentMediaTime(); [_lru bringNodeToHead:node]; } pthread_mutex_unlock(&_lock); return node ? node->_value : nil; } //寫入某個快取物件,開銷預設為0 - (void)setObject:(id)object forKey:(id)key { [self setObject:object forKey:key withCost:0]; } //寫入某個快取物件,並存入快取開銷 - (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost { if (!key) return; if (!object) { [self removeObjectForKey:key]; return; } pthread_mutex_lock(&_lock); _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); NSTimeInterval now = CACurrentMediaTime(); if (node) { //如果存在與傳入的key值匹配的node,則更新該node的value,cost,time,並將這個node移到連結串列頭部 //更新總cost _lru->_totalCost -= node->_cost; _lru->_totalCost += cost; //更新node node->_cost = cost; node->_time = now; node->_value = object; //將node移動至連結串列頭部 [_lru bringNodeToHead:node]; } else { //如果不存在與傳入的key值匹配的node,則新建一個node,將key,value,cost,time賦給它,並將這個node插入到連結串列頭部 //新建node,並賦值 node = [_YYLinkedMapNode new]; node->_cost = cost; node->_time = now; node->_key = key; node->_value = object; //將node插入至連結串列頭部 [_lru insertNodeAtHead:node]; } //如果cost超過了限制,則進行刪除快取操作(從連結串列尾部開始刪除,直到符合限制要求) if (_lru->_totalCost > _costLimit) { dispatch_async(_queue, ^{ [self trimToCost:_costLimit]; }); } //如果total count超過了限制,則進行刪除快取操作(從連結串列尾部開始刪除,刪除一次即可) if (_lru->_totalCount > _countLimit) { _YYLinkedMapNode *node = [_lru removeTailNode]; if (_lru->_releaseAsynchronously) { dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); dispatch_async(queue, ^{ [node class]; //hold and release in queue }); } else if (_lru->_releaseOnMainThread && !pthread_main_np()) { dispatch_async(dispatch_get_main_queue(), ^{ [node class]; //hold and release in queue }); } } pthread_mutex_unlock(&_lock); } //移除某個快取物件 - (void)removeObjectForKey:(id)key { if (!key) return; pthread_mutex_lock(&_lock); _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); if (node) { //內部呼叫了連結串列的removeNode:方法 [_lru removeNode:node]; if (_lru->_releaseAsynchronously) { dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); dispatch_async(queue, ^{ [node class]; //hold and release in queue }); } else if (_lru->_releaseOnMainThread && !pthread_main_np()) { dispatch_async(dispatch_get_main_queue(), ^{ [node class]; //hold and release in queue }); } } pthread_mutex_unlock(&_lock); } //內部呼叫了連結串列的removeAll方法 - (void)removeAllObjects { pthread_mutex_lock(&_lock); [_lru removeAll]; pthread_mutex_unlock(&_lock); }
上面的實現是針對快取的查詢,寫入,獲取操作的,接下來看一下快取的清理策略。
YYMemoryCache的快取清理策略
如上文所說,在YYCache中,快取的清理可以從快取總數量,快取總開銷,快取距上一次的訪問時間來清理快取。而且每種維度的清理操作都可以分為自動和手動的方式來進行。
快取自動清理
快取的自動清理功能在YYMemoryCache初始化之後就開始了,是一個遞迴呼叫的實現:
//YYMemoryCache.m - (instancetype)init{ ... //開始定期清理 [self _trimRecursively]; ... } //遞迴清理,相隔時間為_autoTrimInterval,在初始化之後立即執行 - (void)_trimRecursively { __weak typeof(self) _self = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(_autoTrimInterval * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{ __strong typeof(_self) self = _self; if (!self) return; //在後臺進行清理操作 [self _trimInBackground]; //呼叫自己,遞迴操作 [self _trimRecursively]; }); } //清理所有不符合限制的快取,順序為:cost,count,age - (void)_trimInBackground { dispatch_async(_queue, ^{ [self _trimToCost:self->_costLimit]; [self _trimToCount:self->_countLimit]; [self _trimToAge:self->_ageLimit]; }); }
//YYMemoryCache.m - (void)trimToCount:(NSUInteger)count { if (count == 0) { [self removeAllObjects]; return; } [self _trimToCount:count]; } - (void)trimToCost:(NSUInteger)cost { [self _trimToCost:cost]; } - (void)trimToAge:(NSTimeInterval)age { [self _trimToAge:age]; }
可以看到,YYMemoryCache是按照快取數量,快取開銷,快取時間的順序來自動清空快取的。我們結合程式碼看一下它是如何按照快取數量來清理快取的(其他兩種清理方式類似,暫不給出):
//YYMemoryCache.m //將記憶體快取數量降至等於或小於傳入的數量;如果傳入的值為0,則刪除全部記憶體快取 - (void)_trimToCount:(NSUInteger)countLimit { BOOL finish = NO; pthread_mutex_lock(&_lock); //如果傳入的引數=0,則刪除所有記憶體快取 if (countLimit == 0) { [_lru removeAll]; finish = YES; } else if (_lru->_totalCount <= countLimit) { //如果當前快取的總數量已經小於或等於傳入的數量,則直接返回YES,不進行清理 finish = YES; } pthread_mutex_unlock(&_lock); if (finish) return; NSMutableArray *holder = [NSMutableArray new]; while (!finish) { //==0的時候說明在嘗試加鎖的時候,獲取鎖成功,從而可以進行操作;否則等待10秒(但是不知道為什麼是10s而不是2s,5s,等等) if (pthread_mutex_trylock(&_lock) == 0) { if (_lru->_totalCount > countLimit) { _YYLinkedMapNode *node = [_lru removeTailNode]; if (node) [holder addObject:node]; } else { finish = YES; } pthread_mutex_unlock(&_lock); } else { usleep(10 * 1000); //10 ms } } if (holder.count) { dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); dispatch_async(queue, ^{ [holder count]; // release in queue }); } }
快取手動清理
其實上面這三種清理的方法在YYMemoryCache封裝成了介面,所以使用者也可以通過YYCache的memoryCache這個屬性來手動清理相應維度上不符合傳入標準的快取:
//YYMemoryCache.h // =========== 快取清理介面 =========== //清理快取到指定個數 - (void)trimToCount:(NSUInteger)count; //清理快取到指定開銷 - (void)trimToCost:(NSUInteger)cost; //清理快取時間小於指定時間的快取 - (void)trimToAge:(NSTimeInterval)age;
看一下它們的實現:
//清理快取到指定個數 - (void)trimToCount:(NSUInteger)count { if (count == 0) { [self removeAllObjects]; return; } [self _trimToCount:count]; } //清理快取到指定開銷 - (void)trimToCost:(NSUInteger)cost { [self _trimToCost:cost]; } //清理快取時間小於指定時間的快取 - (void)trimToAge:(NSTimeInterval)age { [self _trimToAge:age]; }
好了, YYCache 的外部介面以及第一級快取:記憶體快取( YYMemoryCache )就講完了,下一篇講的是磁碟快取與快取設計思路方面的講解。
想看更多技術,讀書筆記,思考類的乾貨麼?掃下方的二維碼關注本公眾號,期待與您的共同成長 ~