写在前面,当时学习Kingfisher
的源码时因为工作的原因中途放下了,只写到图像下载,即ImageDownloader
模块,图像下载的代码因为各种函数作为形参,闭包声明实现等等看的我是云里雾里的,所以把那部分的代码写成了一篇,本以为ImageCache
相关模块也是同样的晦涩代码,所以打算另外边看边写一篇,结果发现ImageCache
的代码相较于ImageDownloader
清晰简单很多,写到一半的时候才发现很多东西都不用写出来,写出来反而感觉自己像个智障一样这种小事都要写出来,因此缓存篇就只能草草了事ORZ。
衔接上文,我们回到位于KingfisherManager
中的cacheImage
函数。
func cacheImage(_ result: Result)
{
switch result {
case .success(let value):
// Add image to cache.
let targetCache = options.targetCache ?? self.cache
targetCache.store(
value.image,
original: value.originalData,
forKey: source.cacheKey,
options: options,
toDisk: !options.cacheMemoryOnly)
{
_ in
if options.waitForCache {
let result = RetrieveImageResult(image: value.image, cacheType: .none, source: source)
completionHandler?(.success(result))
}
}
// Add original image to cache if necessary.
let needToCacheOriginalImage = options.cacheOriginalImage &&
options.processor != DefaultImageProcessor.default
if needToCacheOriginalImage {
let originalCache = options.originalCache ?? targetCache
originalCache.storeToDisk(
value.originalData,
forKey: source.cacheKey,
processorIdentifier: DefaultImageProcessor.default.identifier,
expiration: options.diskCacheExpiration)
}
print("needToCacheOriginalImage: \(needToCacheOriginalImage)")
if !options.waitForCache {
let result = RetrieveImageResult(image: value.image, cacheType: .none, source: source)
completionHandler?(.success(result))
}
case .failure(let error):
completionHandler?(.failure(error))
}
}
上篇我们知道,cacheImage
函数是作为形参传入ImageDownloader
中,在网络请求数据的结尾被回调。
首先逐行分析cacheImage
函数内部代码:
let targetCache = options.targetCache ?? self.cache
同样的,我们可以通过KingfisherOptionsInfo
配置我们自己的缓存管理对象,而当我们不传入时KingfisherManager
在单例初始化时初始化了一个命名为default
的缓存管理对象ImageCache
。
targetCache.store(
value.image,
original: value.originalData,
forKey: source.cacheKey,
options: options,
toDisk: !options.cacheMemoryOnly)
{
_ in
if options.waitForCache {
let result = RetrieveImageResult(image: value.image, cacheType: .none, source: source)
completionHandler?(.success(result))
}
}
ImageCache
实例对象调用store
函数缓存请求成功的图像数据,接收数个形参图像、图像数据、URL地址的缓存标识符以及配置对象以及是否缓存进磁盘(默认为true),并提供一个缓存结束的回调函数,当配置对象配置了要求在缓存结束才进行回调时,相应的逻辑被激活。
而不等待缓存结束则会在store进行的下一步即刻返回:
if !options.waitForCache {
let result = RetrieveImageResult(image: value.image, cacheType: .none, source: source)
completionHandler?(.success(result))
}
// Add original image to cache if necessary.
let needToCacheOriginalImage = options.cacheOriginalImage &&
options.processor != DefaultImageProcessor.default
if needToCacheOriginalImage {
let originalCache = options.originalCache ?? targetCache
originalCache.storeToDisk(
value.originalData,
forKey: source.cacheKey,
processorIdentifier: DefaultImageProcessor.default.identifier,
expiration: options.diskCacheExpiration)
}
若需要缓存原始图片则进行大致相同的逻辑,将数据写入磁盘。
接下来看store
函数内部代码:
open func store(_ image: Image,
original: Data? = nil,
forKey key: String,
options: KingfisherParsedOptionsInfo,
toDisk: Bool = true,
completionHandler: ((CacheStoreResult) -> Void)? = nil)
{
print("options.processor.identifier: \(options.processor.identifier)")
let identifier = options.processor.identifier
let callbackQueue = options.callbackQueue
let computedKey = key.computedKey(with: identifier)
// Memory storage should not throw.
memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration)
guard toDisk else {
if let completionHandler = completionHandler {
let result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))
callbackQueue.execute { completionHandler(result) }
}
return
}
ioQueue.async {
let serializer = options.cacheSerializer
if let data = serializer.data(with: image, original: original) {
self.syncStoreToDisk(
data,
forKey: key,
processorIdentifier: identifier,
callbackQueue: callbackQueue,
expiration: options.diskCacheExpiration,
completionHandler: completionHandler)
} else {
guard let completionHandler = completionHandler else { return }
let diskError = KingfisherError.cacheError(
reason: .cannotSerializeImage(image: image, original: original, serializer: serializer))
let result = CacheStoreResult(
memoryCacheResult: .success(()),
diskCacheResult: .failure(diskError))
callbackQueue.execute { completionHandler(result) }
}
}
}
逻辑大致分为两部分,分别是必然执行的缓存入内存空间和可选择执行的缓存入磁盘,我们先看内存缓存的相关代码
memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration)
memoryStorage
的功能是作为所有的内存缓存数据的容器,memoryStorage
是MemoryStorage.Backend
类型的实例对象,其在ImageCache
初始化时完成初始化,而我们知道,ImageCache
的实例对象被KingfisherManager
的单例所持有,所以memoryStorage
会一直保存记录在其中的缓存数据,直到KingfisherManager
因为各种因素而被销毁。
进入storeNoThrow
函数我们可以发现,函数内部利用传入的形参初始化了一个StorageObject
对象,该对象作为内存缓存基本单元,记录了关于数据、标识符以及过期时间的相关信息,然后,这个基本单元被插入到MemoryStorage.Backend
对象内部维护的一个NSCache
对象中,由此,图像数据在内存中的缓存就完成了。
既然缓存完成,我们自然要监听内存缓存该什么时候过期销毁。
MemoryStorage.Backend
对象在初始化函数内部初始化了一个定时器,该定时器默认120秒循环一次,通过MemoryStorage.Backend
对象内部维护的标识符数组获取每一个缓存对象,获取初始化时记录的过期时间进行过期销毁操作。
接着是磁盘缓存:
let serializer = options.cacheSerializer
if let data = serializer.data(with: image, original: original) {
self.syncStoreToDisk(
data,
forKey: key,
processorIdentifier: identifier,
callbackQueue: callbackQueue,
expiration: options.diskCacheExpiration,
completionHandler: completionHandler)
}
在开始缓存之前,所有的图像数据都会被转化为Data类型,而根据Data的头部byte的数据差异使用不同的格式转化。
public func data(format: ImageFormat) -> Data? {
let data: Data?
switch format {
case .PNG: data = pngRepresentation()
case .JPEG: data = jpegRepresentation(compressionQuality: 1.0)
case .GIF: data = gifRepresentation()
case .unknown: data = normalized.kf.pngRepresentation()
}
return data
}
当数据正确转化完成时,开始进行子线程的同步任务缓存入磁盘。
进入syncStoreToDisk
函数,我们可以看到,这一层的逻辑主要是调用diskStorage
对象进行磁盘存储,以及根据成功或者失败,在主线程中回调信息。
diskStorage
对象和memoryStorage
相同,都是在ImageCache
的初始化函数中进行初始化并持有,ImageCache
的初始化函数根据外部入参初始化了DiskStorage.Config
的配置对象,并利用配置对象对diskSrorage
进行初始化,其中涉及到磁盘缓存的具体沙盒路径、缓存文件夹的创建以及元更改队列的初始化等等。
回到diskStorage
的磁盘写入调用,我们查看store
函数的内部代码可以发现其逻辑大致与memoryStorage
相类似,通过config
配置对象初始化的FileManager
对象向沙盒内写入数据。
config.fileManager.createFile(atPath: fileURL.path, contents: data, attributes: attributes)
内存写入与磁盘写入的逻辑大致就是这样,虽然其中牵扯到一些许多关于初始化数据配置的代码,这部分因为过于分散难以一一描述。
回到KingfisherManager
,在retrieveImageFromCache
函数中我们从内存缓存和磁盘缓存中获取图像数据。
func retrieveImageFromCache(
source: Source,
options: KingfisherParsedOptionsInfo,
completionHandler: ((Result) -> Void)?) -> Bool
{
// 1. Check whether the image was already in target cache. If so, just get it.
let targetCache = options.targetCache ?? cache
let key = source.cacheKey
let targetImageCached = targetCache.imageCachedType(
forKey: key, processorIdentifier: options.processor.identifier)
let validCache = targetImageCached.cached &&
(options.fromMemoryCacheOrRefresh == false || targetImageCached == .memory)
if validCache {
targetCache.retrieveImage(forKey: key, options: options) { result in
guard let completionHandler = completionHandler else { return }
options.callbackQueue.execute {
result.match(
onSuccess: { cacheResult in
let value: Result
if let image = cacheResult.image {
value = result.map {
RetrieveImageResult(image: image, cacheType: $0.cacheType, source: source)
}
} else {
value = .failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key)))
}
completionHandler(value)
},
onFailure: { _ in
completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))))
}
)
}
}
return true
}
let targetImageCached = targetCache.imageCachedType(
forKey: key, processorIdentifier: options.processor.identifier)
我们首先需要判断唯一标示符是否存在于缓存数据中,先从内存缓存中搜寻然后再从磁盘缓存中搜寻。
在上面已经提过,内存缓存中的数据都被保存在MemoryStorage
实例对象的名为storage
的NSCache
实例对象中,通过objectForKey
函数获取判断Key
是否有对应的Object
。
而磁盘缓存则是通过唯一标识符拼装成沙盒路径,通过FileManager
获取本地数据来判断是否有值。
当我们尝试获取磁盘缓存时,磁盘缓存管理对象会尝试生成一个FileMeta
对象,该对象会根据我们给定的拼装路径地址,根据配置字典请求该路径的目标文件的相关信息,这些信息包括文件的创建日期、修改日期、文件大小等等,通过创建日期来判断该文件是否过期,是否返回数据给外部。而当我们成功获取到磁盘数据时,FileMeta
对象也会延长数据文件在磁盘中的到期时间。
到这里有个我有个疑点,我们通过targetImageCached
枚举对象了解到数据是否存在于内存或者磁盘中,但是枚举对象的数据只用来生成validCache
布尔值,没有进入后面的逻辑判断,在进入retrieveImage
函数时,没有使用targetImageCached
直接选择是获取内存数据还是磁盘数据,反而依然是先进入内存缓存获取数据,获取不到再进入磁盘缓存代码,不得不说这个代码有些让人难以理解。
retrieveImage
函数内部与我们之前targetImageCached
的初始化代码有很类似的结构,毕竟我们在初始化targetImageCached
时已经询问过内存管理对象或是磁盘管理对象是否存在唯一标示符对应的数据了,它实际上就是尝试从内存管理对象内部取出相应的数据,判断是否取出成功,磁盘也是相同的道理,只是我们存储在内存管理对象中的是Image
类型的数据,而磁盘中存储的是Data
类型的数据,在从磁盘中获取后还要经过与插入数据到本地相反的转化过程,最终转化为Image
数据传递给外部。