源码阅读笔记NSCache

这里是源码地址,该文章是基于commit43d94d7 on 25 Jan 的NSCache版本

从源码看本质

NSCache可以用内存缓存对象(比如常见的图片),相比于NSMutableDictionary,使用NSCache会有以下特点:

  1. 线程安全
  2. KeyType不需要实现NSCopying
  3. 支持限制缓存空间和数量,达到峰值自动清理

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位置依次删除对象,直到缓存占用空间/数量回归到设定限制之内。

相比之下,AFNetworking也有个图片缓存类叫做AFAutoPurgingImageCache(这里是源码地址 版本基于d6db830 on 9 Oct 2018),从它的实现可以看出,每次缓存图片时都会按照图片的访问时间进行排序操作,然后再依次删除时间较早的图片。这时候其实就可以看出有序链表的优势所在了,不仅插入迅速,而且提前建立了顺序,这样总比每次删除时候临时排序要节省额外的开销。

缺失必要的缓存淘汰算法

看了源码的实现,每次自动清理缓存的时候,删除节点的顺序是从链表的head开始,依次向后清理缓存数据。那么问题在于链表的排序是cost排序,如果出现对缓存对象无法估算占用空间的话,就会导致建立的链表丧失了“有序”的概念,每次添加cost为0的对象,就只能保存在head位置。

比如我用NSCache来缓存图片,然后设置countLimit等于10,即最多允许缓存10张图片。缓存的时候正常调用[cache setObject:image forKey:imageURL],而该方法默认缓存对象的cost为0,即没法估算图片的占用存储空间,因此每次缓存图片时,只能把图片依次插入在head节点。等到缓存图片数量超过10张以后,NSCache因为数量限制原因,开始从head位置清理图片,这就导致每次只能清理掉最新缓存的图片,而最早保存的10张图片就一直占据着缓存里,不会释放,这样的实现其实并不科学对吧。

swift-corelibs-foundation开源以来,NSCache一直保持的应该就是这种简单的算法,而常见的缓存淘汰算法其实可以使用LRU之类的算法,优先清理最早访问的缓存数据,比如AFAutoPurgingImageCache的做法就是如此,但是使用链表来实现的话,也许效果会更好,而我们应该只需要将链表的排序规则改成让head永远指向最近访问的节点,然后从链表尾部开始依次向前删除数据,就可以了。

不过话说回来,使用NSCache做iOS开发,即使不去设置NSCache的空间或数量限制条件,只要响应App内存警告通知的时候及时清理缓存的话,使用起来也没什么问题,所以有没有更好的缓存淘汰算法,也变得无所谓了。

被遗弃的NSDiscardableContent

NSCache有个叫做evictsObjectsWithDiscardedContent属性,文档解释是:

If YES, the cache will evict a discardable-content object after its content is discarded. If NO, it will not. The default value is YES.

关于这里的discardable-content,据说是objc中实现了NSDiscardableContent协议的对象,这里是苹果的文档,里面描述了它的来龙去脉。但是NSCahe源码里没有对evictsObjectsWithDiscardedContent进行任何实现,可能是不想太复杂或者没有人用吧。

关于计算cost的想法

默认的NSCache没有实现便捷的下标方法Subscript,即cache[url] = image,我想之所以不提倡这么做,很可能是因为我们无法传递cost参数,可是图片占用的内存空间是可以计算(或者估算)的。所以如果被缓存的对象有能力计算出自己所占用内存的数值,为什么不使用协议来解决问题呢?

设想一下,我们尝试定义一个NSCacheObjectCostCalculatable的协议,只要求返回一个cost值。

@objc protocol NSCacheObjectCostCalculatable {
    var totalBytes: UInt { get }
}

然后我们让UIImage来实现它,这里参考了AFAutoPurgingImageCache的做法:

extension UIImage: NSCacheObjectCostCalculatable {
    var totalBytes: UInt {
        let bytesPerPixel: CGFloat = 4
        let pixelWidth = self.size.width * self.scale
        let pixelHeight = self.size.height * self.scale
        let estimatedBytes = bytesPerPixel * pixelWidth * pixelHeight
        return UInt(estimatedBytes)
    }
}

当然如果这个实现不够精准的话,也可以参考SDWebImage的计算图片cost的方式,毕竟NSCache的源码中也提到limits are imprecise/not strict这样的情况,所以这里就不争论计算cost哪家强的事情了。既然UIImage实现了自动计算占用空间的协议,那么源码就可以这么改:

open func setObject(_ obj: ObjectType, forKey key: KeyType) {
    if let calculatable = obj as? NSCacheObjectCostCalculatable {
        setObject(obj, forKey: key, cost: calculatable.totalBytes)
    } else {
        setObject(obj, forKey: key, cost: 0)
    }
}

我觉得,NSCache源码可读性还是很高的,思路简洁清晰。但是可能是历史遗留或兼容等问题,目前这个缓存类没有在Swift的源码中有更理想的实现,不过还是非常值得借鉴和学习的。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,843评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,538评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,187评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,264评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,289评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,231评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,116评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,945评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,367评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,581评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,754评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,458评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,068评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,692评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,842评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,797评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,654评论 2 354

推荐阅读更多精彩内容