Kingfisher 源码阅读笔记(3)

这是我参与更文挑战的第13天,活动详情查看: 更文挑战

Kingfisher 源码阅读笔记(1) 的续篇。

Kingfisher 源码阅读笔记(2) 的续篇。

本文为在阅读 Kingfisher 源码时的收货。这篇文章主要写负责内存存储的 MemoryStorage 的设计。

MemoryStorage 为 Kingfisher 中负责内存存储的命名空间。其中,真正用来管理所有存储事务的是 Backend (应该翻译成“后端”?),而 Backend 中用来负责缓存的是 NSCache

let storage = NSCache<NSString, StorageObject<T>>()
复制代码

Backend 中的方法基本都是围绕 NSCache 来进行的:

  1. 通过 NSCache 的 setObject(_ obj:, forKey:, cost:) 方法,添加存储;
  2. 通过 NSCache 的 object(forKey:) 方法,获取指定的 key 对应的缓存,或者判断是否已经缓存;
  3. 通过 NSCache 的 removeObject(forKey:) 或者 removeAllObjects 方法,主动删除或者定期(2分钟)清理过期的存储。

曾经一个导致 crashes 的 bug

Kingfisher 曾经有过一个导致 crashes 的 bug,作者的修复方案为remove-cache-delegate。下面来分析一下导致这个 bug 的原因,以及作者的解决方案。

出现 bug 的原因

首先,NSCache 是线程安全的,不需要加线程锁。但是需要注意的是,在不同的线程中向 NSCache 中添加缓存数据时,如果达到了 NSCache 设置的内存上限和数量上线时,会在当前的线程中触发 NSCache 的代理( NSCacheDelegate)的 cache(_:, willEvictObject:) 方法,通知外界移出了对应的缓存数据。

其次,NSCache 没有提供可以遍历当前所有存储数据的方法,只能通过指定的 key 来获取: object(forKey:) 。所以,需要自己维护 NSCache 中所有缓存数据对应的 key,才能在清理过期的数据时,对 NSCache 中的所有数据进行遍历。

Kingfisher 中,内存中的缓存数据的默认有效期为 300s。两分钟一次的定期任务会检查数据是否已过期,如果已过期,则会从内存中删除。

负责记录所有已缓存数据对应的 key 的定义如下:

var keys = Set<String>()
复制代码

在有 bug 的 Kingfisher 版本中,所有牵扯到的修改缓存 keys 的事件可以看下图:

牵扯到修改缓存 keys 的事件.001.jpeg

在主线程中:

  1. 定时清理过期缓存(默认为:2分钟一次)时会移出对应的 key;
  2. 用户主动删除缓存数据;
  3. 下载图片之后,添加缓存时会添加对应的 key;
  4. 在添加缓存数据时,如果达到了设置的 NSCache 内存上限或数量上线,NSCache 会自动进行清理操作,从而触发回调方法 cache(_: ,willEvictObject:),删除对应的 key。此时回调在主线程

在负责操作 io 的 ioQueue 线程中:

  1. 从磁盘获取缓存图片之后,添加缓存时会添加对应的 key;
  2. 在添加缓存数据时,如果达到了设置的 NSCache 内存上限或数量上线,NSCache 会自动进行清理操作,从而触发回调方法 cache(_: ,willEvictObject:),删除对应的 key。此时回调在 ioQueue 线程中

出现 bug 的原因就是在回调方法 cache(_: ,willEvictObject:) 中修改 keys 集合(Set)没有加锁。

解决方案

为了解决这个问题,首先想到就是加锁,而且需要将原来的 NSLock 换成递归锁 NSRecursiveLock,因为,在清理过期缓存时,会循环调用 NSCache 的清理方法 removeObject(forKey:)

但是 Kingfisher 的作者 onevcat 并不想为此引入递归锁,而是打破了严格的用 keys 来跟踪缓存对象,以此避免额外的锁操作

具体来说就是直接移出了 NSCache 的回调监听,导致的结果就是用户直接删除缓存数据时,会同时移出对应的 key;而 NSCache 触发的移出缓存操作,并不会移出对应的 key。

Keys trackes the objects once inside the storage. For object removing triggered by user, the corresponding key would be also removed. However, for the object removing triggered by cache rule/policy of system, the key will be remained there until next removeExpired happens.

Breaking the strict tracking could save additional locking behaviors.
See github.com/onevcat/Kin…

Kingfisher 的解决方案给我们在解决多线程方案时提供了新的思路:严格的准确有时候是需要代价的,有时候放弃严格的准确,可能会带来效率的提升。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享