本文分析YYMemoryCache實現原理:
YYMemoryCache是記憶體快取,所以存取速度非常快,主要用到兩種資料結構的LRU淘汰演算法
1.LRU
Cache的容量是有限的,當Cache的空間都被佔滿後,如果再次發生快取失效,就必須選擇一個快取塊來替換掉.LRU法是依據各塊使用的情況, 總是選擇那個最長時間未被使用的塊替換。這種方法比較好地反映了程式區域性性規律
2.LRU主要採用兩種資料結構實現
- 雙向連結串列(Doubly Linked List)
- 雜湊表(Dictionary)
3.對一個Cache的操作無非三種:插入、替換、查詢
- 插入:當Cache未滿時,新的資料項只需插到雙連結串列頭部即可
- 替換:當Cache已滿時,將新的資料項插到雙連結串列頭部,並刪除雙連結串列的尾結點即可
- 查詢:每次資料項被查詢到時,都將此資料項移動到連結串列頭部
4.分析圖(分析原始碼時可以對照該圖)
5.YYMemoryCache.m裡的兩個分類
連結串列節點_YYLinkedMapNode
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@interface _YYLinkedMapNode : NSObject { @package // 指向前一個節點 __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic // 指向後一個節點 __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic // 快取key id _key; // 快取物件 id _value; // 當前快取記憶體開銷 NSUInteger _cost; // 快取時間 NSTimeInterval _time; } @end |
連結串列
_YYLinkedMap
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
@interface _YYLinkedMap : NSObject { @package // 用字典儲存所有節點_YYLinkedMapNode (為什麼不用oc字典?因為用CFMutableDictionaryRef效率高,畢竟基於c) CFMutableDictionaryRef _dic; // 總快取開銷 NSUInteger _totalCost; // 總快取數量 NSUInteger _totalCount; // 連結串列頭節點 _YYLinkedMapNode *_head; // 連結串列尾節點 _YYLinkedMapNode *_tail; // 是否在主執行緒上,非同步釋放 _YYLinkedMapNode物件 BOOL _releaseOnMainThread; // 是否非同步釋放 _YYLinkedMapNode物件 BOOL _releaseAsynchronously; } // 新增節點到連結串列頭節點 - (void)insertNodeAtHead:(_YYLinkedMapNode *)node; // 移動當前節點到連結串列頭節點 - (void)bringNodeToHead:(_YYLinkedMapNode *)node; // 移除連結串列節點 - (void)removeNode:(_YYLinkedMapNode *)node; // 移除連結串列尾節點(如果存在) - (_YYLinkedMapNode *)removeTailNode; // 移除所有快取 - (void)removeAll; @end |
方法插入、替換、查詢方法實現:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// 新增節點到連結串列頭節點 - (void)insertNodeAtHead:(_YYLinkedMapNode *)node { // 字典儲存連結串列節點node CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node)); // 疊加該快取開銷到總記憶體開銷 _totalCost += node->_cost; // 總快取數+1 _totalCount++; if (_head) { // 存在連結串列頭,取代當前表頭 node->_next = _head; _head->_prev = node; // 重新賦值連結串列表頭臨時變數_head _head = node; } else { // 不存在連結串列頭 _head = _tail = node; } } |
存在表頭情況圖形分析(其他情況不用圖分析,自己想象吧,呵呵)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
// 移動當前節點到連結串列頭節點 - (void)bringNodeToHead:(_YYLinkedMapNode *)node { // 當前節點已是連結串列頭節點 if (_head == node) return; if (_tail == node) { //**如果node是連結串列尾節點** // 把node指向的上一個節點賦值給連結串列尾節點 _tail = node->_prev; // 把連結串列尾節點指向的下一個節點賦值nil _tail->_next = nil; } else { //**如果node是非連結串列尾節點和連結串列頭節點** // 把node指向的上一個節點賦值給node指向的下一個節點node指向的上一個節點 node->_next->_prev = node->_prev; // 把node指向的下一個節點賦值給node指向的上一個節點node指向的下一個節點 node->_prev->_next = node->_next; } // 把連結串列頭節點賦值給node指向的下一個節點 node->_next = _head; // 把node指向的上一個節點賦值nil node->_prev = nil; // 把節點賦值給連結串列頭節點的指向的上一個節點 _head->_prev = node; _head = node; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 移除節點 - (void)removeNode:(_YYLinkedMapNode *)node { // 從字典中移除node CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key)); // 減掉總記憶體消耗 _totalCost -= node->_cost; // // 總快取數-1 _totalCount--; // 重新連線連結串列(看圖分析吧) if (node->_next) node->_next->_prev = node->_prev; if (node->_prev) node->_prev->_next = node->_next; if (_head == node) _head = node->_next; if (_tail == node) _tail = node->_prev; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 移除尾節點(如果存在) - (_YYLinkedMapNode *)removeTailNode { if (!_tail) return nil; // 拷貝一份要刪除的尾節點指標 _YYLinkedMapNode *tail = _tail; // 移除連結串列尾節點 CFDictionaryRemoveValue(_dic, (__bridge const void *)(_tail->_key)); // 減掉總記憶體消耗 _totalCost -= _tail->_cost; // 總快取數-1 _totalCount--; if (_head == _tail) { // 清除節點,連結串列上已無節點了 _head = _tail = nil; } else { // 設倒數第二個節點為連結串列尾節點 _tail = _tail->_prev; _tail->_next = nil; } // 返回完tail後_tail將會釋放 return tail; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
// 移除所有快取 - (void)removeAll { // 清空記憶體開銷與快取數量 _totalCost = 0; _totalCount = 0; // 清空頭尾節點 _head = nil; _tail = nil; if (CFDictionaryGetCount(_dic) > 0) { // 拷貝一份字典 CFMutableDictionaryRef holder = _dic; // 重新分配新的空間 _dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); if (_releaseAsynchronously) { // 非同步釋放快取 dispatch_queue_t queue = _releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue(); dispatch_async(queue, ^{ CFRelease(holder); // hold and release in specified queue }); } else if (_releaseOnMainThread && !pthread_main_np()) { // 主執行緒上釋放快取 dispatch_async(dispatch_get_main_queue(), ^{ CFRelease(holder); // hold and release in specified queue }); } else { // 同步釋放快取 CFRelease(holder); } } } |
YYMemoryCache.m實現分析(如果上面弄清楚,接下來就簡單多了),增刪改都是呼叫上面的方法,下面分析查詢與新增快取方法實現
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// 查詢快取 - (id)objectForKey:(id)key { if (!key) return nil; // 加鎖,防止資源競爭 // OSSpinLock 自旋鎖,效能最高的鎖。原理很簡單,就是一直 do while 忙等。它的缺點是當等待時會消耗大量 CPU 資源,所以它不適用於較長時間的任務。對於記憶體快取的存取來說,它非常合適。 pthread_mutex_lock(&_lock); // _lru為連結串列_YYLinkedMap,全部節點存在_lru->_dic中 // 獲取節點 _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key)); if (node) { //** 有對應快取 ** // 重新更新快取時間 node->_time = CACurrentMediaTime(); // 把當前node移到連結串列表頭(為什麼移到表頭?根據LRU淘汰演算法:Cache的容量是有限的,當Cache的空間都被佔滿後,如果再次發生快取失效,就必須選擇一個快取塊來替換掉.LRU法是依據各塊使用的情況, 總是選擇那個最長時間未被使用的塊替換。這種方法比較好地反映了程式區域性性規律) [_lru bringNodeToHead:node]; } // 解鎖 pthread_mutex_unlock(&_lock); // 有快取則返回快取值 return node ? node->_value : nil; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
// 新增快取 - (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) { //** 之前有快取,更新舊快取 ** // 更新值 _lru->_totalCost -= node->_cost; _lru->_totalCost += cost; node->_cost = cost; node->_time = now; node->_value = object; // 移動節點到連結串列表頭 [_lru bringNodeToHead:node]; } else { //** 之前未有快取,新增新快取 ** // 新建節點 node = [_YYLinkedMapNode new]; node->_cost = cost; node->_time = now; node->_key = key; node->_value = object; // 新增節點到表頭 [_lru insertNodeAtHead:node]; } if (_lru->_totalCost > _costLimit) { // ** 總快取開銷大於設定的開銷 ** // 非同步清理最久未使用的快取 dispatch_async(_queue, ^{ [self trimToCost:_costLimit]; }); } 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]; // 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); } |
接下來會分析YYDiskCache
實現原理
文章同步到微信公眾號:hans_iOS
有疑問可以在公眾號裡直接發