LRU - 缓存淘汰算法

1.简介

LRU (英文:Least Recently Used), 意为最近最少使用,这个算法的精髓在于如果一块数据最近被访问,那么它将来被访问的几率也很高,根据数据的历史访问来淘汰长时间未使用的数据。

这篇文章主要分享一下关于内存缓存在iOS 中运用,主要分析一下第三方框架中LRU的运用,包括 LottieYYCache.

2.算法实现

缓存淘汰算法

1.新添加的数据放在头部 2.被访问到的数据放在头部3.超过最大缓存量的数据将被移除。

3.运用

1.Lottie

Lottie

LOTAnimationCache 这个类是LRU的最简单的使用,主要是缓存动画,分别看一下 .h .m 文件的实现。

/// 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,我们可以发现 一个缓存类的实现无非以上这几个接口,主要实现起来也特别简单。

//首先这是定义一个最大的缓存数量
static const NSInteger kLOTCacheSize = 50;

//类实现中主要维护两张表,字典通过key-value pair存储动画,用数组存储key
@implementation LOTAnimationCache {
  NSMutableDictionary *animationsCache_;// 储存动画
  NSMutableArray *lruOrderArray_; //保存key
}
//单例的实现,会iOS 的都会写
+ (instancetype)sharedCache {
  static LOTAnimationCache *sharedCache = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
    sharedCache = [[self alloc] init];
  });
  return sharedCache;
}
//本类初始化的时候,初始化数组和字典
- (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) {
        //数组第一个key就是最早存入数组
        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];
//更新数组key
  [lruOrderArray_ removeObject:key];
  [lruOrderArray_ addObject:key];
  return animation;
}
//清除缓存 ,一般在收到内存警告的时候执行此操作,也是一个缓存类必须提供的接口
- (void)clearCache {
  [animationsCache_ removeAllObjects];
  [lruOrderArray_ removeAllObjects];
}
// 移除对应key 的缓存
- (void)removeAnimationForKey:(NSString *)key {
  [lruOrderArray_ removeObject:key];
  [animationsCache_ removeObjectForKey:key];
}
// 销毁整个缓存
- (void)disableCaching {
  [self clearCache];
  animationsCache_ = nil;
  lruOrderArray_ = nil;
}

Lottie 可能是我遇到最简单的缓存类了,也是最容易入门LRU的缓存类,实现起来也是很容易的。

2.YYCache
YYCache 中主要使用双向链表来实现内存缓存,主要分析一下主要实现思路,首先看一下简介

 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.可以被 数量,消耗 和日期来控制,NSCache 是不明确的
3.当收到内存警告或者进入后台的时候自动配置来移除对象。

  1. YYMemoryCache `Access Methods 时间复杂度是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 对应的值
- (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 数量 移除缓存
- (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;

首先看一下底层链表的实现:
1.定义一个节点类
主要包括 前后节点指针,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已经释放掉了");
}
@end
_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
}

/// Insert a node at head and update the total cost.
// 插入一个节点到头结点
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

/// Bring a inner node to header.
//将内部的一个节点放到头部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

/// Remove a inner node and update the total cost.
// 移除一个节点并更新总数
- (void)removeNode:(_YYLinkedMapNode *)node;

/// Remove tail node if exist.
//如果存在尾节点 移除
- (_YYLinkedMapNode *)removeTailNode;

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

@end

初始化一个链表,初始化的时候创建一个字典

@implementation _YYLinkedMap

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

插入一个节点到头部
这个方法等同于第一步 => 添加新数据

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node{
//存入字典表中
  CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node->_value));
如果存在则将当前node 的置为首位,不存在的话,初始化
    if (_head) {
        node->_next = _head;
        _head->_prev = node;
        _head = node;
    }
    else {
        _head = _tail = node;
    }
}

将任意一个节点添加到头部
这个方法等同于第二步 => 被访问的数据移到头部

- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
    if (_head == node) return; // 如果本身是 head ,return 
    //如果是尾节点,重新赋值尾节点
    if (_tail == node) {
        _tail = node->_prev;
        _tail->_next = nil;
    } else {
//如果是中间的节点,重新赋值 prev next 指针
        node->_next->_prev = node->_prev;
        node->_prev->_next = node->_next;
    }
//将拿到的节点添加到头部
    node->_next = _head;
    node->_prev = nil;
    _head->_prev = node;
    _head = node;
}

移除一个节点
用于缓存淘汰算法

- (void)removeNode:(_YYLinkedMapNode *)node {
    CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));
    _totalCost -= node->_cost;
    _totalCount--;
   // 源码的作者这一段写的是精华中的精华代码,思路严谨,也是体现使用__unsafe_unretained精华所在,执行效率很高,各位可以好好体会
    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;
}

移除尾部节点并返回这个节点,这是很多第三方接口设计之精华

- (_YYLinkedMapNode *)removeTailNode {
    if (!_tail) return nil;
    _YYLinkedMapNode *tail = _tail;
    CFDictionaryRemoveValue(_dic, (__bridge const void *)(_tail->_key));
    _totalCost -= _tail->_cost;
    _totalCount--;
   // 如果只有一个节点 ,直接nil
    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) {
        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 循环的方式移除尾节点,直至满足条件
    //这里有一个小技巧是,将移除的节点添加到一个数组中,然后在子线程释放
    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 接口的实现,主要是基于底层链表的实现

存储方法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);
// 拿到一个节点
    _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]; //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));
   //获取到该节点更新时间 并放置于链表的头部
    if (node) {
        node->_time = CACurrentMediaTime();
        [_lru bringNodeToHead:node];
    }
    pthread_mutex_unlock(&_lock);
    return node ? node->_value : nil;
}

移除key 对应的 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);
}

移除所有的数据

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

以上是YYMemoryCache 利用 LRU 缓存淘汰算法实现的内存缓存,当然源码作者使用了很多方法来处理性能,例如在YYMemoryCache 在初始的时候,便开始5s递归剪枝,存储的时候也检查变量进行剪枝,个人认为在存储的时候可以不用,这方面也可提升性能,没必要频繁的去剪枝缓存淘汰数据。
在YYDiskCache 存储中也使用了LRU缓存淘汰算法,基本的原理和实现都是一样的,YYDiskCache 主要是使用 SQLite 和 File System来进行缓存,后面有时间也可以和大家分享一下。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容