區別於NSDictionary, YYMemoryCache對key是retain而不是copy操做(YYMemoryCache的key能夠是任何對象的緣由, 內部是使用CFMutableDictionaryRef實現的), 提供的API和NSCache相似, 而且YYMemoryCache的全部方法都是安全的.node
YYMemoryCache相比NSCache的特色:算法
YYMemoryCache中使用一個雙向鏈表和字典進行增刪改查操做的:數組
YYLinkedMapNode能夠理解爲鏈表中的結點. YYMemoryCache中是將傳入的value封裝成node存儲到緩存中的.緩存
@interface _YYLinkedMapNode : NSObject {
@package
__unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
__unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
id _key; // key
id _value; // value
NSUInteger _cost; // 結點的開銷, 在內存中佔用的字節數
NSTimeInterval _time; // 操做結點的時間
}
@end
@implementation _YYLinkedMapNode
@end
複製代碼
須要注意的是Node並不持有對_prev和_next的強引用, _prev和_next執行的內容是由_dic強引用安全
YYLinkedMap能夠理解爲是一個雙向鏈表, 方便操做頭結點和尾結點.數據結構
@interface _YYLinkedMap : NSObject {
@package
CFMutableDictionaryRef _dic; // do not set object directly key: key, value: Node結點
NSUInteger _totalCost; // 總開銷
NSUInteger _totalCount; // 緩存數量
_YYLinkedMapNode *_head; // MRU, do not change it directly 頭結點
_YYLinkedMapNode *_tail; // LRU, do not change it directly 尾結點
BOOL _releaseOnMainThread; // 是否在主線程release結點
BOOL _releaseAsynchronously; // 是否異步release結點
}
複製代碼
思考: 問什麼YYLinkedMap中要使用一個字典呢?多線程
使用字典是爲了使雙向鏈表的查詢某個結點的操做時間複雜度爲O(1), 若是是雙向鏈表獲取某一個結點的話須要遍歷雙向鏈表, 時間複雜度爲O(n)併發
YYLinkedMap中使用CFMutableDictionaryRef建立字典, 而不是使用NSDictionary建立字典的緣由:app
由於NSDictionary的key須要遵循NSCoding協議, 而CFMutableDictionaryRef則不須要, 另外CFMutableDictionaryRef更加靠近底層, 效率更高, 可是建立的_dic的內存須要咱們本身手動回收.異步
YYLinkedMap的接口:
// 在鏈表頭部插入結點
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
// 將結點移動到鏈表頭部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
// 移除結點
- (void)removeNode:(_YYLinkedMapNode *)node;
// 移除尾結點
- (_YYLinkedMapNode *)removeTailNode;
// 清空全部結點
- (void)removeAll;
複製代碼
YYLinkedMap的操做主要是對雙向鏈表進行的插入 刪除操做, 下圖中虛線爲弱引用, 實線爲強引用
YYLinkedMap的接口實現: 添加結點:- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
// 將結點node保存在_dic中
CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
// 根據結點的cost開銷修改map記錄的總開銷數
_totalCost += node->_cost;
// 記錄緩存個數
_totalCount++;
if (_head) {
// 將結點node設置爲頭結點
node->_next = _head;
_head->_prev = node;
_head = node;
} else {
// 若是頭結點爲nil, 說明鏈表爲空, 添加的結點node就是頭結點 而且仍是尾結點
_head = _tail = node;
}
}
複製代碼
移動結點:
- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
// 若是隻有一個結點, 說明不須要移動
if (_head == node) return;
if (_tail == node) {
// 若是node是尾結點, 從新設置尾結點
_tail = node->_prev;
_tail->_next = nil;
} else {
// node既不是頭結點又不是尾結點, 至關於刪除結點node
node->_next->_prev = node->_prev;
node->_prev->_next = node->_next;
}
// 將node設置爲頭結點
node->_next = _head;
node->_prev = nil;
_head->_prev = node;
_head = node;
}
複製代碼
刪除結點:
- (void)removeNode:(_YYLinkedMapNode *)node {
// 將結點node從字典_dic中移除, 注意這是node的引用計數減1
CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));
// 修改總開銷
_totalCost -= node->_cost;
// 緩存數量減1
_totalCount--;
// 修改node結點的下一個結點prev指向
if (node->_next) node->_next->_prev = node->_prev;
// 修改node結點的上一個結點的next指向
if (node->_prev) node->_prev->_next = node->_next;
// 若是node是頭結點, 將頭結點設置爲node的下一個結點
if (_head == node) _head = node->_next;
// 若是node是尾結點, 將尾結點設置爲node的上一個結點
if (_tail == node) _tail = node->_prev;
}
複製代碼
注意這裏的結點操做執行的順序, 考慮node是頭結點或者尾結點 以及爲中間結點的處理狀況. 移除尾結點:
- (_YYLinkedMapNode *)removeTailNode {
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;
}
複製代碼
清空數據:
- (void)removeAll {
// 清空信息
_totalCost = 0;
_totalCount = 0;
_head = nil;
_tail = nil;
if (CFDictionaryGetCount(_dic) > 0) {
// 至關於對_dic進行了一次mutableCopy, 因爲_dic是不可變, 因此holder和_dic執行了同一塊內存空間(堆空間)
CFMutableDictionaryRef holder = _dic;
// 從新在堆空間申請內存, _dic執行新分配的內存(以前堆空間的內存地址保存在holder中)
_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);
}
}
}
複製代碼
清空數據這裏做者的操做很巧妙, 利用holder引用dic, 從新給dic申請堆空間, 在指定的隊列中實現對holder的release操做.
YYMemoryCache是對外提供的類, 提供了一些配置屬性和外界訪問的方法.
@interface YYMemoryCache : NSObject
// 緩存名稱
@property (nullable, copy) NSString *name;
// 緩存的總數量
@property (readonly) NSUInteger totalCount;
// 緩存的總開銷
@property (readonly) NSUInteger totalCost;
// 緩存的數量限制, 默認值是NSUIntegerMax表示不作限制
@property NSUInteger countLimit;
// 緩存的開銷限制, 默認值是NSUIntegerMax表示不作限制, 例如設置緩存總開銷爲200k, 那麼當總開銷大於200k時就會在後臺自動清理
@property NSUInteger costLimit;
// 緩存的時間限制, 默認是DBL_MAX表示不限制
@property NSTimeInterval ageLimit;
// 自動清理時間設置, 默認值是5s, 也就是5秒鐘會自動檢查清理緩存
@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);
// 當須要移除某個value時, release操做是否在主線程執行, 默認爲NO
@property BOOL releaseOnMainThread;
// 當須要移除某個value時, release造做是否異步執行, 默認爲YES
@property BOOL releaseAsynchronously;
// 緩存中是否存在key
- (BOOL)containsObjectForKey:(id)key;
// 從緩存中獲取和key相關聯的value
- (nullable id)objectForKey:(id)key;
// 緩存key-value, 若是value=nil, 表示移除key的緩存
- (void)setObject:(nullable id)object forKey:(id)key;
// 緩存key-value, 指定開銷
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;
// 從緩存中移除key
- (void)removeObjectForKey:(id)key;
// 清空緩存
- (void)removeAllObjects;
// 將緩存修剪到指定的數量, 從鏈表的尾結點移除
- (void)trimToCount:(NSUInteger)count
// 將緩存修剪到指定的開銷, 從鏈表的尾結點移除
- (void)trimToCost:(NSUInteger)cost;
// 將緩存修剪到指定的時間, 大於這個時間的數據都會被清除
- (void)trimToAge:(NSTimeInterval)age;
@end
複製代碼
在YYMemoryCahce的初始化方法中, 對一些屬性進行了默認參數配置
@implementation YYMemoryCache {
pthread_mutex_t _lock; // 互斥鎖
_YYLinkedMap *_lru; // least recent used
dispatch_queue_t _queue; // 串行隊列
}
- (instancetype)init {
self = super.init;
// 初始化鎖
pthread_mutex_init(&_lock, NULL);
// 初始化雙向鏈表
_lru = [_YYLinkedMap new];
// 串行隊列, 用於執行修剪操做, 爲何是串行隊列?
_queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);
_countLimit = NSUIntegerMax;
_costLimit = NSUIntegerMax;
_ageLimit = DBL_MAX;
_autoTrimInterval = 5.0;
_shouldRemoveAllObjectsOnMemoryWarning = YES;
_shouldRemoveAllObjectsWhenEnteringBackground = YES;
// 註冊App的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidReceiveMemoryWarningNotification) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_appDidEnterBackgroundNotification) name:UIApplicationDidEnterBackgroundNotification object:nil];
// 自動清除功能
[self _trimRecursively];
return self;
}
複製代碼
在init方法中調用了_trimRecursively方法.
- (void)_trimRecursively {
// 使用weakself, block不會對self進行強引用
__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修飾, 防止self被提早銷燬
__strong typeof(_self) self = _self;
if (!self) return;
[self _trimInBackground];
[self _trimRecursively];
});
}
- (void)_trimInBackground {
// 爲何是串行隊列? 串行隊列中的任務會等待上個任務執行完成之後才執行, 是想按照cost->count->age的順序去清除緩存
dispatch_async(_queue, ^{
[self _trimToCost:self->_costLimit];
[self _trimToCount:self->_countLimit];
[self _trimToAge:self->_ageLimit];
});
}
複製代碼
_trimRecursively
方法中使用一個dispatch_after
進行延時操做, 延時時間就是屬性autoTrimInterval
, 經過延時遞歸調用_trimRecursively
方法實現自動清理緩存的功能.
在_trimRecursively
中爲何使用__weak
和__strong
去修飾self
呢?
當不使用__weak
和__strong
修飾self
的時候, demo以下:
@interface MyObject : NSObject
@end
@implementation MyObject
- (void)_trimRecursively {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSLog(@"1-----");
if (!self) return; // block會捕獲self, copy, self引用計數加一.
[self _trimRecursively]; // 由於這裏是異步調用, 不會阻塞當前線程(遞歸調用方法中又會重複對self進行 copy和release操做)
NSLog(@"2-----"); // 執行完block之後, block釋放對self的引用, release, self引用計數減一.
});
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
MyObject *obj = [[MyObject alloc] init];
[obj _trimRecursively];
}
@end
複製代碼
當viewDidLoad
執行完成之後, obj
就應該被釋放, 可是在_trimRecursively
的block中一直存在對obj
的強引用, 因此就形成了obj永遠不會被釋放.
_trimToCost
_trimToCount
_trimToAge
三種清理緩存的方法處理是同樣的, 只是清除緩存的依據不同, 因此這裏只分析了一種狀況.
// static修飾能夠在多個文件中定義
// inline內聯函數, 提升效率, 在調用的時候是在調用出直接替換, 而普通的函數須要開闢棧空間(push pop操做)
static inline dispatch_queue_t YYMemoryCacheGetReleaseQueue() {
return dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
}
複製代碼
- (void)_trimToCost:(NSUInteger)costLimit {
BOOL finish = NO;
// 避免多線程訪問出現數據錯亂, 因此選擇加鎖
pthread_mutex_lock(&_lock);
if (costLimit == 0) {
// 若是內存開銷設置爲0, 表示清空緩存
[_lru removeAll];
finish = YES;
} else if (_lru->_totalCost <= costLimit) {
// 若是當前緩存的開銷小於指定限制開銷, 不須要清理
finish = YES;
}
pthread_mutex_unlock(&_lock);
if (finish) return;
// 建立一個數組, 數組中存放的是結點, 爲了在指定的線程 異步或者同步release
NSMutableArray *holder = [NSMutableArray new];
// 這裏的時間複雜度是O(n)???
while (!finish) {
// 嘗試加鎖, 若是成功就爲0
if (pthread_mutex_trylock(&_lock) == 0) {
// 將須要移除的結點添加到數組中
if (_lru->_totalCost > costLimit) {
// 這裏node雖然從dic字典和雙向鏈表中移除了, 可是又加入了數組中, 因此這裏node並不會被銷燬
_YYLinkedMapNode *node = [_lru removeTailNode];
if (node) [holder addObject:node];
} else {
finish = YES;
}
pthread_mutex_unlock(&_lock);
} else {
// 爲了防止爭奪資源, sleep10ms
usleep(10 * 1000); //10 ms
}
}
// holder中保存了須要release的node結點
if (holder.count) {
// 根據配置選擇是否在主線程中release結點資源
// YYMemoryCacheGetReleaseQueue()是內聯函數, 建立的全局併發隊列
dispatch_queue_t queue = _lru->_releaseOnMainThread ? dispatch_get_main_queue() : YYMemoryCacheGetReleaseQueue();
// 在隊列中移除
dispatch_async(queue, ^{
// 這裏個人理解是 block會對holder強引用, 當block執行完成之後holder的引用計數減1
[holder count]; // release in queue
});
}
// 由於block中對holder有強引用, 因此出了}之後, holder的引用計數減1, 不會被銷燬, 而是在block中銷燬(實如今指定的隊列中銷燬).
}
複製代碼
YYMemoryCache中提供了一些配置屬性, 經過重寫setter方法, 實現對YYLinkedMap的配置
- (NSUInteger)totalCount {
pthread_mutex_lock(&_lock);
NSUInteger count = _lru->_totalCount;
pthread_mutex_unlock(&_lock);
return count;
}
- (NSUInteger)totalCost {
pthread_mutex_lock(&_lock);
NSUInteger totalCost = _lru->_totalCost;
pthread_mutex_unlock(&_lock);
return totalCost;
}
- (BOOL)releaseOnMainThread {
pthread_mutex_lock(&_lock);
BOOL releaseOnMainThread = _lru->_releaseOnMainThread;
pthread_mutex_unlock(&_lock);
return releaseOnMainThread;
}
- (void)setReleaseOnMainThread:(BOOL)releaseOnMainThread {
pthread_mutex_lock(&_lock);
_lru->_releaseOnMainThread = releaseOnMainThread;
pthread_mutex_unlock(&_lock);
}
- (BOOL)releaseAsynchronously {
pthread_mutex_lock(&_lock);
BOOL releaseAsynchronously = _lru->_releaseAsynchronously;
pthread_mutex_unlock(&_lock);
return releaseAsynchronously;
}
複製代碼
查找:
- (BOOL)containsObjectForKey:(id)key {
if (!key) return NO;
// 由於須要操做map中的數據, 加鎖
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);
// 先從字典中獲取node
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
if (node) {
// 由於操做了node, 因此須要將node的時間修改爲當前時間
node->_time = CACurrentMediaTime();
// 操做告終點, 將結點設置爲頭結點.
[_lru bringNodeToHead:node];
}
pthread_mutex_unlock(&_lock);
return node ? node->_value : nil;
}
複製代碼
增長:
- (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;
// 若是object爲nil, 說明是移除key
if (!object) {
[self removeObjectForKey:key];
return;
}
pthread_mutex_lock(&_lock);
// 將node存儲在字典中
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
// 獲取當前時間, 用於設置node的操做時間
NSTimeInterval now = CACurrentMediaTime();
if (node) {
// 若是node已經存在緩存中, 須要將node的time修改, 而且將node添加到表頭
_lru->_totalCost -= node->_cost;
_lru->_totalCost += cost;
node->_cost = cost;
node->_time = now;
node->_value = object;
[_lru bringNodeToHead:node];
} else {
// 若是node不存在, 須要建立node, 而且添加到表頭
node = [_YYLinkedMapNode new];
node->_cost = cost;
node->_time = now;
node->_key = key;
node->_value = object;
[_lru insertNodeAtHead:node];
}
// 添加完成之後, 由於_totalCost增長, 須要檢查總開銷是否超過了設定的標準_costLimit
if (_lru->_totalCost > _costLimit) {
// 在串行隊列中異步清除緩存
dispatch_async(_queue, ^{
[self trimToCost:_costLimit];
});
}
// 添加完成之後, 由於_totalCount增長, 須要檢查_totalCount是否大於_countLimit
if (_lru->_totalCount > _countLimit) {
// 由於每次只增長一個node, 因此只須要移除最後一個尾結點便可
_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);
// 獲取須要移除的node
_YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
if (node) {
[_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);
}
- (void)removeAllObjects {
pthread_mutex_lock(&_lock);
[_lru removeAll];
pthread_mutex_unlock(&_lock);
}
複製代碼
- (NSString *)description {
if (_name) return [NSString stringWithFormat:@"<%@: %p> (%@)", self.class, self, _name];
else return [NSString stringWithFormat:@"<%@: %p>", self.class, self];
}
複製代碼
重寫description
方法, 當log YYMemoryCache
的對象時就會調用這個方法, log相關信息.
LRU(least-recently-used): 最近最少使用, 是一種淘汰算法, 若是最近模塊數據被訪問, 那麼未來這塊數據被訪問的機率也很大. 最久沒有被訪問的數據, 未來被訪問的機率會很小. 將最久沒有使用的淘汰掉, 須要使用一個time字段, 記錄每次訪問的時間, 當須要淘汰的時候, 就選擇time最大的淘汰.
MRU(most-recently-used): 最近最常使用, 和LRU相反.
YYMemoryCache須要操做資源時加鎖. Pthread_mutex鎖.
YYMemoryCache中對清除緩存的操做 訪問_YYLinkedMap中的數據 以及getter setter方法都加鎖處理, 避免多線程訪問的時候出現數據錯亂.
// dispatch_semaphore是信號量, 當信號量爲1時也能夠當作鎖來使用, 當信號量小於1時就會等待
dispatch_semaphore_t lock = dispatch_semaphore_create(1);
dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER);
...
dispatch_semaphore_signal(lock);
pthread_mutex_t lock;
pthread_mutex_init(&lock, NULL);
pthread_mutex_lock(&lock);
...
pthread_mutex_unlock(&lock);
複製代碼
當沒有等待的狀況下, dispatch_semaphore的性能要比pthread_mutext高, 可是一旦出現等待的狀況, 性能就會降低不少.