LRU - 緩存淘汰算法

1.簡介

LRU (英文:Least Recently Used), 意為最近最少使用闷板,這個算法的精髓在于如果一塊數(shù)據(jù)最近被訪問技扼,那么它將來被訪問的幾率也很高铸本,根據(jù)數(shù)據(jù)的歷史訪問來淘汰長時間未使用的數(shù)據(jù)硕糊。

這篇文章主要分享一下關(guān)于內(nèi)存緩存在iOS 中運用峦嗤,主要分析一下第三方框架中LRU的運用蕊唐,包括 LottieYYCache.

2.算法實現(xiàn)

緩存淘汰算法

1.新添加的數(shù)據(jù)放在頭部 2.被訪問到的數(shù)據(jù)放在頭部3.超過最大緩存量的數(shù)據(jù)將被移除。

3.運用

1.Lottie

Lottie

LOTAnimationCache 這個類是LRU的最簡單的使用烁设,主要是緩存動畫,分別看一下 .h .m 文件的實現(xiàn)。

/// Global Cache 單例類
+ (instancetype)sharedCache;

/// Adds animation to the cache  主要添加對象API
- (void)addAnimation:(LOTComposition *)animation forKey:(NSString *)key;

/// Returns animation from cache.  獲取緩存
- (LOTComposition * _Nullable)animationForKey:(NSString *)key;

/// Removes a specific animation from the cache 移除緩存
- (void)removeAnimationForKey:(NSString *)key;

/// Clears Everything from the Cache 清除緩存
- (void)clearCache;

/// Disables Caching Animation Model Objects 銷毀緩存模型
- (void)disableCaching;

通過上面主要接口的API,我們可以發(fā)現(xiàn) 一個緩存類的實現(xiàn)無非以上這幾個接口装黑,主要實現(xiàn)起來也特別簡單副瀑。

//首先這是定義一個最大的緩存數(shù)量
static const NSInteger kLOTCacheSize = 50;

//類實現(xiàn)中主要維護(hù)兩張表,字典通過key-value pair存儲動畫,用數(shù)組存儲key
@implementation LOTAnimationCache {
  NSMutableDictionary *animationsCache_;// 儲存動畫
  NSMutableArray *lruOrderArray_; //保存key
}
//單例的實現(xiàn)恋谭,會iOS 的都會寫
+ (instancetype)sharedCache {
  static LOTAnimationCache *sharedCache = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    sharedCache = [[self alloc] init];
  });
  return sharedCache;
}
//本類初始化的時候糠睡,初始化數(shù)組和字典
- (instancetype)init {
  self = [super init];
  if (self) {
    animationsCache_ = [[NSMutableDictionary alloc] init];
    lruOrderArray_ = [[NSMutableArray alloc] init];
  }
  return self;
}
//這是最主要的方法
- (void)addAnimation:(LOTComposition *)animation forKey:(NSString *)key{
//清除超過最大緩存量的值
 if (lruOrderArray_.count >= kLOTCacheSize) {
        //數(shù)組第一個key就是最早存入數(shù)組
        NSString *oldKey = lruOrderArray_.firstObject;
       //移除舊key
        [lruOrderArray_ removeObject:oldKey];
       //移除值
        [animationsCache_ removeObjectForKey:oldKey];
    }
    //移除舊key
    [lruOrderArray_ removeObject:key];
    //添加新key
    [lruOrderArray_ addObject:key];
    //存儲值
    [animationsCache_ setObject:animation forKey:key];
}
//通過key 獲取 value
- (LOTComposition *)animationForKey:(NSString *)key {
  if (!key) {
    return nil;
  }
//從 字典中獲取 value
  LOTComposition *animation = [animationsCache_ objectForKey:key];
//更新數(shù)組key
  [lruOrderArray_ removeObject:key];
  [lruOrderArray_ addObject:key];
  return animation;
}
//清除緩存 ,一般在收到內(nèi)存警告的時候執(zhí)行此操作疚颊,也是一個緩存類必須提供的接口
- (void)clearCache {
  [animationsCache_ removeAllObjects];
  [lruOrderArray_ removeAllObjects];
}
// 移除對應(yīng)key 的緩存
- (void)removeAnimationForKey:(NSString *)key {
  [lruOrderArray_ removeObject:key];
  [animationsCache_ removeObjectForKey:key];
}
// 銷毀整個緩存
- (void)disableCaching {
  [self clearCache];
  animationsCache_ = nil;
  lruOrderArray_ = nil;
}

Lottie 可能是我遇到最簡單的緩存類了狈孔,也是最容易入門LRU的緩存類,實現(xiàn)起來也是很容易的。

2.YYCache
YYCache 中主要使用雙向鏈表來實現(xiàn)內(nèi)存緩存材义,主要分析一下主要實現(xiàn)思路均抽,首先看一下簡介

 YYMemoryCache is a fast in-memory cache that stores key-value pairs.
 In contrast to NSDictionary, keys are retained and not copied.
 The API and performance is similar to `NSCache`, all methods are thread-safe.

YYMemoryCache 是一個用來key-value 鍵值對。
與NSDictionary 形成對比的的是keys 會被持有 但是不會被拷貝其掂。
API 和性能類似于NSCache油挥,所有的方法都是線程安全的。

 YYMemoryCache objects differ from NSCache in a few ways:
 
 * It uses LRU (least-recently-used) to remove objects; NSCache's eviction method
   is non-deterministic.
 * It can be controlled by cost, count and age; NSCache's limits are imprecise.
 * It can be configured to automatically evict objects when receive memory 
   warning or app enter background.
 
 The time of `Access Methods` in YYMemoryCache is typically in constant time (O(1)).

YYMemoryCache 有以下幾點不同于NSCache:
1.使用LRU(最近最少使用) 來移除對象款熬, NSCache 是不確定的
2.可以被 數(shù)量深寥,消耗 和日期來控制,NSCache 是不明確的
3.當(dāng)收到內(nèi)存警告或者進(jìn)入后臺的時候自動配置來移除對象贤牛。

  1. YYMemoryCache `Access Methods 時間復(fù)雜度是O(1).

主要的方法

#pragma mark - Access Methods

/**
 Returns a Boolean value that indicates whether a given key is in cache.
 */
//判斷cache 中是否包含key
- (BOOL)containsObjectForKey:(id)key;

/**
 Returns the value associated with a given key.
 */
//返回給定的key 對用的值
- (nullable id)objectForKey:(id)key;

/**
 Sets the value of the specified key in the cache (0 cost).
 */
//賦值 key-value  0消耗
- (void)setObject:(nullable id)object forKey:(id)key;

/**
 Sets the value of the specified key in the cache, and associates the key-value 
 pair with the specified cost.
 */
//賦值 key-value  cost消耗
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;

/**
 Removes the value of the specified key in the cache.
 */
// 移除指定key 對應(yīng)的值
- (void)removeObjectForKey:(id)key;

/**
 Empties the cache immediately.
 */
//移除所有的值
- (void)removeAllObjects;

// 淘汰算法
#pragma mark - Trim
/**
 Removes objects from the cache with LRU, until the `totalCount` is below or equal to
 the specified value.
 */
//對count 數(shù)量 移除緩存
- (void)trimToCount:(NSUInteger)count;

/**
 Removes objects from the cache with LRU, until the `totalCost` is or equal to
 */
//對cost  移除緩存
- (void)trimToCost:(NSUInteger)cost;

/**
 Removes objects from the cache with LRU, until all expiry objects removed by the
 specified value.
 */
// 對age(緩存時間) 移除緩存
- (void)trimToAge:(NSTimeInterval)age;

首先看一下底層鏈表的實現(xiàn):
1.定義一個節(jié)點類
主要包括 前后節(jié)點指針惋鹅,key 和 value 以及_cost _time

@interface _YYLinkedMapNode : NSObject {
    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    id _key;
    id _value;
    NSUInteger _cost;
    NSTimeInterval _time;
}
@end

@implementation _YYLinkedMapNode

- (void)dealloc{
  
    printf("本類_YYLinkedMapNode已經(jīng)釋放掉了");
}
@end
_YYLinkedMap

接下來看一下雙向鏈表的實現(xiàn),
主要包括一個字典負(fù)責(zé)存儲,頭結(jié)點和尾節(jié)點殉簸,總消耗和總數(shù)量

@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
}

/// Insert a node at head and update the total cost.
// 插入一個節(jié)點到頭結(jié)點
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

/// Bring a inner node to header.
//將內(nèi)部的一個節(jié)點放到頭部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

/// Remove a inner node and update the total cost.
// 移除一個節(jié)點并更新總數(shù)
- (void)removeNode:(_YYLinkedMapNode *)node;

/// Remove tail node if exist.
//如果存在尾節(jié)點 移除
- (_YYLinkedMapNode *)removeTailNode;

/// Remove all node in background queue.
//移除所有
- (void)removeAll;

@end

初始化一個鏈表,初始化的時候創(chuàng)建一個字典

@implementation _YYLinkedMap

- (instancetype)init {
    self = [super init];
    
  //初始化一個字典
    _dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
 
    return self;
}
- (void)dealloc {
    CFRelease(_dic);
}

插入一個節(jié)點到頭部
這個方法等同于第一步 => 添加新數(shù)據(jù)

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node{
//存入字典表中
  CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node->_value));
如果存在則將當(dāng)前node 的置為首位负饲,不存在的話,初始化
    if (_head) {
        node->_next = _head;
        _head->_prev = node;
        _head = node;
    }
    else {
        _head = _tail = node;
    }
}

將任意一個節(jié)點添加到頭部
這個方法等同于第二步 => 被訪問的數(shù)據(jù)移到頭部

- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
    if (_head == node) return; // 如果本身是 head ,return 
    //如果是尾節(jié)點喂链,重新賦值尾節(jié)點
    if (_tail == node) {
        _tail = node->_prev;
        _tail->_next = nil;
    } else {
//如果是中間的節(jié)點返十,重新賦值 prev next 指針
        node->_next->_prev = node->_prev;
        node->_prev->_next = node->_next;
    }
//將拿到的節(jié)點添加到頭部
    node->_next = _head;
    node->_prev = nil;
    _head->_prev = node;
    _head = node;
}

移除一個節(jié)點
用于緩存淘汰算法

- (void)removeNode:(_YYLinkedMapNode *)node {
    CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));
    _totalCost -= node->_cost;
    _totalCount--;
   // 源碼的作者這一段寫的是精華中的精華代碼,思路嚴(yán)謹(jǐn)椭微,也是體現(xiàn)使用__unsafe_unretained精華所在洞坑,執(zhí)行效率很高,各位可以好好體會
    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;
}

移除尾部節(jié)點并返回這個節(jié)點蝇率,這是很多第三方接口設(shè)計之精華

- (_YYLinkedMapNode *)removeTailNode {
    if (!_tail) return nil;
    _YYLinkedMapNode *tail = _tail;
    CFDictionaryRemoveValue(_dic, (__bridge const void *)(_tail->_key));
    _totalCost -= _tail->_cost;
    _totalCount--;
   // 如果只有一個節(jié)點 迟杂,直接nil
    if (_head == _tail) {
        _head = _tail = nil;
    } else {
        _tail = _tail->_prev;
        _tail->_next = nil;
    }
    return tail;
}

移除所有節(jié)點

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

接下面看一下緩存淘汰的一些算法
包括 Cost Count Age 這三種Limit 來淘汰緩存

- (void)_trimToCost:(NSUInteger)costLimit{
// 判斷臨界條件 costLimit = 0 全部移除
// 如果 <= 什么也不需要做
 BOOL finish = NO;
    pthread_mutex_lock(&_lock);
    if (costLimit == 0) {
        [_lru removeAll];
        finish = YES;
    } else if (_lru->_totalCost <= costLimit) {
        finish = YES;
    }
    pthread_mutex_unlock(&_lock);
    if (finish) return;
    
    //這里采用while 循環(huán)的方式移除尾節(jié)點排拷,直至滿足條件
    //這里有一個小技巧是,將移除的節(jié)點添加到一個數(shù)組中锅尘,然后在子線程釋放
    NSMutableArray *holder = [NSMutableArray new];
    while (!finish) {
        if (pthread_mutex_trylock(&_lock) == 0) {
            if (_lru->_totalCost > costLimit) {
                _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
        });
    }
}

以下兩個緩存淘汰一樣的思路监氢,不再展示

- (void)_trimToAge:(NSTimeInterval)ageLimit
- (void)_trimToCount:(NSUInteger)countLimit

接下來看一下YYMemoryCache 主要的緩存API 接口的實現(xiàn)布蔗,主要是基于底層鏈表的實現(xiàn)

存儲方法key - value pair

- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost {
    //容錯判斷
    if (!key) return;
    if (!object) {
        [self removeObjectForKey:key];
        return;
    }
    
    pthread_mutex_lock(&_lock);
// 拿到一個節(jié)點
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    NSTimeInterval now = CACurrentMediaTime();
    if (node) {
  //節(jié)點存在
        _lru->_totalCost -= node->_cost;
        _lru->_totalCost += cost;
        node->_cost = cost;
        node->_time = now;
        node->_value = object;
        [_lru bringNodeToHead:node];
    } else {
        // 節(jié)點不存在 -> 直接添加到頭部
        node = [_YYLinkedMapNode new];
        node->_cost = cost;
        node->_time = now;
        node->_key = key;
        node->_value = object;
        [_lru insertNodeAtHead:node];
    }
   //做一次數(shù)據(jù)的剪枝
  // 超過最大容量,清理內(nèi)存
    if (_lru->_totalCost > _costLimit) {
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];
        });
    }
 // 超過最大數(shù)量浪腐,清理內(nèi)存
 // 小技巧同樣是異步釋放內(nèi)存
    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);
}

接下來是獲取值

- (id)objectForKey:(id)key {
    if (!key) return nil;
    pthread_mutex_lock(&_lock);
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
   //獲取到該節(jié)點更新時間 并放置于鏈表的頭部
    if (node) {
        node->_time = CACurrentMediaTime();
        [_lru bringNodeToHead:node];
    }
    pthread_mutex_unlock(&_lock);
    return node ? node->_value : nil;
}

移除key 對應(yīng)的 value

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

移除所有的數(shù)據(jù)

- (void)removeAllObjects {
    pthread_mutex_lock(&_lock);
    [_lru removeAll];
    pthread_mutex_unlock(&_lock);
}

以上是YYMemoryCache 利用 LRU 緩存淘汰算法實現(xiàn)的內(nèi)存緩存纵揍,當(dāng)然源碼作者使用了很多方法來處理性能,例如在YYMemoryCache 在初始的時候议街,便開始5s遞歸剪枝泽谨,存儲的時候也檢查變量進(jìn)行剪枝,個人認(rèn)為在存儲的時候可以不用特漩,這方面也可提升性能吧雹,沒必要頻繁的去剪枝緩存淘汰數(shù)據(jù)。
在YYDiskCache 存儲中也使用了LRU緩存淘汰算法涂身,基本的原理和實現(xiàn)都是一樣的雄卷,YYDiskCache 主要是使用 SQLite 和 File System來進(jìn)行緩存,后面有時間也可以和大家分享一下访得。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末龙亲,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子悍抑,更是在濱河造成了極大的恐慌鳄炉,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件搜骡,死亡現(xiàn)場離奇詭異拂盯,居然都是意外死亡,警方通過查閱死者的電腦和手機记靡,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門谈竿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人摸吠,你說我怎么就攤上這事空凸。” “怎么了寸痢?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵呀洲,是天一觀的道長。 經(jīng)常有香客問我啼止,道長道逗,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任献烦,我火速辦了婚禮滓窍,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘巩那。我一直安慰自己吏夯,他們只是感情好此蜈,可當(dāng)我...
    茶點故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著锦亦,像睡著了一般舶替。 火紅的嫁衣襯著肌膚如雪令境。 梳的紋絲不亂的頭發(fā)上杠园,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天,我揣著相機與錄音舔庶,去河邊找鬼抛蚁。 笑死,一個胖子當(dāng)著我的面吹牛惕橙,可吹牛的內(nèi)容都是我干的瞧甩。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼弥鹦,長吁一口氣:“原來是場噩夢啊……” “哼肚逸!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起彬坏,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤朦促,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后栓始,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體务冕,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年幻赚,在試婚紗的時候發(fā)現(xiàn)自己被綠了禀忆。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡落恼,死狀恐怖箩退,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情佳谦,我是刑警寧澤戴涝,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站吠昭,受9級特大地震影響喊括,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜矢棚,卻給世界環(huán)境...
    茶點故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一郑什、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧蒲肋,春花似錦蘑拯、人聲如沸钝满。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽弯蚜。三九已至,卻和暖如春剃法,著一層夾襖步出監(jiān)牢的瞬間碎捺,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工贷洲, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留收厨,地道東北人。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓优构,卻偏偏與公主長得像诵叁,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子钦椭,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,914評論 2 355

推薦閱讀更多精彩內(nèi)容