iOS-Kingfisher个人浅析(缓存篇)

写在前面,当时学习Kingfisher的源码时因为工作的原因中途放下了,只写到图像下载,即ImageDownloader模块,图像下载的代码因为各种函数作为形参,闭包声明实现等等看的我是云里雾里的,所以把那部分的代码写成了一篇,本以为ImageCache相关模块也是同样的晦涩代码,所以打算另外边看边写一篇,结果发现ImageCache的代码相较于ImageDownloader清晰简单很多,写到一半的时候才发现很多东西都不用写出来,写出来反而感觉自己像个智障一样这种小事都要写出来,因此缓存篇就只能草草了事ORZ。

衔接上文,我们回到位于KingfisherManager中的cacheImage函数。

 func cacheImage(_ result: Result<ImageLoadingResult, KingfisherError>)
        {
            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的功能是作为所有的内存缓存数据的容器,memoryStorageMemoryStorage.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<RetrieveImageResult, KingfisherError>) -> 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<RetrieveImageResult, KingfisherError>
                            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实例对象的名为storageNSCache实例对象中,通过objectForKey函数获取判断Key是否有对应的Object
而磁盘缓存则是通过唯一标识符拼装成沙盒路径,通过FileManager获取本地数据来判断是否有值。

当我们尝试获取磁盘缓存时,磁盘缓存管理对象会尝试生成一个FileMeta对象,该对象会根据我们给定的拼装路径地址,根据配置字典请求该路径的目标文件的相关信息,这些信息包括文件的创建日期、修改日期、文件大小等等,通过创建日期来判断该文件是否过期,是否返回数据给外部。而当我们成功获取到磁盘数据时,FileMeta对象也会延长数据文件在磁盘中的到期时间。

到这里有个我有个疑点,我们通过targetImageCached枚举对象了解到数据是否存在于内存或者磁盘中,但是枚举对象的数据只用来生成validCache布尔值,没有进入后面的逻辑判断,在进入retrieveImage函数时,没有使用targetImageCached直接选择是获取内存数据还是磁盘数据,反而依然是先进入内存缓存获取数据,获取不到再进入磁盘缓存代码,不得不说这个代码有些让人难以理解。

retrieveImage函数内部与我们之前targetImageCached的初始化代码有很类似的结构,毕竟我们在初始化targetImageCached时已经询问过内存管理对象或是磁盘管理对象是否存在唯一标示符对应的数据了,它实际上就是尝试从内存管理对象内部取出相应的数据,判断是否取出成功,磁盘也是相同的道理,只是我们存储在内存管理对象中的是Image类型的数据,而磁盘中存储的是Data类型的数据,在从磁盘中获取后还要经过与插入数据到本地相反的转化过程,最终转化为Image数据传递给外部。

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,101评论 1 32
  • ORA-00001: 违反唯一约束条件 (.) 错误说明:当在唯一索引所对应的列上键入重复值时,会触发此异常。 O...
    我想起个好名字阅读 5,317评论 0 9
  • 1、 最近的我很开心,因为有一本以我为原型的小说即将出版了。而书的作者,正是我的男朋友,吴秦。他是一个小有名气的作...
    尖尖有毒阅读 2,635评论 6 5
  • 代码 说明 1 成功! -1 网络链接失败 -2 请填写程序密钥 -3...
    阴阳_2557阅读 3,602评论 0 0
  • 那一夜 我亲吻魔鬼的唇 立下魔鬼契约 从此 我坠入无底的深渊 那一夜 我偷饮上帝的佳酿 被视为上帝叛徒 从此 我被...
    老猫文学馆阅读 309评论 0 3