YYCache 源码学习(二)—— YYMemoryCache

系列其他文章:

  1. YYCache 源码学习(一)—— YYCache
  2. YYCache 源码学习(二)—— YYMemoryCache
  3. YYCache 源码学习(三)—— YYDiskCache

pthread_mutex_t

ibireme 在之前使用 OSSpinLock 来保证线程安全,但是现在 OSSpinLock 已经不在安全了,可以查看这篇文章 不再安全的 OSSpinLock。现在在 YYCache 中已经改用 pthread_mutex_t 来保证线程安全了。

为了线程安全,通常会用到锁。POSIX 下抽象了一个锁的类型的结构:pthread_mutex_t。通过对该结构的操作,来判断资源是否可以访问。顾名思义,加锁(lock)后,别人就无法打开,只有当锁没有关闭(unlock)的时候才能访问资源。

它主要有以下几个函数:

  1. pthread_mutex_init(pthread_mutex_t mutex,const pthread_mutexattr_t attr):初始化函数,初始化锁变量 mutex。attr 为锁属性,NULL值为默认属性。。
  2. pthread_mutex_lock(pthread_mutex_t *mutex):加锁
  3. pthread_mutex_unlock(pthread_mutex_t *mutex):释放锁
  4. pthread_mutex_destroy(pthread_mutex_t *mutex):注销锁
  5. pthread_mutex_tylock(pthread_mutex_t *mutex):加锁,但是与2不一样的是当锁已经在使用的时候,返回为EBUSY,而不是挂起等待。

LRU(Least Recently Used)

YYCache 采用 LRU 的缓存策略。简短的说,LRU 就是在清除缓存的时候,最先清除近期最少被使用到缓存。YYCache 通过 _YYLinkedMapNode_YYLinkedMap 两个私有类来实现该策略。

_YYLinkedMapNode

_YYLinkedMapNode 的实例相当于 LRU 时间线上的每一个节点。一个 _YYLinkedMapNode 实例,作为缓存的容器,记录了缓存数据的内容,对应的 key,使用的存储空间,最后一次使用的时间,同时,作为 LRU 时间线上的一个节点,还记录了它之前和之后的节点。

_YYLinkedMap

_YYLinkedMap 是 LRU 的时间线,使用 CFMutableDictionaryRef 存储各个节点,也就是 _YYLinkedMapNode 实例。此外,_YYLinkedMap 还记录了当前缓存数据的总存储空间,存储数量,以及是否在主线程释放缓存,是否异步释放缓存等配置。

默认不在主线程释放缓存,且异步释放。

向时间线头部插入新的时间节点

- (void)insertNodeAtHead:(_YYLinkedMapNode *)node

  1. node 存入 _dic 中,node 的 key 即为存储的 key;
  2. 总的存储消耗根据 nodecost 增加,总的缓存数加一;
  3. 如果当前 _YYLinkedMap 中已经有一个 head 节点,则需要将新的节点放在旧的节点之前,也就是将 node 的下一个节点 _next 指向旧的 _head 节点,同时将旧的 _head 的前一节点 _prev 指向 node,最后将 _YYLinkedMap_head 指向新的 head 节点–node。如果当前 _YYLinkedMap 中不存在 head 节点,则将 _YYLinkedMap_head_tail 都指向 node
将已经存在的时间节点移到时间线头部

- (void)bringNodeToHead:(_YYLinkedMapNode *)node

  1. 如果需要移到头部的节点就是当前头部的节点,则不进行任何操作;
  2. 如果需要移到头部的节点本为 _tail 节点,则将本来倒数第二的节点作为_tail 节点,并且将其的_next 置为 nil。若不是,则需要将 node 的后一个节点的 _prev 指向 node 的前一个节点,将指向 node 的后一个节点。经过上述操作,以及将 node 从本来的时间线中释放出来,并且也将新的时间线拼合起来了;
  3. node_next 指向旧的 _head 节点,将旧的 _head 节点的 _prev 指向 node, 将 node_prev 置为 nil, 将 _head 指向新的 node 节点。

因为是 node 本身是已经存在的节点,所以不需要做存入 _dic , _totalCost 增加和 _titalCount 增加的操作。

移除时间节点

- (void)removeNode:(_YYLinkedMapNode *)node

  1. node_dic 中移除;
  2. 相应的减少 _totalCost_totalCount
  3. 如果 node 存在 _next 节点,则需要将 _next 节点的 _prev 指向 node_prev 节点;
  4. 如果 node 存在 _prev 节点,则需要将 _prev 节点的 _next 指向 node_next 节点;
  5. 如果 node 为当前的 _head 节点,则将 node 存在 _next 节点作为 _head 节点;
  6. 如果 node 为当前的 _tail 节点,则将 node 存在 _prev 节点作为 _tail 节点;
移除时间线末端时间节点

- (_YYLinkedMapNode *)removeTailNode

  1. 如果不存在 _tail 节点,则直接返回 nil, 反之,继续下述步骤;
  2. _dic 中移除 _tail
  3. 相应的减少 _totalCost_totalCount
  4. 如果 _head_tail 是同一个节点,则将_head_tail 都置为 nil(即当前只有一个节点),否则将旧的 _tail_prev 节点作为新的 _tail 节点,同时将其 _next 置为 nil
  5. 返回被移除的旧 _tail 节点。
移除所有时间节点

- (void)removeAll

  1. _totalCost_totalCount 置为 0,将 _head_tail 置为 nil;
  2. 若不存在节点,则不进行别的操作,反之,执行下述步骤;
  3. 创建一个临时变量 holder,并指向 _dic ,然后将一个空的 CFMutableDictionaryRef 指向 _dic,接下来的任务就是释放 holder
  4. 若需要异步释放(也就是 _releaseAsynchronouslyYES),则根据 _releaseOnMainThread 选择释放的线程,若 _releaseOnMainThreadYES 则是主线程,反之为一个优先级较低的 global 线程;
  5. 若不需要异步释放,但需要在主线程释放,而当前线线程又不是主线程的话,则需要额外调用主线程来释放(若当前不是主线程,则函数 pthread_main_np() 返回一个非零值);
  6. 直接在当前调用的线程上释放。
dealloc

在此处需要注意的一点是,_YYLinkedMap 使用 CFMutableDictionaryRef 存储各个节点,CoreFoundation 框架内的结构的内存是不由 ARC 来管理,所以需要手动调用 CFRelease 来释放 _dic

私有方法

定时清除缓存

- (void)_trimRecursively

YYMemoryCache 提供了定时清理缓存的功能,根据 _autoTrimInterval(默认 5 秒) 的设置,定时检测缓存是否超过了各项限制,若是,则进行清理。

这里采用了递归的方式来实现不断循环的检测。

后台清除缓存

- (void)_trimInBackground

_queue 队列下,根据各个限制,对缓存进行清理。

根据 cost 清除缓存

- (void)_trimToCost:(NSUInteger)costLimit

根据缓存 cost 的限制,对缓存进行清理。
_countLimit = NSUIntegerMax
_ageLimit = DBL_MAX;

  1. 判断 cost 的限制是否为 0,若为 0 则表示全部缓存都有删除(默认的 _costLimitNSUIntegerMax, 所以在默认情况下几乎不会根据 cost 去清理缓存);
  2. 判断当前缓存的 cost 是否已经小于显示,若是则可以结束处理,反之需要继续处理缓存;
  3. 在进行缓存清理是,是遵循 LRU 策略的,每一个被清理的缓存(_YYLinkedMapNode 的实例)会先被暂时存储在 holder 中,最好再在 _queue 线程中释放。需要注意的是,在 while 语句中,会使用 pthread_mutex_trylock(&_lock) == 0 对锁是否可加锁进行判断,若当前不可加锁,则会在 10 ms 后再次尝试。这么做的原因是因为在进行缓存清理的时候,很可能在别的地方正在对缓冲进行别的操作,所以要等到别的操作完成,再来继续对缓存进行清理的工作。

根据 count 清除缓存

- (void)_trimToCount:(NSUInteger)countLimit

根据 count 的清理与 cost 十分相似,只是判断的添加更改了。

根据 age 清除缓存

- (void)_trimToAge:(NSTimeInterval)ageLimit

根据 age 的清理与 cost 十分相似,只是判断的添加变成了时间。稍有不同的是,在进行条件判断的时候,需要先判断 _lru 是否存在 tail 节点,若不存在则表示当前无缓存,也就不存在清理的必要性,若存在,则总是去判断 tail 节点的时间,因为 tail 总是代表了时间最早的那个缓存。

收到内存警告

- (void)_appDidReceiveMemoryWarningNotification

如果赋值了 didReceiveMemoryWarningBlock 则是在收到通知的时候调用 didReceiveMemoryWarningBlock。若使能了收到内存警告的时候,清理缓存,则会执行清理缓存的工作。

app 进入后台

- (void)_appDidEnterBackgroundNotification

如果赋值了 didEnterBackgroundBlock,则在收到通知的时候会调用 didEnterBackgroundBlock。若使能了收到进入后台通知的时候,清理缓存,则会进行清理缓存的工作。

公有方法

初始化

  1. 创建锁(pthread_mutex_init);
  2. 创建 _YYLinkedMap 实例,用于管理 LRU 时间线,同时也是所有缓存的存储容器;
  3. 创建一个并行队列;
  4. 初始化 _countLimit, _costLimit 为最大值 NSUIntegerMax(当前平台下最大的无符号整型数),_ageLimitDBL_MAX(当前平台下最大的双精度浮点数),自动移除超出显示的缓冲的时间间隔为 5 秒,使能在收到内存警告或者进入后台后清空缓存;
  5. 监听 UIApplicationDidReceiveMemoryWarningNotificationUIApplicationDidEnterBackgroundNotification 为了实现在收到内存警告或者进入后台后清空缓存的功能;
  6. 开始递归的自动清除超出限制的缓存。

dealloc

  1. 移除对通知的监听,通知的监听和移除必须成对出现,否则很容易引起程序奔溃的情况;
  2. _lru 以前所有节点,也就是移除了所有缓存;
  3. 销毁锁。

总缓存数量

该接口对外只暴露了一个只读的接口,理由很简单:这是一个由本身存储了多少缓存来决定的参数,外界不应该,也没有理由对其进行写操作;

该数值来自 _lru 上存储的总缓存(节点)数。

总缓存大小

该接口对外只暴露了一个只读的接口,理由很简单:这是一个由本身存储了多少缓存来决定的参数,外界不应该,也没有理由对其进行写操作;

该数值来自 _lru 上存储的总缓存(节点)大小。

是否在主线程释放

对应 _lru_releaseOnMainThread

是否异步释放

对应 _lru_releaseAsynchronously

是否包含某个 key 的缓存

- (BOOL)containsObjectForKey:(id)key

若 key 不存在,就直接返回 NO,否则对 _lru_dic 判断是否包含某个 key 对应的数据。

获取某个 key 相关的内存

- (id)objectForKey:(id)key

若 key 不存在,就直接返回 nil,否则从 _lru_dic 取出某个 key 对应的 _YYLinkedMapNode 实例 node,若 node 存在,更新 node 的时间,并更新到 head 位置,并返回 node_value(也就是实际上的缓存内容),若 node 不存在则返回 nil

存储缓存

  • - (void)setObject:(id)object forKey:(id)key
  • - (void)setObject:(id)object forKey:(id)key withCost:(NSUInteger)cost

方法 - setObject: forKey: 直接调用 - setObject: forKey: withCost:cost0

在方法 - setObject: forKey: withCost: 中,若 key 为空,则直接返回,若 key 有值,而 object 为空,则相当于被 key 对应的数据删除。其他情况下,操作如下:

  1. 试图从 _lru_dic 中获取 key 对应的 _YYLinkedMapNode 实例 node
  2. node 存在,更新总的大小(将旧值减除,然后加上新的值),更新 node_costtime_value(即 object),将 node 移到 head 的位置;若 node 不存在,则新建一个 _YYLinkedMapNode 实例,赋值后,将 node 插入到 head 位置;
  3. 判断是否超过了对缓存的大小或数量的限制
    • 若超出大小限制,则在 _queue 队列下,调用 - trimToCost: 方法来移除近期未使用的缓存数据(因为每个缓存的大小不一样,所以在添加一个缓存导致大小超出限制,移除的时候不能简单的移除时间线上最末端的节点了)。
    • 若超出个数限制,则直接移除时间线末端的节点就可以了。在移除后,释放的时候需要考虑释放的线程。但是,在 ARC 下不能直接调用 - release,所以不能直接在某个线程下释放。在这里有一个小技巧,如下代码所示,通过在 block 中持有 node 使得 node 在该线程下被释放。
1
2
3
dispatch_async(queue, ^{
[node class]; //hold and release in queue
});

移除某个缓存数据

- (void)removeObjectForKey:(id)key

若 key 不存在,则直接返回。从 _lru_dic 中获取指定 key 的 _YYLinkedMapNode 实例 node。若 node 不存在,则直接返回。先将 node_lru 里移除,然后再根据是否在主线程释放和释放异步释放的设置,释放移除 node

移除所有缓存数据

- (void)removeAllObjects

直接调用 _lru- removeAll 方法。

限制缓存的数量为指定值

- (void)trimToCount:(NSUInteger)count

若数量为 0,则直接调用 - removeAllObjects,否者调用私有方法 - _trimToCount:

其实可以只使用直接调用 - _trimToCount:,因为 - _trimToCount: 也会去判断 count == 0

限制缓存的大小为指定值

- (void)trimToCost:(NSUInteger)cost

直接调用私有方法 - _trimToCost:

显示缓存的时间为指定时间

- (void)trimToAge:(NSTimeInterval)age

直接调用私有方法 - _trimToAge:

相关链接

0%