Kingfisher 源码解析(四)

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 编程技巧,做一个浅显的分析。


comments powered by Disqus