YYCache是国内开发者ibireme开源的一个线程安全的高性能键值缓存组件,代码风格简洁清晰,在GitHub上已经有了1600+颗星。
阅读它的源码有助于建立比较完整的缓存设计的思路,同时也能巩固一下双向链表,线程锁,数据库操作相关的知识。如果你还没有看过YYCache的源码,那么恭喜你,阅读此文会对理解YYCache的源码有比较大的帮助。
在正式开始讲解源码之前,先简单看一下该框架的使用方法。
基本使用方法
举一个缓存用户姓名的例子来看一下YYCache的几个API:
1 | //需要缓存的对象 |
总体来看这些API与NSCache
是差不多的。
下面接着看一下该框架的架构:
架构与职责划分
首先看一下架构图:
架构图
职责划分
从架构图上来看,该组件里面的成员并不多:
- YYCache:提供了最外层的接口,调用了YYMemoryCache与YYDiskCache的相关方法。
- YYMemoryCache:负责处理容量小,相对高速的内存缓存。线程安全,支持自动和手动清理缓存等功能。
- _YYLinkedMap:YYMemoryCache使用的双向链表类。
- _YYLinkedMapNode:是_YYLinkedMap使用的节点类。
- YYDiskCache:负责处理容量大,相对低速的磁盘缓存。线程安全,支持异步操作,自动和手动清理缓存等功能。
- YYKVStorage:YYDiskCache的底层实现类,用于管理磁盘缓存。
- YYKVStorageItem:内置在YYKVStorage中,是YYKVStorage内部用于封装某个缓存的类。
每个成员的详细的功能会在下文结合代码介绍。
代码讲解
知道了YYCache的架构和职责划分以后,现在结合代码开始正式讲解。
讲解分为下面6个部分:
- YYCache
- YYMemoryCache
- YYDiskCache
- 保证线程安全的不同方案
- 提高缓存性能的几个尝试
- 其他知识点
YYCache
YYCache给用户提供所有最外层的缓存操作接口,而这些接口的内部内部实际上是调用了YYMemoryCache和YYDiskCache对象的相关方法。
因为YYMemoryCache和YYDiskCache的实例作为YYCache的两个公开的属性,所以用户无法直接使用YYMemoryCache和YYDiskCache对象,只能通过属性的方式来间接使用它们。
我们来看一下YYCache的属性和接口:
YYCache的属性和接口
1 |
|
从上面的接口可以看出YYCache的接口和NSCache很相近,而且在接口上都区分了有无回调的功能。
下面结合代码看一下这些接口是如何实现的:
YYCache的接口实现
下面省略了带有回调的接口,因为与无回调的接口非常接近。
1 | - (BOOL)containsObjectForKey:(NSString *)key { |
从上面的接口实现可以看出:在YYCache中,永远都是先访问内存缓存,然后再访问磁盘缓存(包括了写入,读取,查询,删除缓存的操作)。而且关于内存缓存(_memoryCache)的操作,是不存在block回调的。
现在了解了YYCache的接口以及实现,下面我分别讲解一下YYMemoryCache(内存缓存)和YYDiskCache(磁盘缓存)这两个类。
YYMemoryCache
YYMemoryCache负责处理容量小,相对高速的内存缓存:它将需要缓存的对象与传入的key关联起来,操作类似于NSCache。
但是与NSCache不同的是,YYMemoryCache的内部有:
- 缓存淘汰算法:使用LRU(least-recently-used) 算法来淘汰(清理)使用频率较低的缓存。
- 缓存清理策略:使用三个维度来标记,分别是count(缓存数量),cost(开销),age(距上一次的访问时间)。YYMemoryCache提供了分别针对这三个维度的清理缓存的接口。用户可以根据不同的需求(策略)来清理在某一维度超标的缓存。
一个是淘汰算法,另一个是清理维度,乍一看可能没什么太大区别。我在这里先简单区分一下:
缓存淘汰算法的目的在于区分出使用频率高和使用频率低的缓存,当缓存数量达到一定限制的时候会优先清理那些使用频率低的缓存。因为使用频率已经比较低的缓存在将来的使用频率也很有可能会低。
缓存清理维度是给每个缓存添加的标记:
如果用户需要删除age(距上一次的访问时间)超过1天的缓存,在YYMemoryCache内部,就会从使用频率最低的那个缓存开始查找,直到所有距上一次的访问时间超过1天的缓存都清理掉为止。
如果用户需要将缓存总开销清理到总开销小于或等于某个值,在YYMemoryCache内部,就会从使用频率最低的那个缓存开始清理,直到总开销小于或等于这个值。
如果用户需要将缓存总数清理到总开销小于或等于某个值,在YYMemoryCache内部,就会从使用频率最低的那个缓存开始清理,直到总开销小于或等于这个值。
可以看出,无论是以哪个维度来清理缓存,都是从缓存使用频率最低的那个缓存开始清理。而YYMemoryCache保留的所有缓存的使用频率的高低,是由LRU这个算法决定的。
现在知道了这二者的区别,下面来具体讲解一下缓存淘汰算法和缓存清理策略:
YYMemoryCache的缓存淘汰算法
在详细讲解这个算法之前我觉得有必要先说一下该算法的核心:
我个人认为LRU缓存替换策略的核心在于如果某个缓存访问的频率越高,就认定用户在将来越有可能访问这个缓存。
所以在这个算法中,将那些最新访问(写入),最多次被访问的缓存移到最前面,然后那些很早之前写入,不经常访问的缓存就被自动放在了后面。这样一来,在保留的缓存个数一定的情况下,留下的缓存都是访问频率比较高的,这样一来也就提升了缓存的命中率。谁都不想留着一些很难被用户再次访问的缓存,毕竟缓存本身也占有一定的资源不是么?
其实这个道理和一些商城类app的商品推荐逻辑是一样的:
如果首页只能展示10个商品,对于一个程序员用户来说,可能推荐的是于那些他最近购买商品类似的机械键盘鼠标,技术书籍或者显示屏之类的商品,而不是一些洋娃娃或是钢笔之类的商品。
那么LRU算法具体是怎么做的呢?
在YYMemoryCache中,使用了双向链表这个数据结构来保存这些缓存:
- 当写入一个新的缓存时,要把这个缓存节点放在链表头部,并且并且原链表头部的缓存节点要变成现在链表的第二个缓存节点。
- 当访问一个已有的缓存时,要把这个缓存节点移动到链表头部,原位置两侧的缓存要接上,并且原链表头部的缓存节点要变成现在链表的第二个缓存节点。
- (根据清理维度)自动清理缓存时,要从链表的最后端逐个清理。
这样一来,就可以保证链表前端的缓存是最近写入过和经常访问过的。而且该算法总是从链表的最后端删除缓存,这也就保证了留下的都是一些“比较新鲜的”缓存。
下面结合代码来讲解一下这个算法的实现:
YYMemoryCache用一个链表节点类来保存某个单独的内存缓存的信息(键,值,缓存时间等),然后用一个双向链表类来保存和管理这些节点。这两个类的名称分别是:
- _YYLinkedMapNode:链表内的节点类,可以看做是对某个单独内存缓存的封装。
- _YYLinkedMap:双向链表类,用于保存和管理所有内存缓存(节点)
_YYLinkedMapNode
_YYLinkedMapNode可以被看做是对某个缓存的封装:它包含了该节点上一个和下一个节点的指针,以及缓存的key和对应的值(对象),还有该缓存的开销和访问时间。
1 | @interface _YYLinkedMapNode : NSObject { |
下面看一下双向链表类:
_YYLinkedMap
1 | @interface _YYLinkedMap : NSObject { |
从链表类的属性上看:链表类内置了CFMutableDictionaryRef,用于保存节点的键值对,它还持有了链表内节点的总开销,总数量,头尾节点等数据。
可以参考下面这张图来看一下二者的关系:
看一下_YYLinkedMap的接口的实现:
将节点插入到链表头部:
1 | - (void)insertNodeAtHead:(_YYLinkedMapNode *)node { |
要看懂节点操作的代码只要了解双向链表的特性即可。在双向链表中:
- 每个节点都有两个分别指向前后节点的指针。所以说每个节点都知道它前一个节点和后一个节点是谁。
- 链表的头部节点指向它前面节点的指针为空;链表尾部节点指向它后侧节点的指针也为空。
为了便于理解,我们可以把这个抽象概念类比于幼儿园手拉手的小朋友们:
每个小朋友的左手都拉着前面小朋友的右手;每个小朋友的右手都拉着后面小朋友的左手;
而且最前面的小朋友的左手和最后面的小朋友的右手都没有拉任何一个小朋友。
将某个节点移动到链表头部:
1 |
|
第一次看上面的代码我自己是懵逼的,不过如果结合上面小朋友拉手的例子就可以快一点理解。
如果要其中一个小朋友放在队伍的最前面,需要
- 将原来这个小朋友前后的小朋友的手拉上。
- 然后将这个小朋友的右手和原来排在第一位的小朋友的左手拉上。
上面说的比较简略,但是相信对大家理解整个过程会有帮助。
也可以再结合链表的图解来看一下:
读者同样可以利用这种思考方式理解下面这段代码:
移除链表中的某个节点:
1 | - (void)removeNode:(_YYLinkedMapNode *)node { |
移除并返回尾部的node:
1 | - (_YYLinkedMapNode *)removeTailNode { |
OK,现在了解了YYMemoryCache底层的节点操作的代码。现在来看一下YYMemoryCache是如何使用它们的。
YYMemoryCache的属性和接口
1 | //YYMemoryCache.h |
YYMemoryCache的接口实现
在YYMemoryCache的初始化方法里,会实例化一个_YYLinkedMap的实例来赋给_lru这个成员变量。
1 |
|
然后所有的关于缓存的操作,都要用到_lru这个成员变量,因为它才是在底层持有这些缓存(节点)的双向链表类。下面我们来看一下这些缓存操作接口的实现:
1 |
|
上面的实现是针对缓存的查询,写入,获取操作的,接下来看一下缓存的清理策略。
YYMemoryCache的缓存清理策略
如上文所说,在YYCache中,缓存的清理可以从缓存总数量,缓存总开销,缓存距上一次的访问时间来清理缓存。而且每种维度的清理操作都可以分为自动和手动的方式来进行。
缓存自动清理
缓存的自动清理功能在YYMemoryCache初始化之后就开始了,是一个递归调用的实现:
1 | //YYMemoryCache.m |
1 | //YYMemoryCache.m |
可以看到,YYMemoryCache是按照缓存数量,缓存开销,缓存时间的顺序来自动清空缓存的。我们结合代码看一下它是如何按照缓存数量来清理缓存的(其他两种清理方式类似,暂不给出):
1 | //YYMemoryCache.m |
缓存手动清理
其实上面这三种清理的方法在YYMemoryCache封装成了接口,所以用户也可以通过YYCache的memoryCache这个属性来手动清理相应维度上不符合传入标准的缓存:
1 | //YYMemoryCache.h |
看一下它们的实现:
1 | //清理缓存到指定个数 |
YYDiskCache
YYDiskCache负责处理容量大,相对低速的磁盘缓存。线程安全,支持异步操作。作为YYCache的第二级缓存,它与第一级缓存YYMemoryCache的相同点是:
- 都具有查询,写入,读取,删除缓存的接口。
- 不直接操作缓存,也是间接地通过另一个类(YYKVStorage)来操作缓存。
- 它使用LRU算法来清理缓存。
- 支持按 cost,count 和 age 这三个维度来清理不符合标准的缓存。
它与YYMemoryCache不同点是:
- 根据缓存数据的大小来采取不同的形式的缓存:
- 数据库sqlite: 针对小容量缓存,缓存的data和元数据都保存在数据库里。
- 文件+数据库的形式: 针对大容量缓存,缓存的data写在文件系统里,其元数据保存在数据库里。
- 除了 cost,count 和 age 三个维度之外,还添加了一个磁盘容量的维度。
这里需要说明的是:
对于上面的第一条:我看源码的时候只看出来有这两种缓存形式,但是从内部的缓存type枚举来看,其实是分为三种的:
1 | typedef NS_ENUM(NSUInteger, YYKVStorageType) { |
也就是说我只找到了第二,第三种缓存形式,而第一种纯粹的文件存储(YYKVStorageTypeFile)形式的实现我没有找到:当type为
YYKVStorageTypeFile和YYKVStorageTypeMixed的时候的缓存实现都是一致的:都是讲data存在文件里,将元数据放在数据库里面。
在YYDiskCache的初始化方法里,没有发现正确的将缓存类型设置为YYKVStorageTypeFile的方法:
1 | //YYDiskCache.m |
从上面的代码可以看出来,当给指定初始化方法initWithPath:inlineThreshold:
的第二个参数传入0的时候,缓存类型才是YYKVStorageTypeFile。而且比较常用的初始化方法initWithPath:
的实现里,是将20kb传入了指定初始化方法里,结果就是将type设置成了YYKVStorageTypeMixed。
而且我也想不出如果只有文件形式的缓存的话,其元数据如何保存。如果有读者知道的话,麻烦告知一下,非常感谢了~~
在本文暂时对于上面提到的”文件+数据库的形式”在下文统一说成文件缓存了。
在接口的设计上,YYDiskCache与YYMemoryCache是高度一致的,只不过因为有些时候大文件的访问可能会比较耗时,所以框架作者在保留了与YYMemoryCache一样的接口的基础上,还在原来的基础上添加了block回调,避免阻塞线程。来看一下YYDiskCache的接口(省略了注释):
1 | //YYDiskCache.h |
从上面的接口代码可以看出,YYDiskCache与YYMemoryCache在接口设计上是非常相似的。但是,YYDiskCache有一个非常重要的属性,它作为用sqlite做缓存还是用文件做缓存的分水岭:
1 | //YYDiskCache.h |
这个属性的默认值是20480byte,也就是20kb。即是说,如果缓存数据的长度大于这个值,就使用文件存储;如果小于这个值,就是用sqlite存储。来看一下这个属性是如何使用的:
首先我们会在YYDiskCache的指定初始化方法里看到这个属性:
1 | //YYDiskCache.m |
在这里将_inlineThreshold赋值,也是唯一一次的赋值。然后在写入缓存的操作里判断写入缓存的大小是否大于这个临界值,如果是,则使用文件缓存:
1 | //YYDiskCache.m |
现在我们知道了YYDiskCache相对于YYMemoryCache最大的不同之处是缓存类型的不同。
细心的朋友会发现上面这个写入缓存的方法(saveItemWithKey:value:filename:extendedData:)实际上是属于_kv的。这个_kv就是上面提到的YYKVStorage的实例,它在YYDiskCache的初始化方法里被赋值:
1 | //YYDiskCache.m |
同样地,再举其他两个接口为例,内部也是调用了_kv的方法:
1 | - (BOOL)containsObjectForKey:(NSString *)key { |
所以是时候来看一下YYKVStorage的接口和实现了:
YYKVStorage
YYKVStorage实例负责保存和管理所有磁盘缓存。和YYMemoryCache里面的_YYLinkedMap将缓存封装成节点类_YYLinkedMapNode类似,YYKVStorage也将某个单独的磁盘缓存封装成了一个类,这个类就是YYKVStorageItem,它保存了某个缓存所对应的一些信息(key, value, 文件名,大小等等):
1 | //YYKVStorageItem.h |
既然在这里将缓存封装成了YYKVStorageItem实例,那么作为缓存的管理者,YYKVStorage就必然有操作YYKVStorageItem的接口了:
1 | //YYKVStorage.h |
大家最关心的应该是写入缓存的接口是如何实现的,下面重点讲一下写入缓存的接口:
1 | //写入某个item |
这三个接口都比较类似,上面的两个方法都会调用最下面参数最多的方法。在详细讲解写入缓存的代码之前,我先讲一下写入缓存的大致逻辑,有助于让大家理解整个YYDiskCache写入缓存的流程:
- 首先判断传入的key和value是否符合要求,如果不符合要求,则立即返回NO,缓存失败。
- 再判断是否type==YYKVStorageTypeFile并且文件名为空字符串(或nil):如果是,则立即返回NO,缓存失败。
- 判断filename是否为空字符串:
- 如果不为空:写入文件,并将缓存的key,等信息写入数据库,但是不将key对应的data写入数据库。
- 如果为空:
- 如果缓存类型为YYKVStorageTypeSQLite:将缓存文件删除
- 如果缓存类型不为YYKVStorageTypeSQLite:则将缓存的key和对应的data等其他信息存入数据库。
- 如果缓存类型为YYKVStorageTypeSQLite:将缓存文件删除
1 | - (BOOL)saveItem:(YYKVStorageItem *)item { |
从上面的代码可以看出,在底层写入缓存的方法是_dbSaveWithKey:value:fileName:extendedData:
,这个方法使用了两次:
- 在以文件(和数据库)存储缓存时
- 在以数据库存储缓存时
不过虽然调用了两次,我们可以从传入的参数是有差别的:第二次filename传了nil。那么我们来看一下_dbSaveWithKey:value:fileName:extendedData:
内部是如何区分有无filename的情况的:
- 当filename为空时,说明在外部没有写入该缓存的文件:则把data写入数据库里
- 当filename不为空时,说明在外部有写入该缓存的文件:则不把data也写入了数据库里
下面结合代码看一下:
1 | //数据库存储 |
框架作者用数据库的一条记录来保存关于某个缓存的所有信息。
而且数据库的第四个字段是保存缓存对应的data的,从上面的代码可以看出当filename为空和不为空的时候的处理的差别。
上面的sqlite3_stmt
可以看作是一个已经把sql语句解析了的、用sqlite自己标记记录的内部数据结构。
而sqlite3_bind_text和sqlite3_bind_int是绑定函数,可以看作是将变量插入到字段的操作。
OK,现在看完了写入缓存,我们再来看一下获取缓存的操作:
1 | //YYKVSorage.m |
从上面这段代码我们可以看到获取YYKVStorageItem的实例的方法是_dbGetItemWithKey:excludeInlineData:
我们来看一下它的实现:
- 首先根据查找key的sql语句生成stmt
- 然后将传入的key与该stmt进行绑定
- 最后通过这个stmt来查找出与该key对应的有关该缓存的其他数据并生成item。
来看一下代码:
1 | - (YYKVStorageItem *)_dbGetItemWithKey:(NSString *)key excludeInlineData:(BOOL)excludeInlineData { |
我们可以看到最终生成YYKVStorageItem实例的是通过_dbGetItemFromStmt:excludeInlineData:
来实现的:
1 | - (YYKVStorageItem *)_dbGetItemFromStmt:(sqlite3_stmt *)stmt excludeInlineData:(BOOL)excludeInlineData { |
上面这段代码分为两个部分:
- 获取数据库里每一个字段对应的数据
- 将数据赋给YYKVStorageItem的实例
需要注意的是:
- 字符串类型需要使用
stringWithUTF8String:
来转成NSString类型。 - 这里面会判断
excludeInlineData
:- 如果为TRUE,就提取存入的data数据
- 如果为FALSE,就不提取
保证线程安全的方案
我相信对于某个设计来说,它的产生一定是基于某种个特定问题下的某个场景的
由上文可以看出:
- YYMemoryCache 使用了 pthread_mutex 线程锁(互斥锁)来确保线程安全
- YYDiskCache 则选择了更适合它的 dispatch_semaphore。
内存缓存操作的互斥锁
在YYMemoryCache中,是使用互斥锁来保证线程安全的。
首先在YYMemoryCache的初始化方法中得到了互斥锁,并在它的所有接口里都加入了互斥锁来保证线程安全,包括setter,getter方法和缓存操作的实现。举几个例子:
1 | - (NSUInteger)totalCost { |
而且需要在dealloc方法中销毁这个锁头:
1 | - (void)dealloc { |
磁盘缓存使用信号量来代替锁
框架作者采用了信号量的方式来给
首先在初始化的时候实例化了一个信号量:
1 | - (instancetype)initWithPath:(NSString *)path |
然后使用了宏来代替加锁解锁的代码:
1 |
简单说一下信号量:
dispatch_semaphore是GCD用来同步的一种方式,与他相关的共有三个函数,分别是
- dispatch_semaphore_create:定义信号量
- dispatch_semaphore_signal:使信号量+1
- dispatch_semaphore_wait:使信号量-1
当信号量为0时,就会做等待处理,这是其他线程如果访问的话就会让其等待。所以如果信号量在最开始的的时候被设置为1,那么就可以实现“锁”的功能:
- 执行某段代码之前,执行dispatch_semaphore_wait函数,让信号量-1变为0,执行这段代码。
- 此时如果其他线程过来访问这段代码,就要让其等待。
- 当这段代码在当前线程结束以后,执行dispatch_semaphore_signal函数,令信号量再次+1,那么如果有正在等待的线程就可以访问了。
需要注意的是:如果有多个线程等待,那么后来信号量恢复以后访问的顺序就是线程遇到dispatch_semaphore_wait的顺序。
这也就是信号量和互斥锁的一个区别:互斥量用于线程的互斥,信号线用于线程的同步。
互斥:是指某一资源同时只允许一个访问者对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的。
同步:是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。也就是说使用信号量可以使多个线程有序访问某个资源。
那么问题来了:为什么内存缓存使用的是互斥锁(pthread_mutex),而磁盘缓存使用的就是信号量(dispatch_semaphore)呢?
答案在框架作者的文章YYCache 设计思路里可以找到:
为什么内存缓存使用互斥锁(pthread_mutex)?
框架作者在最初使用的是自旋锁(OSSpinLock)作为内存缓存的线程锁,但是后来得知其不够安全,所以退而求其次,使用了pthread_mutex。
为什么磁盘缓存使用的是信号量(dispatch_semaphore)?
dispatch_semaphore 是信号量,但当信号总量设为 1 时也可以当作锁来。在没有等待情况出现时,它的性能比 pthread_mutex 还要高,但一旦有等待情况出现时,性能就会下降许多。相对于 OSSpinLock 来说,它的优势在于等待时不会消耗 CPU 资源。对磁盘缓存来说,它比较合适。
因为YYDiskCache在写入比较大的缓存时,可能会有比较长的等待时间,而dispatch_semaphore在这个时候是不消耗CPU资源的,所以比较适合。
提高缓存性能的几个尝试
选择合适的线程锁
可以参考上一部分YYMemoryCache 和YYDiskCache使用的不同的锁以及原因。
选择合适的数据结构
在YYMemoryCache中,作者选择了双向链表来保存这些缓存节点。那么可以思考一下,为什么要用双向链表而不是单向链表或是数组呢?
为什么不选择单向链表:单链表的节点只知道它后面的节点(只有指向后一节点的指针),而不知道前面的。所以如果想移动其中一个节点的话,其前后的节点不好做衔接。
为什么不选择数组:数组中元素在内存的排列是连续的,对于寻址操作非常便利;但是对于插入,删除操作很不方便,需要整体移动,移动的元素个数越多,代价越大。而链表恰恰相反,因为其节点的关联仅仅是靠指针,所以对于插入和删除操作会很便利,而寻址操作缺比较费时。由于在LRU策略中会有非常多的移动,插入和删除节点的操作,所以使用双向链表是比较有优势的。
选择合适的线程来操作不同的任务
无论缓存的自动清理和释放,作者默认把这些任务放到子线程去做:
看一下释放所有内存缓存的操作:
1 | - (void)removeAll { |
这里的YYMemoryCacheGetReleaseQueue()
使用了内联函数,返回了低优先级的并发队列。
1 | //内联函数,返回优先级最低的全局并发队列 |
选择底层的类
同样是字典实现,但是作者使用了更底层且快速的CFDictionary而没有用NSDictionary来实现。
其他知识点
禁用原生初始化方法并标明新定义的指定初始化方法
YYCache有4个供外部调用的初始化接口,无论是对象方法还是类方法都需要传入一个字符串(名称或路径)。
而两个原生的初始化方法被框架作者禁掉了:
1 | - (instancetype)init UNAVAILABLE_ATTRIBUTE; |
如果用户使用了上面两个初始化方法就会在编译期报错。
而剩下的四个可以使用的初始化方法中,有一个是指定初始化方法,被作者用NS_DESIGNATED_INITIALIZER
标记了。
1 | - (nullable instancetype)initWithName:(NSString *)name; |
指定初始化方法就是所有可使用的初始化方法都必须调用的方法。更详细的介绍可以参考我的下面两篇文章:
- iOS 代码规范中讲解“类”的这一部分。
- 《Effective objc》干货三部曲(三):技巧篇中的第16条。
异步释放对象的技巧
为了异步将某个对象释放掉,可以通过在GCD的block里面给它发个消息来实现。这个技巧在该框架中很常见,举一个删除一个内存缓存的例子:
首先将这个缓存的node类取出,然后异步将其释放掉。
1 | - (void)removeObjectForKey:(id)key { |
为了释放掉这个node对象,在一个异步执行的(主队列或自定义队列里)block里给其发送了class
这个消息。不需要纠结这个消息具体是什么,他的目的是为了避免编译错误,因为我们无法在block里面硬生生地将某个对象写进去。
其实关于上面这一点我自己也有点拿不准,希望理解得比较透彻的同学能在下面留个言~ ^^
内存警告和进入后台的监听
YYCache默认在收到内存警告和进入后台时,自动清除所有内存缓存。所以在YYMemoryCache的初始化方法里,我们可以看到这两个监听的动作:
1 | //YYMemoryCache.m |
然后实现监听到消息后的处理方法:
1 | //内存警告时,删除所有内存缓存 |
判断头文件的导入
1 |
在这里作者使用__has_include来检查Frameworks是否引入某个类。
因为YYWebImage已经集成YYCache,所以如果导入过YYWebImage的话就无需重再导入YYCache了。
最后的话
通过看该组件的源码,我收获的不仅有缓存设计的思路,还有:
- 双向链表的概念以及相关操作
- 数据库的使用
- 互斥锁,信号量的使用
- 实现线程安全的方案
- 变量,方法的命名以及接口的设计
相信读过这篇文章的你也会有一些收获~
如果能趁热打铁,下载一个YYCache源码看就更好啦~
————————————————- 2018年7月17日更新 ————————————————-
注意注意!!!
笔者在近期开通了个人公众号,主要分享编程,读书笔记,思考类的文章。
- 编程类文章:包括笔者以前发布的精选技术文章,以及后续发布的技术文章(以原创为主),并且逐渐脱离 iOS 的内容,将侧重点会转移到提高编程能力的方向上。
- 读书笔记类文章:分享编程类,思考类,心理类,职场类书籍的读书笔记。
- 思考类文章:分享笔者平时在技术上,生活上的思考。
因为公众号每天发布的消息数有限制,所以到目前为止还没有将所有过去的精选文章都发布在公众号上,后续会逐步发布的。
而且因为各大博客平台的各种限制,后面还会在公众号上发布一些短小精干,以小见大的干货文章哦~
扫下方的公众号二维码并点击关注,期待与您的共同成长~