我们在处理数据的时候,第一步就会接触到缓存。这里就来看看应该选择什么样的缓存来解决你的问题。
在一般的场景下,我们并不会去考虑缓存的所带来的优化与损耗,因为大部分场景这一点损耗都可以忽略不计,但是在某些极限情况下,这就不能被忽略了。那么我们就需要来思考采用什么样的方式去缓存数据。
首先,缓存可以被分为两类:
- 内存缓存
- 磁盘缓存,也可以称为持久化
那么我们按照这两者来分析下,现在比较流行的几种方式。
Memory Cache
NSCache
官方提供的缓存一般来说已经足够使用了,线程安全,功能也特别完善,拥有灵活的控制,在内存不足时也会回收内存。底层应该是基于hash表的,所以性能表现也十分优秀。
但是系统实现的缓存失效策略却是未知的,无法保证是LRU策略。同时NSCache会在系统回到后台的时候清空缓存,如果你希望在app的生命周期内都可以缓存,那么NSCache难以做到。
一般来说NSCache是首选,除非需要一些特殊的要求。
NSArray & NSDictionary
这两者是系统内置集合类型,可以用作缓存数据,但是是非线程安全的,在使用中需要特别小心,同时需要自己去控制缓存失效。
在数据量比较小的时候,这两者都是没有问题的,但是在数据量变得庞大的时候就会有一定的性能问题。
由于NSArray是数组实现,位置查找的效率为1,内容查找的效率为n,所以在大量频繁的内容查找中,会降低其性能。这时候推荐NSSet或者NSOrderedSet来替代NSArray。NSSet的底层是hash表。
在使用hash表的时候,如何来更好的实现其查找性能呢,就需要保持key的hash随机分布。一般来说我们都会使用string作为key,在自己实现的key值别忘了重写hash。
在某些情境下,使用集合类型也非常有效。
其他
也可以根据自己的需求来设计自己的容器类,比如自平衡二叉树、B树等。不过首先要了解上述几种是否已经满足自己的需求。
Persistent
持久化缓存拥有多种不同的数据格式和存储方式,这里按照几种方式和开源库来看看各自的方案。
NSUserDefaults
这是系统提供的最简单的一种保存数据的方式,自带了缓存和同步机制,利用的是NSCoding的方式,所以NSCoding拥有的缺陷UserDefault也会拥有。
当数据量增多变大,会导致plist文件太大,从而影响加载性能,所以只能保存少量的小型数据。
NSCoding
这是系统提供的持久化方案,不仅仅保存了数据,同时也保存了类别信息。但这也带来了部分缺陷,那就是数据兼容问题。
当软件升级时,修改了类名,或者改动了内部成员实现,就可能导致数据错误设置崩溃。所以需要小心控制数据版本信息。
由于这种方式是一次性的读取与写入,在数据量大的时候也会产生一些问题。同时这种方式并不适合部分读取部分修改的场景,如果数据比较大需要重新考虑。
JSON
另一种代替NSCoding的方式便是使用JSON来保存,虽然在数据兼容性上会比NSCoding稍微优秀一些,但依然没有根本解决这个问题,所以这是一个可选方案。
YYKit
YYKit使用了LRU策略,明确了缓存失效策略。
内存缓存使用了线性链表+NSDictionary来实现,由于LRU的特性,插入永远在开始,而删除永远在结尾,所以拥有较高的性能。但是查找还是依赖于hash表来实现。这样在插入和查找都避免了对方的缺陷,实现了更加高效的结果。缺点是需要同时保存和修改两份数据索引。
磁盘缓存使用了sqlite来保存文件缓存信息(filename, last_modify_time),所以在读写小数据的时候(20KB)会直接在sqlite中读写,而不会生成一个独立的文件。所以在小文件和未命中的情况下效率会高很多。而读写大文件时,效率会降低一些,考虑到sqlite的缓存和执行,并不会降低太多。由于sqlite对时间创建了索引,所以在缓存过期查找上面会优秀一些。这种设计解决了小文件和未命中的效率问题,但是并不能实现高并发读写文件。
这种按照数据量来区分数据存储方式的方法解决了大文件和小文件之间的性能差别,但也给缓存系统带来了一定的复杂性。同时如果sqlite的索引失效会导致查找效率的降低。
YYCache带来了一种通用型的存储方式,但在很多时候还是需要自己来实现特定的需求。
PINCache
使用了大量的Lock来处理多线程读写,拥有异步读写接口,没有太多的特别优化。
磁盘缓存单纯使用了文件缓存,在初始化的时候就把整个目录及其元素的属性读到内存,来提高效率,但是使用的是数组存储,效率一般。
SPTPersistentCache
他将数据信息通过memory map的方式写到了文件头部,说是为了并发读写,但这也时每次更新updateTime需要写整个文件,这样必定会导致性能降低。个人建议还是把文件信息写到另一个文件中,方便内存缓存。
这种方式比较适合的场景是只读数据,对于经常变化的数据反而可能会降低性能。
Haneke & SDWebImage
这两者非常相似,Haneke功能更少,但是更加紧凑,代码结构也更加好。而SDWebImage功能非常完善,使用的人也非常的多。但也并非没有瑕疵。
图片缓存读取全部在一个子线程中进行,导致在高并发读取的时候会阻塞线程,同样下载和解码也会有类似的问题。这么设计同时也是为了保证线程安全,所以采用了顺序队列的操作,但是对于单文件来说,这样是正确的,对于多文件来说没有必要这样做。在目前移动端以及pc端来看,性能的瓶颈还不在这个地方,依然在IO上面,所以除非特殊情况,不会出现性能问题。
图片的二次处理能力不够(比如手动加圆角,裁剪,滤镜),需要自己去处理并且缓存,这对于一个图片库来说是一个遗憾,好在目前大部分工作CDN都会帮我们做掉。
预加载图片无法和正常加载使用同一套机制,预加载和正常加载如果同时触发会加载2次。SD没有考虑到预加载和正常加载使用同一个Operation缓存,导致双方都会触发真实的下载,从而浪费了流量。
作为一个图片库,图片一般内容都比较大,所以采用了文件缓存的机制,使用key作为文件名。由于文件系统自身拥有的缓存,所以在查找的效率上并不低。
FastImageCache
这也是一个图片缓存方案,增加了处理图片的一些中间件。
该作者认为效率问题主要出现在图片从磁盘读取到内存,再进行解压,以及渲染前的内存拷贝。解决这类问题的最好方法就是进行memory map,将处理好的内容直接写入文件,这样在下一次载入的时候就不需要重新处理了。
作者也指出了这种方式会导致一张高压缩率的图片,进行内存映射后会变得很大,这一非常大的缺陷。
内存映射也是一种很好的方案,在存储资源丰富,而处理需要很长时间的情况下,是最简单的处理方式。随着现在设备性能的提高,一般不会存在处理的性能瓶颈,所以也需要按情景来判断。
最后
这里分析了几种内存缓存和磁盘缓存的情况,一般内存缓存较为简单,不会有太多的性能问题,而磁盘缓存拥有很多的方案,每种方案都有各自的适用场景,需要根据自身的实际情况来选择。
这里所列的几种磁盘缓存都比较简单,之后会介绍一些比较复杂的存储方案。