NSCache初体验之YYCache源码阅读

不可免俗的老生常谈

  • NSCache 是类似 NSMutableDictionary 的集合. 可设置
  • NSCache 的数据在杀进程之后会消失(没进行本地保存操作的话)
  • NSCache 触发内存警告后, 删除内存的操作不能指定对象
  • NSCache 是线程安全的

YYCache

YYCache 设计思路
👆以上是源码地址和设计思路, 自取

YYCacheSDWebImage 的缓存机制一样, 都是内存和硬盘两级缓存.

首先解读 YYMemoryCache

YYMemoryCache 的简介 属性 方法 成员变量

/**
 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 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)).
 */
@interface YYMemoryCache : NSObject

@property (nullable, copy) NSString *name;
@property (readonly) NSUInteger totalCount;
@property (readonly) NSUInteger totalCost;
@property NSUInteger countLimit; // 缓存的最大个数 不是严格的限制  默认不限制
@property NSUInteger costLimit; // 缓存可占的最大内存容量 不是严格的限制  默认不限制
@property NSTimeInterval ageLimit; // 过期时间限制 不是严格的限制  默认不限制
@property NSTimeInterval autoTrimInterval; // 自动执行剪枝的时间 默认5秒
@property BOOL shouldRemoveAllObjectsOnMemoryWarning; // 接收到内存警告全部移除缓存 默认 YES
@property BOOL shouldRemoveAllObjectsWhenEnteringBackground; // App进入后台全部移除缓存 默认 YES
@property (nullable, copy) void(^didReceiveMemoryWarningBlock)(YYMemoryCache *cache); // 接收到内存警告时的回调
@property (nullable, copy) void(^didEnterBackgroundBlock)(YYMemoryCache *cache);// 进入后台的回调

/**
  @discussion You may set this value to `YES` if the key-value object contains the instance which should be released in main thread (such as UIView/CALayer).
 */
@property BOOL releaseOnMainThread; // 是否在主线程释放 默认 NO 

/**
 If `YES`, the key-value pair will be released asynchronously to avoid blocking  the access methods, otherwise it will be released in the access method   (such as removeObjectForKey:). Default is YES.
 */
@property BOOL releaseAsynchronously; // 是否异步释放 默认 YES

// 判断是否包含
- (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;

// 需要修剪的个数
- (void)trimToCount:(NSUInteger)count;
// 需要修剪的内存
- (void)trimToCost:(NSUInteger)cost;
// 需要修剪的时间
- (void)trimToAge:(NSTimeInterval)age;

@end

@implementation YYMemoryCache {
    pthread_mutex_t _lock;
    _YYLinkedMap *_lru;
    dispatch_queue_t _queue;
}

简单捋一下实现过程:
新增用 - (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost 为例:

- (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); // 完成, 解锁
}

移除的比较简单, 移除指定的就从字典取出, 判断是否为空, 调用 _YYLinkedMap 里的 - (void)removeNode:(_YYLinkedMapNode *)node 方法进行移除; 全部移除就是调用 _YYLinkedMap 里的 - (void)removeAll 方法. 当然所有操作有会加锁解锁.
值得拿出来说一说的是, 在指定线程中移除缓存对象的操作. 其实移除的代码在上面新增的代码里也有, 就是结尾判断是否超出限制值的部分. 第一次看的时候也不理解为什么要在 GCD 里面执行 [node class] 这个方法, 虽然注释清楚的写着在队列中保持和释放 .
后来在 YYCache 设计思路 的评论里找到了答案.

Q: 为什么要在 GCDBlock 里调用 [node class]?
A: 在 GCDBlock 里调用 [node class], 是为了让 Block 捕获到 node, 这样才能保证 node 的释放操作是在 Block 对应的 queue 上执行.
Q: 为什么这样能保证 node 的释放操作在 Block 对应的 queue 上?
A: node 在当前作用域结束的时候是不会被释放的, 因为还有 Block 对其强引用, 所以 node 的释放是应该是在 Block 执行完, 所以保证了 nodeBlock 对应的 queue

删除全部的时候, 是用一个 NSMutableArray *holder 装所有待移除对象, 最后在 GCDBlock 里调用 [holder count], 这里的 [holder count][node class] 是同一个用法

再提个点, 关于锁... YYMemoryCache 里使用的是 dispatch_semaphorepthread_mutex_t
提一嘴, pthread_mutex_t 是互斥锁. 说到互斥锁就不得不说自旋锁.
互斥锁等待过程中会休眠. 线程等待时间过长的适合使用互斥锁
自旋锁会处于忙等的状态. 线程等待时间不长的适合用自旋锁

剪枝这个也比较简单, 就是做相对于的判断, 看是否超出了设置的限制值, 如果超出, 通过一个 while() 循环, LRU 淘汰算法移除超出部分.

方法部分基本捋完了, 还有一个注意点是在 - (instancetype)init 方法里面, 简单看下, init 方法主要是初始化一些属性和给属性赋默认值; 添加了两个通知, 用来接收 App 进入后台和内存警告的通知.
除此之外, 就剩最后一个 _trimRecursively 方法的调用. 一并把 _trimRecursively 的完整代码也贴在 init 下面. 通过代码可知, 这里其实是启动了一个类似定时器的这么一个功能, 每 5 秒 _trimRecursively 就会再次调用一次本身, 再通过 _trimInBackground 方法去检查一遍缓存是否超出设定的值, 是否需要剪枝.
YYCache 设计思路的评论里可以看到, 许多读者对这个方法的调用是存在异议的, 这样调是否会影响性能, 是否有必要等等, 但作者都没有针对这些问题进行回答, 也不得而知他的想法了. 个人而言, 如果使用过程中不需要实时检测, 可以自行注释这行, 或者给 autoTrimInterval 属性赋一个超大值🤣

- (instancetype)init {
    self = super.init;
    pthread_mutex_init(&_lock, NULL);
    _lru = [_YYLinkedMap new];
    _queue = dispatch_queue_create("com.ibireme.cache.memory", DISPATCH_QUEUE_SERIAL);
    
    _countLimit = NSUIntegerMax;
    _costLimit = NSUIntegerMax;
    _ageLimit = DBL_MAX;
    _autoTrimInterval = 5.0;
    _shouldRemoveAllObjectsOnMemoryWarning = YES;
    _shouldRemoveAllObjectsWhenEnteringBackground = YES;
    
    [[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];
    });
}

接着来看 YYMemoryCache 里面的一个属性 _YYLinkedMap 这个类的实现
_YYLinkedMap 主要实现缓存对象的存储、缓存对象的移除、双向链表和 LRU 淘汰算法. 对象的存储用 MRU 算法, 移除上用 LRU 算法. 代码的实现主要是双向链表的实现. 对链表有概念的还是很容易看懂代码的.
ps: 双向链表就长这样👇

双向链表.jpg

_YYLinkedMap 的成员属性和主要实现方法(顺便加上 _YYLinkedMapNode 的成员变量):

@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

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

@implementation _YYLinkedMap
- (instancetype)init {
    self = [super init];
    _dic = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    _releaseOnMainThread = NO;
    _releaseAsynchronously = YES;
    return self;
}

- (void)dealloc {
    CFRelease(_dic);
}
// 在头部插入一个节点
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node {
    CFDictionarySetValue(_dic, (__bridge const void *)(node->_key), (__bridge const void *)(node));
    _totalCost += node->_cost;
    _totalCount++;
    if (_head) {
        node->_next = _head;
        _head->_prev = node;
        _head = node;
    } else {
        _head = _tail = node;
    }
}
// 将链表中的节点移动到头部
- (void)bringNodeToHead:(_YYLinkedMapNode *)node {
    if (_head == node) return;
    
    if (_tail == node) {
        _tail = node->_prev;
        _tail->_next = nil;
    } else {
        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--;
    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--;
    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);
        }
    }
}

@end

捋了一遍 _YYLinkedMap 代码下来, 产生了两个疑问:

  1. 双向链表的作用是啥?
    A: 双向链表在这里是为了实现 LRU 淘汰算法. 在 _YYLinkedMap 的实现中, 会把最近一次使用到的缓存放在链表里的 head, 最长时间没有使用到的就会在链表的 tail. 移除的时候都是移除 tail (全部移除除外), 因而得以实现 LRU 淘汰算法

题外话: 简单提一下 LRU 和 MRU 的概念
LRU (Least recently used) 近期使用最少算法
MRU (Most recently used) 近期使用最多算法
两者都是缓存淘汰算法, YYCache 这里不管是缓存还是硬盘, 都是使用 LRU 淘汰算法

  1. 为什么用 __unsafe_unretained 修饰?
    A: 在 ARC 下, 基本不会使用 __unsafe_unretained, 就算需要修饰弱指针, 也会用 weak 来修饰, 这里为什么用不安全的 __unsafe_unretained 呢? 在作者的另一篇大作iOS JSON 模型转换库评测的 Tip 里可找到答案. 总结一下作者原本的意思, 就是 __weak 修饰的属性, 访问时会调用一些方法, 这样会带来额外的开销, 使用__unsafe_unretained 就是为了节省开销.
    作者原话:
    iOS JSON 模型转换库评测截图.jpg

YYMemoryCache 源码阅读至此完毕, YYDiskCache 的源码阅读, 有时间再补充...

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

推荐阅读更多精彩内容