October 14, 2018
image 缓存分为两种:内存缓存和磁盘缓存。在 ImageCache.swift
类中分别有两种实例来处理
// Memory
fileprivate let memoryCache = NSCache<NSString, AnyObject>()
// Disk
fileprivate let ioQueue: DispatchQueue
fileprivate var fileManager: FileManager!
默认的最大缓存时间是一周
open var maxCachePeriodInSecond: TimeInterval = 60 * 60 * 24 * 7
下面来看看缓存图片的核心代码
open func store(_ image: Image,
original: Data? = nil,
forKey key: String,
processorIdentifier identifier: String = "",
cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default,
toDisk: Bool = true,
completionHandler: (() -> Void)? = nil)
{
// 先在内存中缓存
let computedKey = key.computedKey(with: identifier)
memoryCache.setObject(image, forKey: computedKey as NSString, cost: image.kf.imageCost)
// 主线程中执行回调
func callHandlerInMainQueue() {
if let handler = completionHandler {
DispatchQueue.main.async {
handler()
}
}
}
// 默认保存磁盘
if toDisk {
// 队列中异步保存
ioQueue.async {
// 根据 Image 序列化成 data 数据
if let data = serializer.data(with: image, original: original) {
// 如果缓存路径不存在则创建一个默认缓存路径
if !self.fileManager.fileExists(atPath: self.diskCachePath) {
do {
try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
} catch _ {}
}
// 写入磁盘缓存
self.fileManager.createFile(atPath: self.cachePath(forComputedKey: computedKey), contents: data, attributes: nil)
}
// 执行完成回调
callHandlerInMainQueue()
}
} else {
// 如果不需要写入磁盘则直接执行完成回调
callHandlerInMainQueue()
}
}
总结一下该方法主要做的事情:
- 首先在内存中缓存一份
- 嵌套定义了一个函数,负责在主线程中执行完成时的回调
- 除非显式的指明不进行磁盘缓存,否则默认是在磁盘中也缓存一份
- 先判断目标路径是否存在,不存在则创建一个默认缓存路径
- 利用
FileManager
写入磁盘缓存中 - 执行完成的回调闭包
- 如果不需要缓存磁盘则直接执行回调闭包
ImageCache
还提供了另外一个从缓存中取图片的方法 retrieveImage(forKey key:options:completionHandler:) -> RetrieveImageDiskTask?
,简单看看它的实现过程
// 守卫一下,没有完成回调,直接返回空任务 task
guard let completionHandler = completionHandler else {
return nil
}
var block: RetrieveImageDiskTask?
let options = options ?? KingfisherEmptyOptionsInfo
let imageModifier = options.imageModifier
// 根据 图片的唯一 computedKey 去内存中找,如果找到了直接执行完成回调
if let image = self.retrieveImageInMemoryCache(forKey: key, options: options) {
// 回调队列中异步执行
options.callbackDispatchQueue.safeAsync {
completionHandler(imageModifier.modify(image), .memory)
}
} else if options.fromMemoryCacheOrRefresh { // 如果显式标记只从磁盘中找,那就回调没有找到
options.callbackDispatchQueue.safeAsync {
completionHandler(nil, .none)
}
} else {
// 没有找到,创建一个 DispatchWorkItem 去磁盘中找
var sSelf: ImageCache! = self
block = DispatchWorkItem(block: {
// 根据 computedKey 开始在磁盘中取
if let image = sSelf.retrieveImageInDiskCache(forKey: key, options: options) {
// 如果显式的标记了后台解码图片
if options.backgroundDecode {
// processQueue 队列中异步解码图片,再只在内存中缓存一份
sSelf.processQueue.async {
let result = image.kf.decoded
sSelf.store(result,
forKey: key,
processorIdentifier: options.processor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: false,
completionHandler: nil)
// callbackDispatchQueue 队列异步执行完成回调
options.callbackDispatchQueue.safeAsync {
completionHandler(imageModifier.modify(result), .disk)
sSelf = nil
}
}
} else {
// 不用后台解码图片则直接在内存缓存图片
sSelf.store(image,
forKey: key,
processorIdentifier: options.processor.identifier,
cacheSerializer: options.cacheSerializer,
toDisk: false,
completionHandler: nil
)
// 完成回调
options.callbackDispatchQueue.safeAsync {
completionHandler(imageModifier.modify(image), .disk)
sSelf = nil
}
}
} else {
// 磁盘和内存中都没找到图片,回调通知没有找到图片
options.callbackDispatchQueue.safeAsync {
completionHandler(nil, .none)
sSelf = nil
}
}
})
// ioQueue 异步执行这个任务
sSelf.ioQueue.async(execute: block!)
}
return block
总结一下就是
- 先判断有没有完成回调,没有则提前返回;
- 根据图片 url 生成的 computedKey 唯一标识符去内存中找,如果找到了就执行回调结果并返回;
- 没有找到则创建一个
dispatchworkItem
去磁盘中找,找到了在内存中缓存一份在回调结果并返回; - 还没有找到就回调没有找到的结果。
此外,ImageCache
还提供了清除缓存的方法,这里清除同样也分为内存清除和磁盘的清理。
内存清理的过程很简单
@objc public func clearMemoryCache() {
memoryCache.removeAllObjects()
}
磁盘清理也不复杂
open func clearDiskCache(completion handler: (()->())? = nil) {
ioQueue.async {
do {
// 根据 diskCachePath 删除文件
try self.fileManager.removeItem(atPath: self.diskCachePath)
// 再根据 diskCachePath 重新创建一个空文件夹
try self.fileManager.createDirectory(atPath: self.diskCachePath, withIntermediateDirectories: true, attributes: nil)
} catch _ { }
// 主线程中回调完成结果
if let handler = handler {
DispatchQueue.main.async {
handler()
}
}
}
}
ImageCache
默认会为我们提供了 App 进入后台时清理过期图片缓存文件的功能
@objc public func backgroundCleanExpiredDiskCache() {
// 守卫一下,如果 application 单例不存在则直接返回
guard let sharedApplication = Kingfisher<UIApplication>.shared else { return }
// 结束后台任务
func endBackgroundTask(_ task: inout UIBackgroundTaskIdentifier) {
sharedApplication.endBackgroundTask(task)
task = UIBackgroundTaskIdentifier.invalid
}
var backgroundTask: UIBackgroundTaskIdentifier!
backgroundTask = sharedApplication.beginBackgroundTask {
endBackgroundTask(&backgroundTask!)
}
// 清理过期缓存
cleanExpiredDiskCache {
endBackgroundTask(&backgroundTask!)
}
}
cleanExpiredDiskCache(completion:)
方法实现如下
// 在 ioQueue 队列中异步执行
ioQueue.async {
// 在磁盘中遍历分别找到
// URLsToDelete: 数组 需要删除的 url 图片
// diskCacheSize: 计算出的磁盘缓存大小
// cacheFiles: 数组 保存了已经缓存的文件
var (URLsToDelete, diskCacheSize, cachedFiles) = self.travelCachedFiles(onlyForCacheSize: false)
// 根据这个数组遍历删除图片文件
for fileURL in URLsToDelete {
do {
try self.fileManager.removeItem(at: fileURL)
} catch _ { }
}
if self.maxDiskCacheSize > 0 && diskCacheSize > self.maxDiskCacheSize {
let targetSize = self.maxDiskCacheSize / 2
// 按时间排序,删除最旧的文件
let sortedFiles = cachedFiles.keysSortedByValue {
resourceValue1, resourceValue2 -> Bool in
if let date1 = resourceValue1.contentAccessDate,
let date2 = resourceValue2.contentAccessDate
{
return date1.compare(date2) == .orderedAscending
}
return true
}
for fileURL in sortedFiles {
// 尝试删除
do {
try self.fileManager.removeItem(at: fileURL)
} catch { }
// 装进待删除的数组中
URLsToDelete.append(fileURL)
// 获取每个图片的大小,并从磁盘总大小中减去
if let fileSize = cachedFiles[fileURL]?.totalFileAllocatedSize {
diskCacheSize -= UInt(fileSize)
}
// 一旦磁盘缓存大小已经小于目标大小就停止
if diskCacheSize < targetSize {
break
}
}
}
// 主队列的主线程中发送完成清除过期图片的通知
DispatchQueue.main.async {
if URLsToDelete.count != 0 {
let cleanedHashes = URLsToDelete.map { $0.lastPathComponent }
NotificationCenter.default.post(name: .KingfisherDidCleanDiskCache, object: self, userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes])
}
// 执行回调
handler?()
}
}
ImageCache
类是 Kingfisher 的核心之一,它支撑起了整个图片的缓存逻辑,提供了取图,存图和清除缓存的核心功能。这里面涉及了异步编程的很多地方,非常值得一读。
最后一篇,将会分析一下剩余 Kingfisher 提供到的一些功能,同时也会对里面值得学习的 swift 编程技巧,做一个浅显的分析。