一、为什么使用缓存
缓存的目的是以空间换时间。
出于优化考虑:服务器压力、用户体验、用户流量等;
出于功能考虑:离线存储、微信会话列表、新闻列表等;
重度使用缓存的 APP:微信、微博等。
二、iOS 上的缓存框架
NSCache、PINCache、YYCache、SDWebImage(分析 SDImageCache 部分)
1、NSCache
苹果提供的一个简单的内存缓存;
类似 NSDictionary 一个可变的集合;
提供了可设置缓存大小与内存大小限制的方式;
保证了处理的数据的线程安全性;
内存警告时自动清理部分缓存数据
2、PINCache
PINCache 项目是在 Tumblr 宣布不再维护 TMCache 后,由 Pinterest 维护和改进的基于 TMCache 的一个内存缓存,修复了 TMCache 存在的性能和死锁问题。
3、YYCache
YYCache 是国内开发者 ibireme 开源的一个线程安全的高性能缓存组件。
4、SDWebImage
SDWebImage 框架通过给 UIImageView 和 UIButton 添加分类,实现了一个异步下载图片并且支持缓存的功能。整个框架的接口简洁,分工明确。
三、线程安全
这些缓存框架都是线程安全的。
多线程操作共享数据不会出现意想不到的结果就是线程安全的,否则则是不安全的。多个线程同时访问或读取同一共享数据,每个线程的读到的数据是一样的,则不存在线程不安全。如果多个线程对同一资源进行读写操作,那么每个线程读到的结果不可预料,线程是不安全的。
接下来:
1、iOS 中有哪些方法保证线程安全;
2、这些缓存框架是如何保证线程安全的。
四、iOS 开发中的锁
1、OSSPinLock
2、dispatch_semaphore
3、pthread_mutex pthread_mutex(recursive) NSRecursiveLock
4、NSLock
5、NSCondition NSConditionLock
6、@synchronized
YYCache 和 PINCache 在内存缓存使用的是 pthread_mutex,在磁盘缓存使用的是Semaphone。 SDWebImage 在内存缓存使用的是 NSCache,本身就是线程安全的。在磁盘缓存使用串行队列来保证线程安全。
五、缓存
1、缓存的读取
缓存读取的逻辑大致为:
先访问内存缓存,再访问磁盘缓存(写入、读取、查询、删除);
读取缓存时,如果在内存缓存中无法获取对应的缓存,则会去磁盘缓存中寻找。如果在磁盘缓存中找到了对应的缓存,则会将该对象再次写入内存缓存中,保证下一次尝试获取同一缓存时能够在内存中就能返回,提高速度。
2、二级缓存
MemoryCache
PINMemoryCache 通过维护一个 dic 记录 object 最后一次访问的时间,通过排序来实现 LRU;
YYMemoryCache 缓存内部通过双向链表和 NSDictionary 实现 LRU 淘汰算法;
SDImageCache 缓存使用的是 NSCache。
DiskCache
PINDiskCache 和 SDImageCache 是基于文件系统的;
YYDiskCache 采用了 SQLite&文件系统实现。
3、SDWebImage
SDImageCache 默认图片清理时间为一周。
SDImageCache 缓存的写入:
1、将图片缓存在内存中;
2、判断图片格式是 png 或 jpeg,将图片转化为 NSData 数据;
3、如果是在 mac_os 系统中,直接将图片转化为 NSBitmapImageRep 数据;
4、获取图片的存储路径,其中图片的文件名通过传入的 key 经过 md5 加密后获得的;
5、将图片存储在磁盘中。
SDImageCache 缓存的删除:
1、获取磁盘中图片的最后修改日期;
2、根据日期将图片进行分类,将超过最长存放时间的文件存储在删除数组中,其他的文件信息存储在另一个 dic 中,并计算除去要删除的文件之外的文件大小;
3、根据删除数组中的文件路径,将对应的文件删除;
4、判断剩下的文件大小是否超过用户现在的最大容量;
5、如果超过,则将剩余文件按修改时间进行升序排列,删除修改时间最早的文件,直到剩余文件大小小于最大磁盘容量。
清理时机:
系统内存不足时,会将内存中所有的图片缓存删除;
当系统进入后台时,会对磁盘中的文件数据进行清理;
当收到程序关闭通知时,会对磁盘中的文件数据进行清理。
4、PINCache
PINCache 是线程安全的键值对缓存框架,用于缓存一些临时数据或需要频繁加载的数据。
PINCache 除了可以按键取值、按键存值、按键删除值之外,还可以移除某个日期之外的缓存数据、删除所有缓存、限制缓存大小。
PINMemoryCache:
维护了三个 dic,分别为_dictionary、_dates、_costs,字典的 key 相同,value 分别为对象、最后访问日期、大小。
清理缓存:
内存警告和进入后台时,默认自动清除所有的内存缓存。
PINDiskCache
PINDiskCache 以文件形式存储缓存,在内存中维护了两个 dic分别为 _dates、_sizes,分别存储了文件的最后编辑时间和文件大小。
在初始化时子线程遍历硬盘缓存初始化这两个值,开发中可以根据业务逻辑调用api删除硬盘缓存。
支持清理的维度:age、byte。
5、YYCache
YYCache:提供了最外层的接口,调用了 YYmemoryCache 和 YYDiskCache 的相关方法;
YYMemoryCache:负责处理容量小,相对高速的内存缓存。线程安全,支持手动和自动清理缓存等功能;
_YYLinkedMap:YYMemoryCache 使用的双向链表类;
_YYLinkedMapNode:是 _YYLinkedMap 使用的节点类;
YYDiskCache:负责处理容量大,相对低速的磁盘缓存。线程安全,支持异步操作,自动和手动清理缓存等功能;
YYKVStorage:YYDiskCache 的底层实现类,用于管理磁盘缓存;
YYKVStorageItem:内置在 YYKVStorage 中,是 YYKVStorage 内部用于封装某个缓存的类。
YYMemoryCache:
将需要缓存的对象与传入的 key 关联起来,类似于 NSCache。
不同于 NSCache 的是,它的内部有:
缓存淘汰算法:LRU 算法来淘汰使用频率较低的缓存;
缓存清理策略:三个维度分别为 count(缓存数量)、cost(开销)、age(距上一次的访问时间)。可根据不同的需求清理某一维度超标的缓存。
无论从哪一维度清理缓存,都是从使用频率最低的那个缓存开始清理。
在 YYMemoryCache 中,使用了双向链表来保存这些缓存:
当写入一个新的缓存时,要把这个缓存节点放到链表头部,并且原链表头部的缓存节点要变成现在链表的第二个节点;
当访问一个已有的缓存时,要把这个缓存节点移动到链表的头部,原位置两侧的缓存接上,原头部节点变为第二个;
(根据清理维度)自动清理缓存时,要从链表的最后端逐个清理。
清理缓存:
内存警告和进入后台时,默认自动清除所有的内存缓存。
YYDiskCache:
与第一级缓存相同点是:
都具有查询、写入、读取、删除缓存的接口;
不直接操作缓存,通过另一个类(YYKVStorage)来操作;
使用 LRU 算法来清理缓存;
支持 cost、count、age 三个维度清理不符合标准的缓存。
不同点是:
1、根据缓存数据的大小来采取不同的形式的缓存:
数据库 sqlite:针对小容量缓存,缓存的 data 和元数据都保存在数据库里;
文件+数据库形式:针对大容量缓存,缓存的 data 写在文件系统中,其元数据保存在数据库中。
2、除了 cost、count、age 三个维度,还添加了一个磁盘容量的维度。
六、缓存框架的选型
由图可见:
1、YYMemoryCache 的性能不错,仅次于 NSDictory+OSSpinLock;
2、NSCache 的写入性能较差。读写性能不错;
3、PINMemoryCache 的读写性能还可以,但读取速度差于 NSCache;
由图可见:
1、存取小数据时(NSNumber),YYDiskCache的性能远远高于基于文件存储的库;
2、较大数据的存取性能比较接近,但得益于 SQLite 存储的元数据,YYDiskCache 实现了 LRU 淘汰算法、更快的数据统计,更多的容量控制选项。
总结
1、选择合适的线程锁;
2、选择合适的数据结构;
3、选择合适的线程来操作不同的任务;
4、选择合适的存储方式;
5、选择底层的类;
6、变量、方法的命名以及接口的设计。
ppt:https://github.com/yuetianlu/cache_ppt
参考:
https://blog.ibireme.com/2015/10/26/yycache/
https://juejin.im/post/5a657a946fb9a01cb64ee761
https://juejin.im/post/5a4080d16fb9a0451969d0aa
https://www.cnblogs.com/fengmin/p/5318782.html
https://bestswifter.com/ios-lock/