Kingfisher 源码解析(二)

October 12, 2018   

Kingfisher 通过给 ImageView 和 Button 分别扩展了两个分类方法来设置图片

这里没有使用 UIImageView 和 UIButton 是因为该库也支持 MacOS ,所以在 MacOS 上是 NSImageView 和 NSButton 控件。详情见 Kingfisher.swift 中

// 判断系统软件版本是 MacOS 还是 其他平台来分别导入对应 framework
#if os(macOS)
    import AppKit
    public typealias Image = NSImage
    public typealias View = NSView
    public typealias Color = NSColor
    public typealias ImageView = NSImageView
    public typealias Button = NSButton
#else
    import UIKit
    public typealias Image = UIImage
    public typealias Color = UIColor
    #if !os(watchOS)
    public typealias ImageView = UIImageView
    public typealias View = UIView
    public typealias Button = UIButton
    #else
    import WatchKit
    #endif
#endif

设置图片的核心方法

@discardableResult
public func setImage(with resource: Resource?,
                     placeholder: Placeholder? = nil,
                     options: KingfisherOptionsInfo? = nil,
                     progressBlock: DownloadProgressBlock? = nil,
                     completionHandler: CompletionHandler? = nil) -> RetrieveImageTask
  • Resource 图片的 URL
  • placeholder 占位图
  • KingfisherOptionsInfo 一个数组,保存图片下载后的一系列处理操作
  • progressBlock 进度回调
  • completionHandler 完成回调
  • 返回一个下载处理 task

方法的核心逻辑

  1. 首先判断 URL 是否存在,否则直接返回;
  2. 将用户传的 KingfisherOptionsInfo 和 默认的 KingfisherEmptyOptionsInfo 合并,有过有展位图保存展位图;
  3. 如果有加载 loading 则开始展示加载 loading 动画;
  4. 保存下载链接;
  5. 使用 Kingfisher 的 retrieveImage(with:options:progressBlock:completionHandler) 方法去获取图片;
  6. retrieveImage 方法返回一个 图片处理 task 保存并返回这个task

Kingfisher 利用 runtime 给 ImageView 分别扩展了 webURL 和 imageTask 属性,用来分别保存当前控件上的图片链接和下载任务,方便以后取回复用取回图片和操作这个下载任务。

UIButton 和 NSButton 因为存在背景图片所以增加了一个方法

@discardableResult
public func setBackgroundImage(with resource: Resource?,
                               for state: UIControl.State,
                               placeholder: UIImage? = nil,
                               options: KingfisherOptionsInfo? = nil,
                               progressBlock: DownloadProgressBlock? = nil,
                               completionHandler: CompletionHandler? = nil) -> RetrieveImageTask

方法的实现过程思路和 UIImageView 一样。同样也给 Button 扩展了 背景图 url 和 下载任务 task

扩展里给 watchOS 增加了 WKInterfaceImage 方法,该控件与 iOS 的 UIImageView 一样,所以里面的实现基本和 ImageView 的扩展实现差不多。

那我们就顺着代码的实现看看这个 retrieveImage 方法到底做了什么

图片的取回操作,在最核心的 KingfisherManager.swift 文件中,一共有 3 个 核心方法

  • retrieveImage(with resource:options:progressBlock:completionHandler:) -> RetrieveImageTask
  • downloadAndCacheImage(with url:forKey key:retrieveImageTask:progressBlock: completionHandler:options:) -> RetrieveImageDownloadTask?
  • tryToRetrieveImageFromCache(forKey key: with url:retrieveImageTask:progressBlock:completionHandler:options:)

其中第一个方法会分别调用下面两个方法,判断 options 中 是否有 forceRefresh 值,如果有则去调用第二方法去下载图片,否则调用第三个方法尝试从缓存中取回图片

下载图片时,其实是调用 ImageDownloader.swift 中的 downloadImage(with:retrieveImageTask:options:progressBlock:completionHandler) 方法

let targetCache = options.targetCache ?? self.cache
if let error = error, error.code == KingfisherError.notModified.rawValue {
  // 没有修改。尝试从缓存中找
  targetCache.retrieveImage(forKey: key, options: options, completionHandler: { 
    (cacheImage, cacheType) -> Void in
  	completionHandler?(cacheImage, nil, cacheType, url)                                                                          
  })
  return
}

if let image = image, let originalData = originalData {
  // 目标缓存路径缓存图片。
  // 如果没有特别要求,则会在磁盘也缓存一份
  targetCache.store(image,
                    original: originalData,
                    forKey: key,
                    processorIdentifier:options.processor.identifier,
                    cacheSerializer: options.cacheSerializer,
                    toDisk: !options.cacheMemoryOnly,
                    completionHandler: {
                      guard options.waitForCache else { return }

                      let cacheType = targetCache.imageCachedType(forKey: key, processorIdentifier: options.processor.identifier)
                      completionHandler?(image, nil, cacheType, url)
                    })

  if options.cacheOriginalImage && options.processor != DefaultImageProcessor.default {
    let originalCache = options.originalCache ?? targetCache
    let defaultProcessor = DefaultImageProcessor.default
    processQueue.async {
      if let originalImage = defaultProcessor.process(item: .data(originalData), options: options) {
        // 原始缓存路径也会缓存一份
        originalCache.store(originalImage,
                            original: originalData,
                            forKey: key,
                            processorIdentifier: defaultProcessor.identifier,
                            cacheSerializer: options.cacheSerializer,
                            toDisk: !options.cacheMemoryOnly,
                            completionHandler: nil)
      }
    }
  }
}

if options.waitForCache == false {
  completionHandler?(image, error, .none, url)
}

取回图片的逻辑也很简单

// If found, we could finish now.
if image != nil {
  diskTaskCompletionHandler(image, nil, cacheType, url)
  return
}

// If not found, and we are using a default processor, download it!
let processor = options.processor
guard processor != DefaultImageProcessor.default else {
  handleNoCache()
  return
}
            
// If processor is not the default one, we have a chance to check whether
// the original image is already in cache.
let originalCache = options.originalCache ?? targetCache
let optionsWithoutProcessor = options.removeAllMatchesIgnoringAssociatedValue(.processor(processor))
originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { image, cacheType in
	// If we found the original image, there is no need to download it again.
 	// We could just apply processor to it now.
	guard let image = image else {
		handleNoCache()
		return
	}

	processQueue.async {
		guard let processedImage = processor.process(item: .image(image), options: options) else {
        options.callbackDispatchQueue.safeAsync {
            diskTaskCompletionHandler(nil, nil, .none, url)
        }
          	return
        }
      	targetCache.store(processedImage,
                          original: nil,
                          forKey: key,
                          processorIdentifier:options.processor.identifier,
                          cacheSerializer: options.cacheSerializer,
                          toDisk: !options.cacheMemoryOnly,
                          completionHandler: {
			guard options.waitForCache else { return }

			let cacheType = targetCache.imageCachedType(forKey: key, processorIdentifier: options.processor.identifier)
                            options.callbackDispatchQueue.safeAsync {
             	diskTaskCompletionHandler(processedImage, nil, cacheType, url)
                                        }
                    })

        if options.waitForCache == false {
          options.callbackDispatchQueue.safeAsync {
            diskTaskCompletionHandler(processedImage, nil, .none, url)
          }
        }
    }
}
  • 首先尝试存目标缓存中取,如果取到了则直接返回;
  • 如果 options 的 processor 不是默认的,则直接重新下载;
  • 尝试从原始缓存中找,如果没找到则重新下载;
  • 加工图片,并在目标缓存中在存储一份,回调成功闭包

comments powered by Disqus