YYCache(1)---YYMemoryCache

一.类结构图

简单引用一些作者(ibireme)[https://github.com/ibireme/]在YYCache 设计思路的介绍。
1)YYMemoryCache: 内存缓存,没有异步访问的接口,尽量优化了同步访问的性能,用OSSpinLock来保证线程安全。缓存内部使用双向链表和NSDictionary实现了LRU淘汰算法。
2)YYDiskCache: 磁盘缓存,采用SQLite配合文件的存储方式。据作者在iPhone6 64G上测试,在存取小数据的时候,它的性能远远高于基于文件存储的库;而较大数据的存取性能则比较接近。YYDiskCache也实现了LRU淘汰算法。

YYCache(1)---YYMemoryCache_第1张图片
类结构图

二.学前准备

2.1 LRU淘汰算法

网上找了一个简单的说明

2.2 OSSpinLock及一些锁

iOS/MacOS自有的自旋锁。当一个线程获得锁之后,其他线程会一直循环,查看该锁是否被释放。所以,该锁适用于锁的持有者保存时间较短的情况下。
顺带提及一下dispatch_semaphore(信号量),GCD用它来控制多线程并发。参考
还有,pthread_mutex是一种互斥锁,当锁被占用时,别的想使用该锁的线程都会被阻塞。

2.3 YYLinkedMapNode && YYLinkedMap

YYCache实现了LRU淘汰算法,我们看一下代码中是如何实现的。(首先你要确保你知道什么是LRU淘汰算法)

2.3.1 YYLinkedMapNode

YYLinkedMapNode的实例作为双向链表的节点,提供了指向前后节点的指针_prev和_next、key-value、cost、time

2.3.2 YYLinkedMap

YYLinkedMap不是线程安全的,也不会检测数据的有效性。它是用于服务YYMemoryCache的,作者不希望别人直接使用它。
作为链表,它提供了一些方法。提升节点至表头的方法主要是为了提高查找的命中率。

//插入节点
- (void)insertNodeAtHead:(_YYLinkedMapNode *)node;

//将某一存在的节点提升至表头
- (void)bringNodeToHead:(_YYLinkedMapNode *)node;

//删除节点
- (void)removeNode:(_YYLinkedMapNode *)node;

//如果链表存在,删除尾节点
- (_YYLinkedMapNode *)removeTailNode;

//在后台线程删除所有节点
- (void)removeAll;

三.代码阅读

github上下载Demo工程CacheBenchmark,后文都以此为基础学习。demo中存在不同缓存类的对比,本文主要学习YYCache,也只关心这部分的代码。

3.1 YYMemoryCache添加数据

Demo对内存缓存的测试主要集中于 + (void)memoryCacheBenchmark; 方法中。首先对比了 setObject:forKey: 存取基本数据类型的性能。我们从YYMemoryCache的这个方法开始学习它内部的处理。

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

代码分析:
1)pthread_mutex_lock: 这里用了互斥锁,确保了线程安全
2)根据传入的key值去链表_lru中取node
3)如果取到node,更新node,并将其提升至表头;如果没有取到node,生成一个新的node,并插入链表
4)如果_totalCost超过了链表的最大内存开销长度,'修剪'链表。即调用 - (void)_trimToCost:(NSUInteger)costLimit; 方法(不贴代码了)。它会做2件事:

  1. CACurrentMediaTime() 可以引申出iOS中关于时间的处理,建议仔细阅读MrPeak的iOS关于时间的处理

① 把链表末尾的节点删除,直到内存开销长度够用
② 根据_lru是否在要在主线程销毁数据(可配置,_releaseOnMainThread变量),创建线程,并销毁节点。
totalCost默认是没有上限的。如果,手动设置为10MB,那么当链表的数据长度超过10MB时,就会触发清理操作。

5)同理,如果存储的节点数量超过了上限,也会进行清理的。

最后,我们顺便看一下数据获取方法,应该是一目了然的。

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

3.2 YYMemoryCache其他方法

我们看一下init方法:

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

添加了2个通知:
1)_appDidReceiveMemoryWarningNotification: 内存警告通知
2)_appDidEnterBackgroundNotification: 程序进入后台通知

2个通知内部都提供了block调用,我们可以根据需求实现自定义操作,还可以清空所有数据。

总结

从demo的时间来看,YYMemoryCache在性能上还是比较接近NSDictionary的。当然,在这种情况下,肯定首选还是NSDictionary。接下去,我们将看一下磁盘缓存的代码

你可能感兴趣的:(YYCache(1)---YYMemoryCache)