NSCache

Posted by LML on March 23, 2020

前言

开发过程中涉及到了对图片的内存缓存,顺便学习了下NSCache相关知识,并初步了解了一下SDWebimage关于缓存的部分。本文主要介绍NSCache,下一篇在介绍SDWebImage对缓存的处理。
后续有时间在看下磁盘缓存NSURLCache、YYCache。YYCache是比较经典的库,可以仔细看下。

NSCache相比于NSMutableDictionary实现缓存功能的优点

  • NSCache是线程安全的,使用NSMutableDictionary自定义实现缓存时需要考虑加锁和释放锁,NSCache已经帮我们做好了这一步。
  • 在内存不足时NSCache会自动释放存储的对象,不需要手动干预
  • NSCache的键key不会被复制,所以key不需要实现NSCopying协议

NSCache 使用

//The name of the cache
@property (copy) NSString *name;
@property (nullable, assign) id<NSCacheDelegate> delegate;

- (nullable ObjectType)objectForKey:(KeyType)key;

//设置key、value
- (void)setObject:(ObjectType)obj forKey:(KeyType)key; // 0 cost

/*
设置key、value
cost表示obj这个value对象的占用的消耗?可以自行设置每个需要添加进缓存的对象的cost值
这个值与后面的totalCostLimit对应,如果添加进缓存的cost总值大于totalCostLimit就会自动进行删除
感觉在实际开发中直接使用setObject:forKey:方法就可以解决问题了
*/
- (void)setObject:(ObjectType)obj forKey:(KeyType)key cost:(NSUInteger)g;

//根据key删除value对象
- (void)removeObjectForKey:(KeyType)key;

//删除保存的所有的key-value
- (void)removeAllObjects;

/*
NSCache能够占用的消耗?的限制
当NSCache缓存的对象的总cost值大于这个值则会自动释放一部分对象直到占用小于该值
非严格限制意味着如果保存的对象超出这个大小也不一定会被删除
这个值就是与前面setObject:forKey:cost:方法对应
*/
@property NSUInteger totalCostLimit;    // limits are imprecise/not strict

/*
缓存能够保存的key-value个数的最大数量
当保存的数量大于该值就会被自动释放
*/
@property NSUInteger countLimit;    // limits are imprecise/not strict
/*
这个值与NSDiscardableContent协议有关,默认为YES
当一个类实现了该协议,并且这个类的对象不再被使用时意味着可以被释放
*/
@property BOOL evictsObjectsWithDiscardedContent;

@end

//NSCacheDelegate协议
@protocol NSCacheDelegate <NSObject>
@optional
//上述协议只有这一个方法,缓存中的一个对象即将被删除时被回调
- (void)cache:(NSCache *)cache willEvictObject:(id)obj;
@end

NSCache 释放时机

  • 手动:
    • 手动调用removeObjectForKey:方法
    • 手动调用removeAllObjects
  • 自动
    • NSCache缓存对象自身被释放
    • 缓存中对象的个数大于countLimit,或,缓存中对象的总cost值大于totalCostLimit
    • 程序进入后台后
    • 收到系统的内存警告
  • 清理的时候是全部清理还是清理到limit
    • 进入后台+ OOM——清理quanbu
    • 其余清理到limit

NSCache原理分析

基本实现

  • NSMutableDictionary: 保存数据和索引
  • NSLock: 通过每次lock()和unlock()保证了字典读写操作的线程安全
  • NSCacheKey: 作为字典key的封装类,自身实现了hash和isEqual方法;即使存在没有实现Hashable的对象作为key,也可以借助NSObject提供的hashValue
  • NSCacheEntry: 字典value的封装类,以及包含额外信息:
    • cost: 记录对象占用内存空间的size值
    • prevByCost: 链表中的前一个对象
    • nextByCost: 链表中的后一个对象
  • NSCache为什么还要把缓存的对象相互连接成一个链表呢?
    • 方便自动清理。从实现逻辑可以看出,NSCache还包含一个head指针,每次给缓存字典里增加一个新的对象时,同时执行链表插入操作。插入规则是:根据对象的占用内存的空间值cost的大小,将占用内存最小(即cost最小)的对象作为head,向后按大小顺序将对象插入到链表中合适的位置,最终形成一个按cost由小到大顺序排练成的有序链表。链表的特点是节点的快速插入和删除,所以链表的创建几乎可以不用在意性能损耗。当一个有序链表形成后,每次添加缓存对象时,都会检查是否达到缓存设置的峰值,如果超过峰值,就开始从head位置依次删除对象,直到缓存占用空间/数量回归到设定限制之内。

缺点

  • 缺失必要的缓存淘汰算法
    • 每次自动清理缓存的时候,删除节点的顺序是从链表的head开始,依次向后清理缓存数据。那么问题在于链表的排序是cost排序,如果出现对缓存对象无法估算占用空间的话,就会导致建立的链表丧失了“有序”的概念,每次添加cost为0的对象,就只能保存在head位置。
    • 比如我用NSCache来缓存图片,然后设置countLimit等于10,即最多允许缓存10张图片。缓存的时候正常调用[cache setObject:image forKey:imageURL],而该方法默认缓存对象的cost为0,即没法估算图片的占用存储空间,因此每次缓存图片时,只能把图片依次插入在head节点。等到缓存图片数量超过10张以后,NSCache因为数量限制原因,开始从head位置清理图片,这就导致每次只能清理掉最新缓存的图片,而最早保存的10张图片就一直占据着缓存里,不会释放,这样的实现其实并不科学对吧。

参考

https://www.jianshu.com/p/239226822bc6