这一篇主要讲YYMemoryCache。总的来说,YYMemoryCache不同于一般的第三方Cache的key-value存储方式,而是采用的CFDictionary和双向链表相结合的方式进行内存缓存,这种做法的好处就是能够满足LRU(Least Recently Used)淘汰算法。它的层级结构是YYMemoryCache ->_YYLinkedMap ->_YYLinkedMapNode。
_YYLinkedMapNode的结构如下
@package
是为了不让framework外部对其进行操作 下面两个是链表每一个节点的前后指针,因为每个node都会被CFDictionary进行retain操作,所以这里使用的是__unsafe_unretained
(PS:猜猜为什么不用weak)来弱引用node _key
对应外界传进来的key _value
就是需要存储的值 _cost
存储这个value的内存消耗 _time
访问这个node的绝对时间
通过以上6个成员变量,就能完成时间,空间,数量的淘汰算法了。
接下来我们看看是如何操作node来进行Cache的增删查改的。
#增
通过外部调用- (void)setObject:(id)object forKey:(id)key
或者- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost
将对象存入MemoryCache中,具体实现如下
当object不存在时,调用removeObjectForKey
进行删除。目前暂时不讨论node存在的情况,后面会讲。首先先创建一个新的node,然后通过->来对对于的成员变量进行赋值(不用点语法就是为了节省调用方法的时间,提升性能,还有OC中对象进行 ->是访问成员变量,由于之前采用了 @package
操作,所以不能在外部通过->来访问成员变量,和结构体的->类似),然后用_lru(_YYLinkedMap的实例)将node插入到链表头部。然后检查是否超过数量和大小的限制,以进行尾部的node删除,然后在后台线程释放。还有一点要说明的是这里的源码为了保证线程安全使用的是OSSpinLock,它的好处是性能好,但是等待时间过长时会消耗大量CPU资源,所以很适合做内存缓存的锁。不过后来YY大神把源码里所有的OSSpinLock全部替换为了pthread_mutex_t,这是因为OSSpinLock有潜在的优先级反转问题,具体可以查看YY大神博客。(更新:苹果又添加了一个安全的自旋锁:os_unfair_lock,,顾名思义能够保证不同优先级的线程申请锁的时候不会发生优先级反转问题,预计随Xcode8推出而更新) 这里再提一个问题:为什么将对象捕获到block中后,随便调用一个方法就能使其释放?
#删 具体实现如下
根据key取出对于的node,然后操作_lru进行remove如下:
这里主要是根据这个node的_prev和_next所指向的node进行前后指针重新引用,和普通链表删除节点是一样的操作。
#查 代码如下:
查到相关node后,更新node的绝对时间,并且通过bringNodeToHead:
将node放在链表头部,具体代码为:
#改 这部分代码和增是在同一个方法内的,首先判断key-value pair是否存在,存在则将新的value存进去并更新绝对时间和_cost,然后将node插入链表头结点。代码如下
最后我们看看是如何实现淘汰算法的: 1.首先当我们初始化一个MemoryCache实例之后,这个实例就会自创建成功后递归调用- (void)_trimRecursively
如图:
利用dispatch_after根据外部设置的_autoTrimInterval
(默认5s)进行递归操作。分别轮询costLimit,countLimit以及ageLimit。我们只讲讲costLimit,因为其他两个操作是类似的。代码如下:
首先判断外部设置的costLimit是否为0,是则将MemoryCache全部清除代码如下:
可以选择是否异步释放和指定线程释放。
然后判断costCount是否大于costLimit,若大于,则尝试加锁进行尾部节点的移除。为了避免多次释放导致的性能开销,这里是将所有的尾节点放于一个数组中,集中释放。
小结: 1.看了关于其他第三方的缓存方案,YYCache暴露出来的costLimit我理解为是缓存文件的总内存限制,但是除非用户自己手动调用YYMemory
的- (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost;
这个方法,不然直接通过最上YYCache
这个类调用- (void)setObject:(id<NSCoding>)object forKey:(NSString *)key;
这个属性就毫无疑义了,因为默认所有文件的cost都是0。 2.拜读真正大神的源码感觉比看很多标题党博客效果好得多。 3.通过这部分的源码拜读,对于block的变量捕获,加锁解锁机制,性能优化的方法以及自己造轮子代码的结构和抽象以及底层的C函数调用和链表的操作有了更深的认识。移动端也可以见识到各种数据结构以及算法,感觉很棒~ 4.对于GCD的同步异步处理以及线性并发队列的选择有了更明确的观念。 5.未完待续。