YYCache 源码解析(一):使用方法,架构与内存缓存的设计

YYCache是国内开发者ibireme开源的一个线程安全的高性能缓存组件,代码风格简洁清晰,阅读它的源码有助于建立比较完整的缓存设计的思路,同时也能巩固一下双向链表,线程锁,数据库操作相关的知识。

如果你还没有看过YYCache的源码,那么恭喜你,阅读此文会对理解YYCache的源码有比较大的帮助。

YYCache在架构上包含两个层级的缓存,一个是内存缓存,另一个是磁盘缓存,而且由于原文比较长,笔者将他们分别放在两个文章里面讲解,即分为两个公众号文章来发布:

  1. YYCache 源码解析(一):使用方法,架构与内存缓存的设计
  2. YYCache 源码解析(二):磁盘缓存的设计与缓存组件设计思路

本篇为第一篇,讲解的是:

  1. 基本使用方法
  2. 架构与成员职责划分
  3. YYCache的接口内存缓存的设计

一. 基本使用方法

举一个缓存用户姓名的例子来看一下YYCache的几个API:

    //需要缓存的对象
    NSString *userName = @"Jack";

   //需要缓存的对象在缓存里对应的键
    NSString *key = @"user_name";

    //创建一个YYCache实例:userInfoCache
    YYCache *userInfoCache = [YYCache cacheWithName:@"userInfo"];

    //存入键值对
    [userInfoCache setObject:userName forKey:key withBlock:^{
        NSLog(@"caching object succeed");
    }];

    //判断缓存是否存在
    [userInfoCache containsObjectForKey:key withBlock:^(NSString * _Nonnull key, BOOL contains) {
        if (contains){
            NSLog(@"object exists");
        }
    }];

    //根据key读取数据
    [userInfoCache objectForKey:key withBlock:^(NSString * _Nonnull key, id<NSCoding>  _Nonnull object) {
        NSLog(@"user name : %@",object);
    }];

    //根据key移除缓存
    [userInfoCache removeObjectForKey:key withBlock:^(NSString * _Nonnull key) {
        NSLog(@"remove user name %@",key);
    }];

    //移除所有缓存
    [userInfoCache removeAllObjectsWithBlock:^{
        NSLog(@"removing all cache succeed");
    }];

    //移除所有缓存带进度
    [userInfoCache removeAllObjectsWithProgressBlock:^(int removedCount, int totalCount) {
        NSLog(@"remove all cache objects: removedCount :%d  totalCount : %d",removedCount,totalCount);
    } endBlock:^(BOOL error) {
        if(!error){
            NSLog(@"remove all cache objects: succeed");
        }else{
            NSLog(@"remove all cache objects: failed");
        }
    }];

总体来看这些API与NSCache是差不多的。 再来看一下框架的架构图与成员职责划分。

二. 架构与成员职责划分

架构图

YYCache 架构图

成员职责划分

从架构图上来看,该组件里面的成员并不多:

  • YYCache:提供了最外层的接口,调用了YYMemoryCache与YYDiskCache的相关方法。
  • YYMemoryCache:负责处理容量小,相对高速的内存缓存。线程安全,支持自动和手动清理缓存等功能。
  • _YYLinkedMap:YYMemoryCache使用的双向链表类。
  • _YYLinkedMapNode:是_YYLinkedMap使用的节点类。
  • YYDiskCache:负责处理容量大,相对低速的磁盘缓存。线程安全,支持异步操作,自动和手动清理缓存等功能。
  • YYKVStorage:YYDiskCache的底层实现类,用于管理磁盘缓存。
  • YYKVStorageItem:内置在YYKVStorage中,是YYKVStorage内部用于封装某个缓存的类。

三. YYCache的接口与内存缓存的设计

知道了YYCache的架构图与成员职责划分以后,现在结合代码开始正式讲解。 本章的讲解分为下面2个部分:

  • YYCache
  • YYMemoryCache

YYCache

YYCache给用户提供所有最外层的缓存操作接口,而这些接口的内部内部实际上是调用了YYMemoryCache和YYDiskCache对象的相关方法。

我们来看一下YYCache的属性和接口:

YYCache的属性和接口

@interface YYCache : NSObject


@property (copy, readonly) NSString *name;//缓存名称
@property (strong, readonly) YYMemoryCache *memoryCache;//内存缓存
@property (strong, readonly) YYDiskCache *diskCache;//磁盘缓存

//是否包含某缓存,无回调
- (BOOL)containsObjectForKey:(NSString *)key;
//是否包含某缓存,有回调
- (void)containsObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, BOOL contains))block;

//获取缓存对象,无回调
- (nullable id<NSCoding>)objectForKey:(NSString *)key;
//获取缓存对象,有回调
- (void)objectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key, id<NSCoding> object))block;

//写入缓存对象,无回调
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key;
//写入缓存对象,有回调
- (void)setObject:(nullable id<NSCoding>)object forKey:(NSString *)key withBlock:(nullable void(^)(void))block;

//移除某缓存,无回调
- (void)removeObjectForKey:(NSString *)key;
//移除某缓存,有回调
- (void)removeObjectForKey:(NSString *)key withBlock:(nullable void(^)(NSString *key))block;

//移除所有缓存,无回调
- (void)removeAllObjects;
//移除所有缓存,有回调
- (void)removeAllObjectsWithBlock:(void(^)(void))block;
//移除所有缓存,有进度和完成的回调
- (void)removeAllObjectsWithProgressBlock:(nullable void(^)(int removedCount, int totalCount))progress
                                 endBlock:(nullable void(^)(BOOL error))end;

@end

从上面的接口可以看出YYCache的接口和NSCache很相近,而且在接口上都区分了有无回调的功能。 下面结合代码看一下这些接口是如何实现的:

YYCache的接口实现

下面省略了带有回调的接口,因为与无回调的接口非常接近。

- (BOOL)containsObjectForKey:(NSString *)key {

    //先检查内存缓存是否存在,再检查磁盘缓存是否存在
    return [_memoryCache containsObjectForKey:key] || [_diskCache containsObjectForKey:key];
}

- (id<NSCoding>)objectForKey:(NSString *)key {

    //首先尝试获取内存缓存,然后获取磁盘缓存
    id<NSCoding> object = [_memoryCache objectForKey:key];

    //如果内存缓存不存在,就会去磁盘缓存里面找:如果找到了,则再次写入内存缓存中;如果没找到,就返回nil
    if (!object) {
        object = [_diskCache objectForKey:key];
        if (object) {
            [_memoryCache setObject:object forKey:key];
        }
    }
    return object;
}


- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key {
    //先写入内存缓存,后写入磁盘缓存
    [_memoryCache setObject:object forKey:key];
    [_diskCache setObject:object forKey:key];
}


- (void)removeObjectForKey:(NSString *)key {
    //先移除内存缓存,后移除磁盘缓存
    [_memoryCache removeObjectForKey:key];
    [_diskCache removeObjectForKey:key];
}

- (void)removeAllObjects {
    //先全部移除内存缓存,后全部移除磁盘缓存
    [_memoryCache removeAllObjects];
    [_diskCache removeAllObjects];
}

从上面的接口实现可以看出:在YYCache中,永远都是先访问内存缓存,然后再访问磁盘缓存(包括了写入,读取,查询,删除缓存的操作)。而且关于内存缓存(_memoryCache)的操作,是不存在block回调的。

值得一提的是:在读取缓存的操作中,如果在内存缓存中无法获取对应的缓存,则会去磁盘缓存中寻找。如果在磁盘缓存中找到了对应的缓存,则会将该对象再次写入内存缓存中,保证在下一次尝试获取同一缓存时能够在内存中就能返回,提高速度

OK,现在了解了YYCache的接口以及实现,下面我分别讲解一下YYMemoryCache(内存缓存)和YYDiskCache(磁盘缓存)这两个类。

YYMemoryCache

YYMemoryCache负责处理容量小,相对高速的内存缓存:它将需要缓存的对象与传入的key关联起来,操作类似于NSCache。

但是与NSCache不同的是,YYMemoryCache的内部有:

  • 缓存淘汰算法:使用LRU(least-recently-used) 算法来淘汰(清理)使用频率较低的缓存。
  • 缓存清理策略:使用三个维度来标记,分别是count(缓存数量),cost(开销),age(距上一次的访问时间)。YYMemoryCache提供了分别针对这三个维度的清理缓存的接口。用户可以根据不同的需求(策略)来清理在某一维度超标的缓存。

一个是淘汰算法,另一个是清理维度,乍一看可能没什么太大区别。我在这里先简单区分一下:

缓存淘汰算法的目的在于区分出使用频率高和使用频率低的缓存,当缓存数量达到一定限制的时候会优先清理那些使用频率低的缓存。因为使用频率已经比较低的缓存在将来的使用频率也很有可能会低

缓存清理维度是给每个缓存添加的标记:

  • 如果用户需要删除age(距上一次的访问时间)超过1天的缓存,在YYMemoryCache内部,就会从使用频率最低的那个缓存开始查找,直到所有距上一次的访问时间超过1天的缓存都清理掉为止。
  • 如果用户需要将缓存总开销清理到总开销小于或等于某个值,在YYMemoryCache内部,就会从使用频率最低的那个缓存开始清理,直到总开销小于或等于这个值。
  • 如果用户需要将缓存总数清理到总开销小于或等于某个值,在YYMemoryCache内部,就会从使用频率最低的那个缓存开始清理,直到总开销小于或等于这个值。

可以看出,无论是以哪个维度来清理缓存,都是从缓存使用频率最低的那个缓存开始清理。而YYMemoryCache保留的所有缓存的使用频率的高低,是由LRU这个算法决定的。

现在知道了这二者的区别,下面来具体讲解一下缓存淘汰算法和缓存清理策略:

YYMemoryCache的缓存淘汰算法

在详细讲解这个算法之前我觉得有必要先说一下该算法的核心:

我个人认为LRU缓存替换策略的核心在于如果某个缓存访问的频率越高,就认定用户在将来越有可能访问这个缓存。 所以在这个算法中,将那些最新访问(写入),最多次被访问的缓存移到最前面,然后那些很早之前写入,不经常访问的缓存就被自动放在了后面。这样一来,在保留的缓存个数一定的情况下,留下的缓存都是访问频率比较高的,这样一来也就提升了缓存的命中率。谁都不想留着一些很难被用户再次访问的缓存,毕竟缓存本身也占有一定的资源不是么?

其实这个道理和一些商城类app的商品推荐逻辑是一样的: 如果首页只能展示10个商品,对于一个程序员用户来说,可能推荐的是于那些他最近购买商品类似的机械键盘鼠标,技术书籍或者显示屏之类的商品,而不是一些洋娃娃或是钢笔之类的商品。

那么LRU算法具体是怎么做的呢?

在YYMemoryCache中,使用了双向链表这个数据结构来保存这些缓存:

  • 当写入一个新的缓存时,要把这个缓存节点放在链表头部,并且并且原链表头部的缓存节点要变成现在链表的第二个缓存节点。
  • 当访问一个已有的缓存时,要把这个缓存节点移动到链表头部,原位置两侧的缓存要接上,并且原链表头部的缓存节点要变成现在链表的第二个缓存节点。
  • (根据清理维度)自动清理缓存时,要从链表的最后端逐个清理。

这样一来,就可以保证链表前端的缓存是最近写入过和经常访问过的。而且该算法总是从链表的最后端删除缓存,这也就保证了留下的都是一些“比较新鲜的”缓存。

下面结合代码来讲解一下这个算法的实现:

YYMemoryCache用一个链表节点类来保存某个单独的内存缓存的信息(键,值,缓存时间等),然后用一个双向链表类来保存和管理这些节点。这两个类的名称分别是:

  • _YYLinkedMapNode:链表内的节点类,可以看做是对某个单独内存缓存的封装。
  • _YYLinkedMap:双向链表类,用于保存和管理所有内存缓存(节点)

_YYLinkedMapNode

_YYLinkedMapNode可以被看做是对某个缓存的封装:它包含了该节点上一个和下一个节点的指针,以及缓存的key和对应的值(对象),还有该缓存的开销和访问时间。

@interface _YYLinkedMapNode : NSObject {

    @package
    __unsafe_unretained _YYLinkedMapNode *_prev; // retained by dic
    __unsafe_unretained _YYLinkedMapNode *_next; // retained by dic
    id _key;                        //缓存key
    id _value;                            //key对应值
    NSUInteger _cost;                     //缓存开销
    NSTimeInterval _time;                 //访问时间

}
@end

@implementation _YYLinkedMapNode
@end

下面看一下双向链表类:

_YYLinkedMap

@interface _YYLinkedMap : NSObject {
    @package
    CFMutableDictionaryRef _dic;     // 用于存放节点
    NSUInteger _totalCost;           //总开销
    NSUInteger _totalCount;          //节点总数
    _YYLinkedMapNode *_head;            // 链表的头部结点
    _YYLinkedMapNode *_tail;         // 链表的尾部节点
    BOOL _releaseOnMainThread;             //是否在主线程释放,默认为NO
    BOOL _releaseAsynchronously;     //是否在子线程释放,默认为YES
}

//在链表头部插入某节点
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

//将链表内部的某个节点移到链表头部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

//移除某个节点
- (void)removeNode:(_YYLinkedMapNode *)node;

//移除链表的尾部节点并返回它
- (_YYLinkedMapNode *)removeTailNode;

//移除所有节点(默认在子线程操作)
- (void)removeAll;

@end

从链表类的属性上看:链表类内置了CFMutableDictionaryRef,用于保存节点的键值对,它还持有了链表内节点的总开销,总数量,头尾节点等数据。

可以参考下面这张图来看一下二者的关系:

_YYLinkedMap 与 _YYLinkedMapNode 的关系

看一下_YYLinkedMap的接口的实现:

将节点插入到链表头部:

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {

    //设置该node的值
    CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));

    //增加开销和总缓存数量
    _totalCost += node->_cost;
    _totalCount++;

    if (_head) {

        //如果链表内已经存在头节点,则将这个头节点赋给当前节点的尾指针(原第一个节点变成了现第二个节点)
        node->_next = _head;

        //将该节点赋给现第二个节点的头指针(此时_head指向的节点是先第二个节点)
        _head->_prev = node;

        //将该节点赋给链表的头结点指针(该节点变成了现第一个节点)
        _head = node;

    } else {

        //如果链表内没有头结点,说明是空链表。说明是第一次插入,则将链表的头尾节点都设置为当前节点
        _head = _tail = node;
    }
}

要看懂节点操作的代码只要了解双向链表的特性即可。在双向链表中:

  • 每个节点都有两个分别指向前后节点的指针。所以说每个节点都知道它前一个节点和后一个节点是谁。
  • 链表的头部节点指向它前面节点的指针为空;链表尾部节点指向它后侧节点的指针也为空。

为了便于理解,我们可以把这个抽象概念类比于幼儿园手拉手的小朋友们: 每个小朋友的左手都拉着前面小朋友的右手;每个小朋友的右手都拉着后面小朋友的左手; 而且最前面的小朋友的左手和最后面的小朋友的右手都没有拉任何一个小朋友。

将某个节点移动到链表头部:

- (void)bringNodeToHead:(_YYLinkedMapNode *)node {

    //如果该节点已经是链表头部节点,则立即返回,不做任何操作
    if (_head == node) return;


    if (_tail == node) {

        //如果该节点是链表的尾部节点
        //1. 将该节点的头指针指向的节点变成链表的尾节点(将倒数第二个节点变成倒数第一个节点,即尾部节点)
        _tail = node->_prev;

        //2. 将新的尾部节点的尾部指针置空
        _tail->_next = nil;

    } else {

        //如果该节点是链表头部和尾部以外的节点(中间节点)
        //1. 将该node的头指针指向的节点赋给其尾指针指向的节点的头指针
        node->_next->_prev = node->_prev;

        //2. 将该node的尾指针指向的节点赋给其头指针指向的节点的尾指针
        node->_prev->_next = node->_next;
    }

    //将原头节点赋给该节点的尾指针(原第一个节点变成了现第二个节点)
    node->_next = _head;

    //将当前节点的头节点置空
    node->_prev = nil;

    //将现第二个节点的头结点指向当前节点(此时_head指向的节点是现第二个节点)
    _head->_prev = node;

    //将该节点设置为链表的头节点
    _head = node;
}

第一次看上面的代码我自己是懵逼的,不过如果结合上面小朋友拉手的例子就可以快一点理解。 如果要其中一个小朋友放在队伍的最前面,需要

  • 将原来这个小朋友前后的小朋友的手拉上。
  • 然后将这个小朋友的右手和原来排在第一位的小朋友的左手拉上。

上面说的比较简略,但是相信对大家理解整个过程会有帮助。

也可以再结合链表的图解来看一下:

读者同样可以利用这种思考方式理解下面这段代码:

移除链表中的某个节点:

- (void)removeNode:(_YYLinkedMapNode *)node {

    //除去该node的键对应的值
    CFDictionaryRemoveValue(_dic, (__bridge const void *)(node->_key));

    //减去开销和总缓存数量
    _totalCost -= node->_cost;
    _totalCount--;

    //节点操作
    //1. 将该node的头指针指向的节点赋给其尾指针指向的节点的头指针
    if (node->_next) node->_next->_prev = node->_prev;

    //2. 将该node的尾指针指向的节点赋给其头指针指向的节点的尾指针
    if (node->_prev) node->_prev->_next = node->_next;

    //3. 如果该node就是链表的头结点,则将该node的尾部指针指向的节点赋给链表的头节点(第二变成了第一)
    if (_head == node) _head = node->_next;

    //4. 如果该node就是链表的尾节点,则将该node的头部指针指向的节点赋给链表的尾节点(倒数第二变成了倒数第一)
    if (_tail == node) _tail = node->_prev;
}

移除并返回尾部的node:

- (_YYLinkedMapNode *)removeTailNode {

    //如果不存在尾节点,则返回nil
    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;
}

OK,现在了解了YYMemoryCache底层的节点操作的代码。现在来看一下YYMemoryCache是如何使用它们的。

YYMemoryCache的属性和接口

//YYMemoryCache.h
@interface YYMemoryCache : NSObject

#pragma mark - Attribute

//缓存名称,默认为nil
@property (nullable, copy) NSString *name;

//缓存总数量
@property (readonly) NSUInteger totalCount;

//缓存总开销
@property (readonly) NSUInteger totalCost;


#pragma mark - Limit

//数量上限,默认为NSUIntegerMax,也就是无上限
@property NSUInteger countLimit;

//开销上限,默认为NSUIntegerMax,也就是无上限
@property NSUInteger costLimit;

//缓存时间上限,默认为DBL_MAX,也就是无上限
@property NSTimeInterval ageLimit;

//清理超出上限之外的缓存的操作间隔时间,默认为5s
@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);

//缓存清理是否在后台进行,默认为NO
@property BOOL releaseOnMainThread;

//缓存清理是否异步执行,默认为YES
@property BOOL releaseAsynchronously;


#pragma mark - Access Methods

//是否包含某个缓存
- (BOOL)containsObjectForKey:(id)key;

//获取缓存对象
- (nullable id)objectForKey:(id)key;

//写入缓存对象
- (void)setObject:(nullable id)object forKey:(id)key;

//写入缓存对象,并添加对应的开销
- (void)setObject:(nullable id)object forKey:(id)key withCost:(NSUInteger)cost;

//移除某缓存
- (void)removeObjectForKey:(id)key;

//移除所有缓存
- (void)removeAllObjects;

#pragma mark - Trim

// =========== 缓存清理接口 =========== 
//清理缓存到指定个数
- (void)trimToCount:(NSUInteger)count;

//清理缓存到指定开销
- (void)trimToCost:(NSUInteger)cost;

//清理缓存时间小于指定时间的缓存
- (void)trimToAge:(NSTimeInterval)age;

YYMemoryCache的接口实现

在YYMemoryCache的初始化方法里,会实例化一个_YYLinkedMap的实例来赋给_lru这个成员变量。

- (instancetype)init{
      ....
      _lru = [_YYLinkedMap new];
      ...

  }

然后所有的关于缓存的操作,都要用到_lru这个成员变量,因为它才是在底层持有这些缓存(节点)的双向链表类。下面我们来看一下这些缓存操作接口的实现:

//是否包含某个缓存对象
- (BOOL)containsObjectForKey:(id)key {

    //尝试从内置的字典中获得缓存对象
    if (!key) return NO;
    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);
    _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;
}

//写入某个缓存对象,开销默认为0
- (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;

    if (!object) {
        [self removeObjectForKey:key];
        return;
    }

    pthread_mutex_lock(&_lock);

    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    NSTimeInterval now = CACurrentMediaTime();

    if (node) {
        //如果存在与传入的key值匹配的node,则更新该node的value,cost,time,并将这个node移到链表头部

        //更新总cost
        _lru->_totalCost -= node->_cost;
        _lru->_totalCost += cost;

        //更新node
        node->_cost = cost;
        node->_time = now;
        node->_value = object;

        //将node移动至链表头部
        [_lru bringNodeToHead:node];

    } else {

        //如果不存在与传入的key值匹配的node,则新建一个node,将key,value,cost,time赋给它,并将这个node插入到链表头部
        //新建node,并赋值
        node = [_YYLinkedMapNode new];
        node->_cost = cost;
        node->_time = now;
        node->_key = key;
        node->_value = object;

        //将node插入至链表头部
        [_lru insertNodeAtHead:node];
    }

    //如果cost超过了限制,则进行删除缓存操作(从链表尾部开始删除,直到符合限制要求)
    if (_lru->_totalCost > _costLimit) {
        dispatch_async(_queue, ^{
            [self trimToCost:_costLimit];
        });
    }

    //如果total count超过了限制,则进行删除缓存操作(从链表尾部开始删除,删除一次即可)
    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);
}

//移除某个缓存对象
- (void)removeObjectForKey:(id)key {

    if (!key) return;

    pthread_mutex_lock(&_lock);
    _YYLinkedMapNode *node = CFDictionaryGetValue(_lru->_dic, (__bridge const void *)(key));
    if (node) {

        //内部调用了链表的removeNode:方法
        [_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);
}


//内部调用了链表的removeAll方法
- (void)removeAllObjects {
    pthread_mutex_lock(&_lock);
    [_lru removeAll];
    pthread_mutex_unlock(&_lock);
}

上面的实现是针对缓存的查询,写入,获取操作的,接下来看一下缓存的清理策略。

YYMemoryCache的缓存清理策略

如上文所说,在YYCache中,缓存的清理可以从缓存总数量,缓存总开销,缓存距上一次的访问时间来清理缓存。而且每种维度的清理操作都可以分为自动和手动的方式来进行。

缓存自动清理

缓存的自动清理功能在YYMemoryCache初始化之后就开始了,是一个递归调用的实现:

//YYMemoryCache.m
- (instancetype)init{

    ...

    //开始定期清理
    [self _trimRecursively];

    ...
}


//递归清理,相隔时间为_autoTrimInterval,在初始化之后立即执行
- (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];

    });
}

//清理所有不符合限制的缓存,顺序为:cost,count,age
- (void)_trimInBackground {
    dispatch_async(_queue, ^{

        [self _trimToCost:self->_costLimit];
        [self _trimToCount:self->_countLimit];
        [self _trimToAge:self->_ageLimit];

    });
}
//YYMemoryCache.m
- (void)trimToCount:(NSUInteger)count {
    if (count == 0) {
        [self removeAllObjects];
        return;
    }
    [self _trimToCount:count];
}

- (void)trimToCost:(NSUInteger)cost {
    [self _trimToCost:cost];
}

- (void)trimToAge:(NSTimeInterval)age {
    [self _trimToAge:age];
}

可以看到,YYMemoryCache是按照缓存数量,缓存开销,缓存时间的顺序来自动清空缓存的。我们结合代码看一下它是如何按照缓存数量来清理缓存的(其他两种清理方式类似,暂不给出):

//YYMemoryCache.m

//将内存缓存数量降至等于或小于传入的数量;如果传入的值为0,则删除全部内存缓存
- (void)_trimToCount:(NSUInteger)countLimit {

    BOOL finish = NO;

    pthread_mutex_lock(&_lock);

    //如果传入的参数=0,则删除所有内存缓存
    if (countLimit == 0) {

        [_lru removeAll];
        finish = YES;

    } else if (_lru->_totalCount <= countLimit) {

        //如果当前缓存的总数量已经小于或等于传入的数量,则直接返回YES,不进行清理
        finish = YES;
    }
    pthread_mutex_unlock(&_lock);
    if (finish) return;

    NSMutableArray *holder = [NSMutableArray new];
    while (!finish) {

        //==0的时候说明在尝试加锁的时候,获取锁成功,从而可以进行操作;否则等待10秒(但是不知道为什么是10s而不是2s,5s,等等)
        if (pthread_mutex_trylock(&_lock) == 0) {
            if (_lru->_totalCount > countLimit) {
                _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
        });
    }
}

缓存手动清理

其实上面这三种清理的方法在YYMemoryCache封装成了接口,所以用户也可以通过YYCache的memoryCache这个属性来手动清理相应维度上不符合传入标准的缓存:

//YYMemoryCache.h

// =========== 缓存清理接口 =========== 
//清理缓存到指定个数
- (void)trimToCount:(NSUInteger)count;

//清理缓存到指定开销
- (void)trimToCost:(NSUInteger)cost;

//清理缓存时间小于指定时间的缓存
- (void)trimToAge:(NSTimeInterval)age;

看一下它们的实现:

//清理缓存到指定个数
- (void)trimToCount:(NSUInteger)count {
    if (count == 0) {
        [self removeAllObjects];
        return;
    }
    [self _trimToCount:count];
}

//清理缓存到指定开销
- (void)trimToCost:(NSUInteger)cost {
    [self _trimToCost:cost];
}

//清理缓存时间小于指定时间的缓存
- (void)trimToAge:(NSTimeInterval)age {
    [self _trimToAge:age];
}

好了,YYCache的外部接口以及第一级缓存:内存缓存(YYMemoryCache)就讲完了,下一篇讲的是磁盘缓存与缓存设计思路方面的讲解。

原文发布于微信公众号 - 程序员维他命(J_Knight_)

原文发表时间:2018-08-16

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏GreenLeaves

九、将cs文件快速的转换成可执行文件和响应文件(配置编译开关的文件)

1、将包含多个类型的源代码文件转换为可以部署的文件。有如下Program.cs的文件,代码如下: public sealed class Program...

3127
来自专栏微服务生态

淘宝Tedis组件究竟是个啥(一)

淘宝的Tedis组件究竟是个啥呢?可能有一些朋友没有听过这个名字,有一些朋友会经常使用,那么今天我就来和大家深入分析一下,它的使用和原理。

1162
来自专栏walterlv - 吕毅的博客

利用 ReSharper 自定义代码中的错误模式,在代码审查之前就发现并修改错误

发布于 2018-03-20 11:54 更新于 2018-03...

890
来自专栏大史住在大前端

webpack4.0各个击破(5)—— Module篇

使用webpack对脚本进行合并是非常方便的,因为webpack实现了对各种不同模块规范的兼容处理,对前端开发者来说,理解这种实现方式比学习如何配置webpac...

1312
来自专栏码云1024

MFC多线程

4726
来自专栏王清培的专栏

.NET/ASP.NET MVC Controller 控制器(IController控制器的创建过程)

阅读目录: 1.开篇介绍 2.ASP.NETMVC IControllerFactory 控制器工厂接口 3.ASP.NETMVC DefaultControl...

2476
来自专栏H2Cloud

C++中消息自动派发之一 About JSON

1. 闲序   游戏服务器之间通信大多采用异步消息通信。而消息打包常用格式有:google protobuff,facebook thrift, 千千万万种自定...

2563
来自专栏Golang语言社区

多线程编程10个例子--1

留个纪念,不错的总结。十个例子清晰列举啦多线程编程的奥妙。 VC中多线程使用比较广泛而且实用,在网上看到的教程.感觉写的挺好. 一、问题的提出 编写一个耗时的...

4655
来自专栏安恒网络空间安全讲武堂

从零基础到成功解题之0ctf-ezdoor

2174
来自专栏大内老A

如何让普通变量也支持事务回滚?

有一次和人谈起关于事务的话题,谈到怎样的资源才能事务型资源。除了我们经常使用的数据库、消息队列、事务型文件系统(TxF)以及事务性注册表(TxR)等,还有那些资...

1858

扫码关注云+社区

领取腾讯云代金券