一个缓存是由内存缓存和磁盘缓存组成,内存缓存提供容量小但高速的存取功能,磁盘缓存提供大容量但低速的持久化存储。
需要考虑的细节:
比如多线程读写安全、数量限制、总容量限制、存活时间限制、内存警告或应用退到后台时清空缓存等
在读取缓存的操作中,如果在内存缓存中无法获取对应的缓存,则会去磁盘缓存中寻找。如果在磁盘缓存中找到了对应的缓存,则会将该对象再次写入内存缓存中,保证在下一次尝试获取同一缓存时能够在内存中就能返回,提高速度。
memory cache:
- 缓存淘汰算法:使用
LRU(least-recently-used)
算法来淘汰(清理)使用频率较低的缓存。 - 缓存清理策略:使用三个维度来标记,分别是 count(缓存数量),cost(开销),age(距上一次的访问时间)。
缓存淘汰算法的目的在于区分出使用频率高和使用频率低的缓存,当缓存数量达到一定限制的时候会优先清理那些使用频率低的缓存。因为使用频率已经比较低的缓存在将来的使用频率也很有可能会低。
核心知识 :
- LRU: 缓存支持 LRU (least-recently-used) 最近最小使用淘汰算法。
- 缓存控制: 支持多种缓存控制方法:总数量、总大小、存活时间、空闲空间。
-
兼容性: API 基本和
NSCache
保持一致, 所有方法都是线程安全的。 - 同步异步接口:是否支持多方式读写
- 内存缓存 memory cache
- 对象释放控制: 对象的释放(release) 可以配置为同步或异步进行,可以配置在主线程或后台线程进行。
- 自动清空: 当收到内存警告或 App 进入后台时,缓存可以配置为自动清空。
- 磁盘缓存 disk cache
- 可定制性: 磁盘缓存支持自定义的归档解档方法,以支持那些没有实现 NSCoding 协议的对象。
- 存储类型控制: 磁盘缓存支持对每个对象的存储类型 (SQLite/文件) 进行自动或手动控制,以获得更高的存取性能。
关于锁的考虑:
OSSpinLock 的优先级反转问题
新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。
具体来说,如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 spin lock 的忙等状态从而占用大量 CPU。此时低优先级线程无法与高优先级线程争夺 CPU 时间,从而导致任务迟迟完不成、无法释放 lock。这并不只是理论上的问题,libobjc 已经遇到了很多次这个问题了,于是苹果的工程师停用了 OSSpinLock。
OSSpinLock
自旋锁的实现原理比较简单,就是死循环。当a线程获得锁以后,b线程想要获取锁就需要等待a线程释放锁。在没有获得锁的期间,b线程会一直处于忙等的状态。如果a线程在临界区的执行时间过长,则b线程会消耗大量的cpu时间,不太划算。所以,自旋锁用在临界区执行时间比较短的环境性能会很高。
dispatch_semaphore:
dispatch_semaphore实现的原理和自旋锁有点不一样。首先会先将信号量减一,并判断是否大于等于0,如果是,则返回0,并继续执行后续代码,否则,使线程进入睡眠状态,让出cpu时间。直到信号量大于0或者超时,则线程会被重新唤醒执行后续操作。
pthread_mutex:
pthread_mutex表示互斥锁,和信号量的实现原理类似,也是阻塞线程并进入睡眠,需要进行上下文切
NSLock:
NSLock在内部封装了一个 pthread_mutex,属性为 PTHREAD_MUTEX_ERRORCHECK。换。
NSCondition:
NSCondition封装了一个互斥锁和条件变量。互斥锁保证线程安全,条件变量保证执行顺序。
pthread_mutex(recursive):
pthread_mutex锁的一种,属于递归锁。一般一个线程只能申请一把锁,但是,如果是递归锁,则可以申请很多把锁,只要上锁和解锁的操作数量就不会报错。
NSRecursiveLock:
递归锁,pthread_mutex(recursive)的封装。
NSConditionLock:
NSConditionLock借助 NSCondition 来实现,本质是生产者-消费者模型。
@synchronized:
一个对象层面的锁,锁住了整个对象,底层使用了互斥递归锁来实现。
NSCache
NSCache 是苹果提供的一个简单的内存缓存,它有着和 NSDictionary 类似的 API,不同点是它是线程安全的,并且不会 retain key。我在测试时发现了它的几个特点:NSCache 底层并没有用 NSDictionary 等已有的类,而是直接调用了 libcache.dylib,其中线程安全是由 pthread_mutex 完成的。另外,它的性能和 key 的相似度有关,如果有大量相似的 key (比如 “1”, “2”, “3”, …),NSCache 的存取性能会下降得非常厉害,大量的时间被消耗在 CFStringEqual() 上,不知这是不是 NSCache 本身设计的缺陷。
Disk Cache
基于文件系统的,即一个 Value 对应一个文件,通过文件读写来缓存数据。他们的实现都比较简单,性能也都相近,缺点也是同样的:不方便扩展、没有元数据、难以实现较好的淘汰算法、数据统计缓慢。
基于 SQLite 数据库的。基于数据库的缓存可以很好的支持元数据、扩展方便、数据统计速度快,也很容易实现 LRU 或其他淘汰算法,唯一不确定的就是数据库读写的性能.
iPhone 6 64G 下,SQLite 写入性能比直接写文件要高,但读取性能取决于数据大小:当单条数据小于 20K 时,数据越小 SQLite 读取性能越高;单条数据大于 20K 时,直接写为文件速度会更快一些。
网友评论