Kingfisher源码解析之ImageCache

Kingfisher源码解析系列,因为水平有限,哪里有错,肯请不吝赐教swift

Kingfisher中ImageCache里提供内存缓存和磁盘缓存,分别是MemoryStorage.Backend<KFCrossPlatformImage>DiskStorage.Backend<Data>来实现的,注:内存缓存和磁盘缓存都是经过class Backend,不过这2个类,是彻底不一样的类,使用枚举来充当命名空间来区分的,分别定义在MemoryStorage.swiftDiskStorage.swift缓存

内存缓存

内存缓存一共有三个类构成,Backend提供缓存的功能,Config提供缓存的配置项,StorageObject<T>缓存的封装类型安全

Config的主要内容
public struct Config {
    //内存缓存的最大容量,ImageCache.default中提供的默认值是设备物理内存的四分之一
    public var totalCostLimit: Int
    //内存缓存的最大长度
    public var countLimit: Int = .max
    //内存缓存的的过时时长
    public var expiration: StorageExpiration = .seconds(300)
    //清除过时缓存的时间间隔
    public let cleanInterval: TimeInterval
}
复制代码
StorageObject<T>的主要内容
class StorageObject<T> {
      //缓存的真正的值
      let value: T
      //存活时间,也就是多久以后过时
      let expiration: StorageExpiration
      //缓存e的key
      let key: String
      //过时时间,默认值是当前时间加上expiration
      private(set) var estimatedExpiration: Date
      // 更新过时时间
      func extendExpiration(_ extendingExpiration: ExpirationExtending = .cacheTime) {
          switch extendingExpiration {
          case .none://不更新过时时间
              return
          case .cacheTime://把过时时间设置为当前时间加上存活时间
              self.estimatedExpiration = expiration.estimatedExpirationSinceNow
          case .expirationTime(let expirationTime)://把过时时间设置为指定时间
              self.estimatedExpiration = expirationTime.estimatedExpirationSinceNow
          }
      }
      // 是否已通过期
      var expired: Bool {
          //estimatedExpiration.isPast 是对Date的一个扩展方法,判断estimatedExpiration是否小于当前时间
          return estimatedExpiration.isPast
      }
}
复制代码
Backend的主要内容
public class Backend<T: CacheCostCalculable> {
    //使用NSCache进行缓存
    let storage = NSCache<NSString, StorageObject<T>>()
    //存放全部缓存的key,在删除过时缓存是有用
    var keys = Set<String>()
    //定时器,用于定时清除过时数据
    private var cleanTimer: Timer? = nil
    //配置项
    public var config: Config
    ...下面还有一些缓存数据,读取数据,删除缓存,是否已缓存,删除过时数据等方法
}
复制代码

由上面咱们能够看出,Kingfisher中内存缓存是用NSCache实现的,NSCache是一个相似于Dictionary的类,拥有类似的API,不过区别于Dictionary的是,NSCache是线程安全的,而且提供了设置最大缓存个数和最大缓存大小的配置,Backend就是经过设置NSCache的countLimittotalCostLimit来实现最大缓存个数和最大缓存大小。bash

经过下面的代码,看下Backend是如何缓存数据,读取数据,判断是否已缓存,删除缓存,删除过时数据的?代码中有详细的注释,注:下面的代码删除了一些非核心代码,好比异常,加锁保证线程安全等app

缓存数据
func store(value: T,forKey key: String,expiration: StorageExpiration? = nil) {
    //获取存活时间,若缓存时没设置,则从配置中获取
    let expiration = expiration ?? config.expiration
    //判断是否过时,若已通过期直接返回
    guard !expiration.isExpired else { return }
    //把要缓存的值封装成StorageObject类型
    let object = StorageObject(value, key: key, expiration: expiration)
    //把结果缓存起来
    storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
    //把key保存起来
    keys.insert(key)
}
复制代码
读取数据,判断数据是否已缓存
// 读取数据
func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? {
    //从NSCache中获取数据,如获取不到直接返回nil
    guard let object = storage.object(forKey: key as NSString) else { return nil }
    //判断是否过时,若过时直接返回nil
    if object.expired { return nil }
    //去更新过时时间
    object.extendExpiration(extendingExpiration)
    return object.value
}
// 判断是否缓存,其本质就是去读取数据,只是不更新缓存时间,若取到,则已缓存,不然未缓存
func isCached(forKey key: String) -> Bool {
    guard let _ = value(forKey: key, extendingExpiration: .none) else {
        return false
    }
    return true
}
复制代码
删除缓存
func remove(forKey key: String) throws {
    storage.removeObject(forKey: key as NSString)
    keys.remove(key)
}
复制代码
删除过时数据,这里使用Set存储key的缘由是NSCache,并无像Dictionary同样提供获取allKeys或allValues的方法
func removeExpired() {
    for key in keys {
        let nsKey = key as NSString
        //经过key获取数据,若获取失败,则删除从keys中删除key
        guard let object = storage.object(forKey: nsKey) else {
            keys.remove(key)
            continue
        }
        //判断object是否过时,若过时,则从cache中删除数据,从keys中删除key
        if object.estimatedExpiration.isPast {
            storage.removeObject(forKey: nsKey)
            keys.remove(key)
        }
    }
}
复制代码

磁盘缓存

Kingfisher中磁盘缓存是经过文件系统来实现的,也就是说每一个缓存的数据都对应一个文件,其中Kingfisher把文件的建立时间修改成最后一次读取的时间,把文件的修改时间修改成过时时间。异步

磁盘缓存一共有三个类构成,Backend提供缓存的功能,Config提供缓存的配置项,FileMeta存储着文件信息。async

Config的主要内容
public struct Config {
    //磁盘缓存占用磁盘的最大值,为0z时,表示不限制
    public var sizeLimit: UInt
    //存活时间
    public var expiration: StorageExpiration = .days(7)
    //文件的扩展名
    public var pathExtension: String? = nil
    //是否须要把文件名哈希
    public var usesHashedFileName = true
    //操做文件的FileManager
    let fileManager: FileManager
    //文件缓存所在的文件夹,默认在cache文件夹里
    let directory: URL?
}

复制代码
FileMeta的主要内容
struct FileMeta {
    //文件路径
    let url: URL
    //文件最后一次读取时间
    //这个在超过sizeLimit大小时,须要删除文件时,用此属性进行排序,把时间较早的删除掉
    let lastAccessDate: Date?
    //过时时间
    let estimatedExpirationDate: Date?
    //是不是个文件夹
    let isDirectory: Bool
    //文件大小
    let fileSize: Int
}
复制代码
Backend的主要内容
public class Backend<T: DataTransformable> {
    //配置信息
    public var config: Config
    //写入文件所在的文件夹,默认在cache文件夹里
    public let directoryURL: URL
    //修改文件原信息时,所在的队列
    let metaChangingQueue: DispatchQueue
     //该方法会在init着调用,保证directoryURLs文件夹,已经被建立过了
    func prepareDirectory() throws {
        let fileManager = config.fileManager
        let path = directoryURL.path
        guard !fileManager.fileExists(atPath: path) else { return }
        try fileManager.createDirectory(atPath: path,withIntermediateDirectories: true,attributes: nil)
    }
    ...下面还有缓存数据,读取数据,判断是否已缓存,删除缓存,删除过时缓存,删除超过sizeLimit的缓存,统计缓存大小等
}
复制代码

经过下面的代码看Backend是如何缓存数据,读取数据,判断是否已缓存,删除缓存,删除过时缓存,删除超过sizeLimit的缓存,统计缓存大小以及如何经过key生成文件名的?代码中有详细的注释。注:下面的代码删除了一些非核心代码,好比异常,加锁保证线程安全等post

经过key生成文件名

下面那段代码和源码中不太同样,但逻辑是同样的,我改为这样是由于方面我描述测试

//首先判断是否使用key的MD5值当作文件名,如果,则把filename设置成key.MD5
//而后再判断是否设置了扩展名,若设置了,则把扩展名拼接到filename上
func cacheFileName(forKey key: String) -> String {
    var filename = key
    if config.usesHashedFileName {
        filename = key.kf.md5
    } 
    if let ext = config.pathExtension {
        filename =  "\(filename).\(ext)"
    }
    return filename
}
复制代码
缓存数据
func store(
        value: T,
        forKey key: String,
        expiration: StorageExpiration? = nil) throws
    {
        //获取存活时间,若缓存时没设置,则从配置中获取
        let expiration = expiration ?? config.expiration
         //判断是否过时,若已通过期直接返回
        guard !expiration.isExpired else { return }
        // 把value转成data,这里value类型是DataTransformable,须要实现toData等其余方法
        let data: try value.toData()
        //经过cacheKeyc生成一个完整的路径
        //完整的路径等于directoryURL+filename
        let fileURL = cacheFileURL(forKey: key)
        let now = Date()
        //把当前时间设置为文件的建立时间,把过时时间设置为文件的修改时间
        let attributes: [FileAttributeKey : Any] = [
            .creationDate: now.fileAttributeDate,
            .modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate
        ]
        //经过fileManager把data写入文件
        config.fileManager.createFile(atPath: fileURL.path, contents: data, attributes: attributes)
    }
复制代码

上面代码中给文件设置建立时间和修改时间用的是给Date扩展的计算属性fileAttributeDate,fileAttributeDate返回的是Date(timeIntervalSince1970: ceil(timeIntervalSince1970)),也就是说把date的秒值向上取整后再转成date,为何要这么作呢?做者解释说,date在内容中实际是一个double类型的值,而在file的属性中,只接受Int类型的值,会默认舍去小数部分,会致使对测试不友好,因此就改为这样了,我不是很理解为何对测试不友好,难道是会致使提早一会结束过时吗?fetch

加载缓存
func value(
        forKey key: String,/
        referenceDate: Date,
        actuallyLoad: Bool,
        extendingExpiration: ExpirationExtending) throws -> T?
    {
        let fileManager = config.fileManager
        //经过cacheKeyc生成一个完整的路径
        let fileURL = cacheFileURL(forKey: key)
        let filePath = fileURL.path
        //判断是否存在该文件是否存在
        guard fileManager.fileExists(atPath: filePath) else {
            return nil
        }
        //经过fileURL生成一个FileMeta文件描述信息的类
        let resourceKeys: Set<URLResourceKey> = [.contentModificationDateKey, .creationDateKey]
        let meta = try FileMeta(fileURL: fileURL, resourceKeys: resourceKeys)
        //判断文件的过时时间是否大于referenceDate
        if meta.expired(referenceDate: referenceDate) {
            return nil
        }
        //判断是不是真的须要去加载数据,好比判断是否已缓存的时候,就不须要真的去加载,只要知道有就行了
        if !actuallyLoad { return T.empty }
        //读取文件
        let data = try Data(contentsOf: fileURL)
        let obj = try T.fromData(data)
        //更新文件的描述信息,本质也是为了h更新最后一次的读取时间和过时时间
        metaChangingQueue.async { meta.extendExpiration(with: fileManager, extendingExpiration: extendingExpiration) }
    }
复制代码
判断是否已缓存

经过调用value方法,判断value的返回值是否为nil,调用时会把actuallyLoad参数传为false,这样就不会去读取文件

经过key删除缓存,以及删除全部缓存
//经过key生成URL,而后把该文件删除
func remove(forKey key: String) throws {
    let fileURL = cacheFileURL(forKey: key)
    config.fileManager.removeItem(at: url)
}
//直接把文件夹删除
func removeAll(skipCreatingDirectory: Bool) throws {
    try config.fileManager.removeItem(at: directoryURL)
    if !skipCreatingDirectory {
        try prepareDirectory()
    }
}
复制代码
获取缓存大小

获取文件夹下的全部文件,并把每一个文件的大小加起来

删除过时的缓存
//删除在指定时间过时的缓存,若传入当前时间,则是删除如今已通过期的文件
    //返回值:删除的文件路径 
    func removeExpiredValues(referenceDate: Date = Date()) throws -> [URL] {
        let propertyKeys: [URLResourceKey] = [
            .isDirectoryKey,
            .contentModificationDateKey
        ]
        //获取全部的文件URL
        let urls = try allFileURLs(for: propertyKeys)
        let keys = Set(propertyKeys)
        //过滤出过时的文件URL
        let expiredFiles = urls.filter { fileURL in
            let meta = FileMeta(fileURL: fileURL, resourceKeys: keys)
            if meta.isDirectory {
                return false
            }
            return meta.expired(referenceDate: referenceDate)
        }
        //遍历全部的过时的文件UR,依次删除它们
        try expiredFiles.forEach { url in
            try removeFile(at: url)
        }
        return expiredFiles
    }
复制代码
缓存大小超过sizeLimit时删除缓存
func removeSizeExceededValues() throws -> [URL] {
        //若是sizeLimit == 0表明不限制大小,直接返回
        if config.sizeLimit == 0 { return [] } 
        var size = try totalSize()
        //若是当前的缓存大小小于sizeLimit直接返回
        if size < config.sizeLimit { return [] }
        let urls = 获取全部的URLs
        //经过urls生成全部的文件信息,这里包含的信息有是不是文件夹,建立时间,和文件大小
        var pendings: [FileMeta] = urls.compactMap { fileURL in
            guard let meta = try? FileMeta(fileURL: fileURL, resourceKeys: keys) else {
                return nil
            }
            return meta
        }
        //经过建立时间排序,也就是经过最后一次的读取时间
        pendings.sort(by: FileMeta.lastAccessDate)
        var removed: [URL] = []
        let target = config.sizeLimit / 2
        //直到当前缓存大小小于sizeLimit的2分之一,不然按照最后的读取时间一次删除
        while size > target, let meta = pendings.popLast() {
            size -= UInt(meta.fileSize)
            try removeFile(at: meta.url)
            removed.append(meta.url)
        }
        return removed
    }
复制代码

补充

在ImageCache里监听了三个通知,分别是收到内存警告,应用即将被杀死,应用已经进入到后台,在这三个通知里分别作了,清空内存缓存,异步的清除磁盘过时缓存和磁盘大小超过simeLimit清除缓存,在后台清除磁盘过时缓存和磁盘大小超过simeLimit清除缓存

相关文章
相关标签/搜索