YYCache 源碼學習—YYMemoryCache

YYCache是一個高性能的緩存工具,本着閱讀優秀的人的代碼就至關於與優秀的人交流的本意,我拜讀了 YYCache 的開源代碼。這篇文章是本身閱讀源碼時的記錄。node

  • 介紹
  • 代碼拆解
    • YYMemoryCache
  • 其它知識點
    • 異步釋放對象
    • 底層類的使用

成員介紹

項目結構

  • YYCache
    • YYMemoryCache
      • _YYLinkedMap
      • _YYLinkedMapNode
    • YYDiskCache
      • YYKVStorage
      • YYKVStorageItem

成員職責

類名 職責
YYCache 對 YYMemoryCache 和 YYDiskCache 的操做進行了封裝
YYMemoryCache 線程安全的內存緩存,支持手動和自動清除緩存及對象釋放控制
_YYLinkedMap 雙向鏈表,供 YYMemoryCache 使用來支持 LRU (least-recently-used) 淘汰算法
_YYLinkedMapNode _YYLinkedMap 的結點
YYDiskCache 磁盤緩存
YYKVStorage YYDiskCache 的底層實現類,用於管理磁盤緩存
YYKVStorageItem 內置在 YYKVStorage 中,是 YYKVStorage 內部用於封裝某個緩存的類

代碼拆解

YYCache

YYCache 提供了最外層的緩存操做方法,而這些方法都是對 YYMemoryCache 及 YYDiskCache 操做的封裝。git

在 YYCache 中對緩存內容進行的操做都是先調用 YYMemoryCache 再調用 YYDiskCache,例如:github

// 是否包含
- (BOOL)containsObjectForKey:(NSString *)key {
    return [_memoryCache containsObjectForKey:key] || [_diskCache containsObjectForKey:key];
}

// 根據 key 查找
- (id<NSCoding>)objectForKey:(NSString *)key {
    id<NSCoding> object = [_memoryCache objectForKey:key];
    if (!object) {
        object = [_diskCache objectForKey:key];
        if (object) {
            [_memoryCache setObject:object forKey:key];
        }
    }
    return object;
}

// 根據 key 設置緩存
- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    [_memoryCache setObject:object forKey:key];
    [_diskCache setObject:object forKey:key];
}

// 根據 key 刪除某個緩存
- (void)removeObjectForKey:(NSString *)key {
    [_memoryCache removeObjectForKey:key];
    [_diskCache removeObjectForKey:key];
}

// 刪除全部緩存
- (void)removeAllObjects {
    [_memoryCache removeAllObjects];
    [_diskCache removeAllObjects];
}
複製代碼

YYMemoryCache

LRU 淘汰算法

YYMemoryCache 使用了 _YYLinkedMap 及 _ YYLInkedMapNode 實現了 LRU 淘汰算法。算法

LRU(Least recently used,最近最少使用)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是「若是數據最近被訪問過,那麼未來被訪問的概率也更高」。緩存

在 YYMemoryCache 中使用了雙向鏈表結構來處理緩存:安全

  1. 寫入一個新的緩存時,將緩存接頭插入到鏈表頭部。
  2. 訪問一個已有的緩存時,將被訪問的緩存結點移動到鏈表頭部。
  3. 清理緩存時,從鏈表的尾部開始逐個清理。

知道了 YYMemoryCache 是怎麼處理緩存以後,咱們來看看這個雙向鏈表的具體實現吧。bash

_YYLinkedMap數據結構

/**
 定義的雙向鏈表,是非線程安全的
 
 不推薦直接使用
 */
@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic; // do not set object directly
    NSUInteger _totalCost;
    NSUInteger _totalCount;
    _YYLinkedMapNode *_head; // MRU, do not change it directly
    _YYLinkedMapNode *_tail; // LRU, do not change it directly
    BOOL _releaseOnMainThread;
    BOOL _releaseAsynchronously;
}

// 在鏈表頭部插入一個新的結點
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

// 將某個結點移動到鏈表頭部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

/// Remove a inner node and update the total cost.
/// Node should already inside the dic.
- (void)removeNode:(_YYLinkedMapNode *)node;

/// Remove tail node if exist.
- (_YYLinkedMapNode *)removeTailNode;

/// Remove all node in background queue.
- (void)removeAll;

@end
複製代碼

_YYLinkedMapNode多線程

@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // 前節點
    __unsafe_unretained _YYLinkedMapNode *_next; // 後節點
    id _key; // 緩存的 key
    id _value; // 緩存數據
    NSUInteger _cost; // 佔用大小
    NSTimeInterval _time; // 最後一次使用時間
}
@end
複製代碼

關於雙向鏈表的操做這裏就不展開了,若是對鏈表的操做不熟悉的,能夠去前面的文章數據結構學習:線性表(鏈式存儲)中回顧一下。app

緩存控制

YYCache 經過三個維度進行緩存的控制:緩存開銷,緩存數量,緩存時間。咱們能夠手動根據這三個維度去清理緩存,YYCache 自身也有自動清理緩存的策略,而且 YYCache 還監聽了 UIApplicationDidReceiveMemoryWarningNotification 及 UIApplicationDidEnterBackgroundNotification 兩個系統通知,在內存報警和進入後臺時根據用戶的配置選擇是否清空全部緩存。

自動清理

在 YYCache 初始化時,就開啓了緩存的自動清理功能以及監聽了進入後臺和內存報警的通知:

- (instancetype)init {
    self = super.init;
    
    .....
    
    [[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;
}
複製代碼

自動清理的實現:

- (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];
    });
}

- (void)_trimInBackground {
    dispatch_async(_queue, ^{
        [self _trimToCost:self->_costLimit];
        [self _trimToCount:self->_countLimit];
        [self _trimToAge:self->_ageLimit];
    });
}
複製代碼

這裏的自動清理是一個遞歸調用,按照 cost,count,age 的順序清理掉不符合要求的數據。

通知監聽的實現:

- (void)_appDidReceiveMemoryWarningNotification {
    if (self.didReceiveMemoryWarningBlock) {
        self.didReceiveMemoryWarningBlock(self);
    }
    if (self.shouldRemoveAllObjectsOnMemoryWarning) {
        [self removeAllObjects];
    }
}

- (void)_appDidEnterBackgroundNotification {
    if (self.didEnterBackgroundBlock) {
        self.didEnterBackgroundBlock(self);
    }
    if (self.shouldRemoveAllObjectsWhenEnteringBackground) {
        [self removeAllObjects];
    }
}
複製代碼

YYCache 提供了兩個屬性:

// 是否在收到內存警告時刪除所有緩存。默認爲 YES
@property BOOL shouldRemoveAllObjectsOnMemoryWarning;

// 是否在進入後臺時刪除所有緩存。默認爲 YES
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;
複製代碼

手動清理

其實上面的自動清理中調用的修剪緩存的方法,所有都提供了外部調用的接口:

- (void)trimToCount:(NSUInteger)count;

- (void)trimToCost:(NSUInteger)cost;

- (void)trimToAge:(NSTimeInterval)age;
複製代碼

手動清理的時候,咱們能夠根據本身的需求對緩存進行控制。

學到的什麼

異步釋放對象

- (void)removeObjectForKey:(id)key {
    
    if (!key) return;
    
    pthread_mutex_lock(&_lock);
    _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);
}
複製代碼

node 在執行這個方法後出了做用域,reference 減一,可是 block 裏面調用 node,使node 被這個 queue Hold 住,reference 加一, 那麼,執行完這個 block 以後,reference count 減一,就達到了再對應線程裏面釋放目的。

底層類的使用

在實現雙向鏈表時,徹底可使用 NSDictionary,可是源碼中使用了更加底層的 CFDictionary 來提升緩存的性能。

多線程的使用

在源碼中能夠看到大量使用了多線程來將緩存的自動清理和釋放放到子線程中執行,對性能的提高有很大的幫助。

總結

看了這麼優秀的源碼,咱們能夠看到做者設計接口的一些思路,看到做者爲性能提高作的一些努力。功能的東西大概就是這些,每一個人都能說上幾句,可是設計思路以及爲提高性能作的一些細節更是咱們須要學習的東西。

相關文章
相關標籤/搜索