YYCache 源码学习(一):YYMemoryCache

其实最近是在重新熟练Swift的使用,我想出了一个比较实用的方法,那就是一边看OC的项目,看懂之后用Swift实现一遍。这样既学习了优秀的源码又练习了Swift,一举两得。

之前看过几篇文章是剖析YYKit里面的一些小模块,对源码对一些解读。不得不说作者ibireme的设计思维和技术细节的处理都非常的棒。所以就选了YYKit里面的一些小模块入手。

YYCache主要分为了两部分:YYMemoryCache内存缓存和磁盘缓存YYDiskCache。平常使用的时候我们一般都只直接操作YYCache这个类,他是对内存缓存和磁盘缓存的封装。

这篇文章主要是讲解YYCache模块里面的YYMemoryCache部分。

API

我们先可以看一下YYMemoryCache的.h文件,浏览一起属性和方法。大多数的都可以见名知意的。

@interface YYMemoryCache : NSObject

#pragma mark - Attribute
///=============================================================================
/// @name Attribute
///=============================================================================

@property (nullable, copy) NSString *name;
@property (readonly) NSUInteger totalCount;
@property (readonly) NSUInteger totalCost;

#pragma mark - Limit
///=============================================================================
/// @name Limit
///=============================================================================

@property NSUInteger countLimit;
@property NSUInteger costLimit;
@property NSTimeInterval ageLimit; //过期时间
@property NSTimeInterval autoTrimInterval;//自动处理的间隔时间
@property BOOL shouldRemoveAllObjectsOnMemoryWarning;
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground;
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache);
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);
@property BOOL releaseOnMainThread;
@property BOOL releaseAsynchronously;

#pragma mark - Access Methods
///=============================================================================
/// @name 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
///=============================================================================
/// @name Trim
///=============================================================================

- (void)trimToCount:(NSUInteger)count;

- (void)trimToCost:(NSUInteger)cost;

- (void)trimToAge:(NSTimeInterval)age;

@end

我把乱七八糟的注释都删掉了,这样可以直观的来看,api分为四个部分,前两部分都是一些属性,后面两个是方法。

第一个Attribute部分是YYMemoryCache类储存的一些基本的属性:name,totalCount(储存对象的总个数),totalCost(储存的总占内存)。Limit部分是一些限制条件,就不一一的说了,单说一个releaseOnMainThread这个属性,可能会有因为,如果如果能异步释放,为什么还要强制去主线程释放呢 ? 这是因为有一些类,像UIView/CALayer这种是要在主线程中释放的,源码注释中也有提到。

第三部分就是一些跟储存相关的方法,最后一部分就是根据限制条件修剪处理内存的方法了 ~

.m代码剖析

LRU 缓存淘汰算法

YYMemoryCache是提供了内存修剪的方法的,既然有修剪,那么我们得有一个算法来确定是修剪掉哪一些。YYMemoryCache 和 YYDiskCache 都是实现的 LRU (least-recently-used) ,即最近最少使用淘汰算法。具体怎么样实现我们往后再说。

实现缓存方式

.m的最前面是实现了两个内部类_YYLinkedMap和_YYLinkedMapNode。 可以看出具体的缓存方法是通过一个双向列表和散列容器来实现的。

_YYLinkedMap中给出来操作结点的方法

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;
- (void)removeNode:(_YYLinkedMapNode *)node;
- (_YYLinkedMapNode *)removeTailNode;
- (void)removeAll;
具体细节剖析

先总起来说一下这些实现的代码,其实很容易读懂,就是通过一个链表的形式来处理缓存的数据,添加缓存的时候,就往链表的尾部添加一个节点,(通过节点来表示我们实际要储存的数据),如果要根据限制条件修剪内存的话,也是循环的删除尾部的那个节点,直到符合限制条件。那我们的LRU 缓存淘汰算法具体怎么使用,我发现在每一个读取了一个数据之后,会把这个数据在链表中对应的结点移动到头部,这样在大概率的情况下使用频率高的缓存数据会在链表的前面。所以修剪的时候可以从尾部修剪。

在具体的实现代码中,作者有很多很亮眼的操作,我们来欣赏一下。

1.定时修剪内存

// 根据限制条件修剪内存的占用  并根绝设定的时间递归调用
- (void)_trimRecursively {
    __weak typeof(self) _self = self;
    //注意这个dispatch_after后面使用的dispatch_get_global_queue  异步
    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];
    });
}

这个就是通过一个延时来调用修剪内存的方法,然后在递归调用本身。注意的是dispatch_after后面使用的dispatch_get_global_queue 来进行异步操作。

2.修剪内存的逻辑

- (void)_trimToCost:(NSUInteger)costLimit {
    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;
    
    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
        });
    }
}

我们以这个按照占内存大小的限制来修剪内存为例子来看一下具体的实现方法。

开始先做几个判断,看是否超过限制需要修剪,不需要修剪就直接return。

修剪的过程就跟上面说到的是一样的,判断是否超过内存限制,超过就删掉尾部的结点,如此循环操作,只要符合限制要求。

异步释放资源:

我们可以看到在removeNode的时候,会使用一个holder数组来接收被移除的这些node,然后最后释放这些结点。为什么要这样做?其实这就是通过异步释放这些资源来减少主线程资源的开销。这里作者在异步中调用了[holder count]; 其实最开始我也不知道这个是什么意思,但是作者标注了release in queue,我猜测是通过调用你这个holder的随便一个方法,让这个异步的线程来管理这个holder,进而通过此异步线程来实现holder中对象的释放

锁的使用:

还有一个比较重要的点,为什么使用pthread_mutex_trylock这个方式加锁,然后在失败之后,线程要sleep。

这个问题就需要我们去研究一下各种锁了。很惭愧我对锁的了解不是很深刻,但是通过看了大神的博客,有了一些了解。(下面内容引用自大神的博客,文末有地址)

作者都是使用的pthread_mutex_t互斥锁,这个锁有一个特性,在多个线程竞争一个资源的时候,除了竞争成功的线程,其他的线程都会被动挂起状态,当竞争成功的线程解锁是,会去主动将挂起的其他线程激活,这个过程包含了上下文切换,CPU抢占,信号发送等开销,很明显,开销有些大。

所以作者使用了pthread_mutex_trylock()尝试解锁,若解锁失败该方法会立即返回,让当前线程不会进入被动的挂起状态(也可以说阻塞),在下一次循环时又继续尝试获取锁。这个过程很有意思,感觉是手动实现了一个自旋锁。而自旋锁有个需要注意的问题是:死循环等待的时间越长,对 cpu 的消耗越大。所以作者做了一个很短的睡眠 usleep(10 * 1000),有效的减小了循环的调用次数,至于这个睡眠时间的长度为什么是 10ms, 作者应该做了测试。

其他部分

其他部分就不一一细说了,作者整体思路很清晰,然后代码逻辑也很好懂,像上面提到的一些细节的处理可见作者的技术水平了。

参考

https://www.jianshu.com/p/408d4d37bcbd

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

推荐阅读更多精彩内容

  • 本文是我自己在秋招复习时的读书笔记,整理的知识点,也是为了防止忘记,尊重劳动成果,转载注明出处哦!如果你也喜欢,那...
    波波波先森阅读 11,147评论 4 56
  • 从 YYCache 源码 Get 到如何设计一个优秀的缓存 来源:Lision 前言 iOS 开发中总会用到各种缓...
    今天lgw阅读 5,883评论 1 22
  • 今天开始分析YYCache 包含的文件类 YYCache YYMemoryCache YYDiskCache YY...
    充满活力的早晨阅读 693评论 4 1
  • 选择安装方式 CD/USB Arch启动盘安装 使用Arch启动盘比较简单方便,没有额外设置,直接阅读下一步。US...
    孤逐王阅读 2,766评论 2 5
  • 文丨红瑀 看着熟悉的书店,我竟然有种恍如隔世的疏离感。一场大病,带走了我身上的十公斤肉,也将我的世界打得粉碎。我将...
    红瑀阅读 1,154评论 16 15