Kingfisher源码阅读

1,530 阅读9分钟

Kingfisher是一个在iOS开发中被广泛使用的用于图片下载与缓存的Swift框架,只需要使用极少的代码,就能实现图片的下载与缓存,同时也提供了许多的配置,满足不同的需求。

整体架构

以下是Kingfisher的一个大概的架构,只涵盖了主要组件,还有一些组件如ImagePrefetcher、RetryStrategy等没有画进去。

Kingfisher-Architecture.drawio.png

主要由以下几个部分组成

  • View Extension:各种View的扩展。其中还会设置image、placeholder、动画等。
  • KingfisherManager:负责获取图片的逻辑,使用ImageDownloader和ImageCache分别进行下载和缓存,也会在下载失败的时候进行重试。
  • ImageDownloader:负责下载相关功能。其中的SessionDelegate不仅仅是delegate,它更像是一个manager。
  • ImageDataProcessor:下载之后,负责将图片从Data转为图片,并可配合不同的imageProcessor来处理图片。
  • ImageCache:负责缓存,使用MemoryStorage和DiskStorage分别在内存和磁盘进行缓存。

整体流程

以下是Kingfisher的最简单的使用,仅仅使用这两行代码,就能实现自动下载url的图片并缓存。

KF.url(url)
  .set(to: imageView) 

我们可以先从下面这个简单的图来了解一下整个流程。

Kingfisher-Download-and-cache-flow.drawio.png

  1. 调用上述代码后,最终会调用imageView的setImage扩展方法
  2. 方法内部调用KingfisherManager去获取图片
  3. KingfisherManager先调用ImageCache获取图片
  4. ImageCache内部优先从内存缓存中取图片,没有再从磁盘缓存中获取
  5. 如果缓存中没有,KingfisherManager才会让ImageDownloader开启下载任务
  6. ImageDownloader下载完之后会初始化一个ImageDataProcessor将data转换成图片并通过回调把图片传给KingfisherManager
  7. KingfisherManager获取到图片后会使用ImageCache先进行缓存,分别缓存到内存和磁盘
  8. 缓存完成后回调KingfisherManagerKingfisherManager再把图片回调给imageView的扩展的setImage方法,此方法里会把图片设置好

异步下载与缓存的实现原理

接下来我们会看一看上述提到的代码实现

Source

Source是一个重要类型,我们需要先了解一下它是什么,才会有利于阅读之后的代码。

我们先看下它的定义

public enum Source {

    // 省略...

    /// The target image should be got from network remotely. The associated `Resource`
    /// value defines detail information like image URL and cache key.
    case network(Resource)
    
    /// The target image should be provided in a data format. Normally, it can be an image
    /// from local storage or in any other encoding format (like Base64).
    case provider(ImageDataProvider)

    // 省略...
}

Source表示来源,在Kingfisher中,图片的来源分为这两个

  • network:从网络中下载
  • provider:本地提供

我们之所以能通过简单的一个url来下载图片,是因为Kingfisher提供了一个方法,将url转这个Source

extension URL: Resource { //... }

extension Resource {

    /// Converts `self` to a valid `Source` based on its `downloadURL` scheme. A `.provider` with
    /// `LocalFileImageDataProvider` associated will be returned if the URL points to a local file. Otherwise,
    /// `.network` is returned.
    public func convertToSource(overrideCacheKey: String? = nil) -> Source {
        let key = overrideCacheKey ?? cacheKey
        return downloadURL.isFileURL ?
            .provider(LocalFileImageDataProvider(fileURL: downloadURL, cacheKey: key)) :
            .network(ImageResource(downloadURL: downloadURL, cacheKey: key))
    }
}

从这里也能看到,如果使用文件url,会转成provider,否则就会转成network

设置图片

设置图片的方法位于ImageView的扩展方法,代码如下:

func setImage(
    with source: Source?,
    placeholder: Placeholder? = nil,
    parsedOptions: KingfisherParsedOptionsInfo,
    progressBlock: DownloadProgressBlock? = nil,
    completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
{
    // ...

    let task = KingfisherManager.shared.retrieveImage(
        with: source,
        options: options,
        downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
        completionHandler: { result in
            CallbackQueue.mainCurrentOrAsync.execute {
                // ...

                switch result {
                case .success(let value):
                    guard self.needsTransition(options: options, cacheType: value.cacheType) else {
                        mutatingSelf.placeholder = nil
                        // 设置图片
                        self.base.image = value.image
                        completionHandler?(result)
                        return
                    }
                    
                    // 动画后设置图片
                    self.makeTransition(image: value.image, transition: options.transition) {
                        completionHandler?(result)
                    }

                case .failure:
                    if let image = options.onFailureImage {
                        // 设置图片
                        self.base.image = image
                    }
                    completionHandler?(result)
                }
            }
        }
    )
    mutatingSelf.imageTask = task
    return task
}

从代码可知,它有两个时机会设置图片

  • 获取图片成功后,使用动画或非动画的形式设置图片
  • 获取图片失败后,可设置自定义的失败图片

KingfisherManager获取图片

从上面的代码可以看出,获取图片的功能是依靠KingfisherManager来实现的。

KingfisherManager是一个重要的类,它有以下几个职责

  • 决定是从缓存中取图片还是下载图片
  • 下载之后缓存图片
  • 在下载失败的时候根据重试策略重新开始下载
  • 低数据模式的处理

以下是它获取图片的代码

private func retrieveImage(
    with source: Source,
    context: RetrievingContext,
    completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask?
{
    let options = context.options
    // 强制下载
    if options.forceRefresh {
        return loadAndCacheImage(
            source: source,
            context: context,
            completionHandler: completionHandler)?.value
        
    } else {
        // 优先从缓存中读取
        let loadedFromCache = retrieveImageFromCache(
            source: source,
            context: context,
            completionHandler: completionHandler)
        
        if loadedFromCache {
            return nil
        }
        
        // 只从缓存中取,否则报错
        if options.onlyFromCache {
            let error = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey))
            completionHandler?(.failure(error))
            return nil
        }
        
        // 下载并缓存
        return loadAndCacheImage(
            source: source,
            context: context,
            completionHandler: completionHandler)?.value
    }
}

@discardableResult
func loadAndCacheImage(
    source: Source,
    context: RetrievingContext,
    completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask.WrappedTask?
{
    let options = context.options
    func _cacheImage(_ result: Result<ImageLoadingResult, KingfisherError>) {
        cacheImage(
            source: source,
            options: options,
            context: context,
            result: result,
            completionHandler: completionHandler
        )
    }

    switch source {
    case .network(let resource):
        let downloader = options.downloader ?? self.downloader
        // 下载完成后缓存图片
        let task = downloader.downloadImage(
            with: resource.downloadURL, options: options, completionHandler: _cacheImage
        )

        if let task = task {
            return .download(task)
        } else {
            return nil
        }

    case .provider(let provider):
        // 从provider获取图片并缓存
        provideImage(provider: provider, options: options, completionHandler: _cacheImage)
        return .dataProviding
    }
}

从代码可知

  • 获取图片的时候总是优先从缓存中获取,没有再下载并缓存。
  • 会根据Source的不同选择不同的获取方式。

下载图片

当需要下载图片的时候,KingfisherManager会让ImageDownloader开始下载任务,以下是ImageDownloader开始下载任务的代码。

private func startDownloadTask(
    context: DownloadingContext,
    callback: SessionDataTask.TaskCallback
) -> DownloadTask
{
    // 添加下载任务
    let downloadTask = addDownloadTask(context: context, callback: callback)

    let sessionTask = downloadTask.sessionTask
    guard !sessionTask.started else {
        return downloadTask
    }

    sessionTask.onTaskDone.delegate(on: self) { (self, done) in
        // ...

        switch result {
        case .success(let (data, response)):

            // 创建ImageDataProcessor将data转为图片
            let processor = ImageDataProcessor(
                data: data, callbacks: callbacks, processingQueue: context.options.processingQueue
            )

            // 转换成功后会调用
            processor.onImageProcessed.delegate(on: self) { (self, done) in
                // ...

                // 将image包装好
                let imageResult = result.map { ImageLoadingResult(image: $0, url: context.url, originalData: data) }
                let queue = callback.options.callbackQueue

                // 回调包装好的结果
                queue.execute { callback.onCompleted?.call(imageResult) }
            }
            // 开始转换
            processor.process()

        case .failure(let error):
            callbacks.forEach { callback in
                let queue = callback.options.callbackQueue
                queue.execute { callback.onCompleted?.call(.failure(error)) }
            }
        }
    }

    // ..

    // 立即开始
    sessionTask.resume()
    return downloadTask
}

从代码可知,ImageDownloader会先创建一个下载任务并立即开始,当下载完成后会将data转为图片,并包装成一个ImageLoadingResult类并回调出去,届时KingfisherManager就会收到结果,并可取出图片。

添加下载任务

在介绍如何添加下载任务之前,我们先了解一下下载任务的种类,它分为三种:

  • DownloadTask
  • SessionDataTask
  • URLSessionDataTask

URLSessionDataTask是iOS的URLSession的下载任务。

他们的关系如下

Kingfisher-Download-task-relationship.png

知道了下载任务的种类,就能更好得理解添加下载任务的实现了。

ImageDownloader添加下载任务是添加到SessionDelegate里的,以下是SessionDelegate添加下载任务的代码。

private var session: URLSession

// ...

private func addDownloadTask(
    context: DownloadingContext,
    callback: SessionDataTask.TaskCallback
) -> DownloadTask
{
    let downloadTask: DownloadTask
    if let existingTask = sessionDelegate.task(for: context.url) {
        // 对已存在的SessionDataTask,只增加一个callback
        downloadTask = sessionDelegate.append(existingTask, url: context.url, callback: callback)
    } else {
        // 创建新的SessionDataTask
        let sessionDataTask = session.dataTask(with: context.request)
        sessionDataTask.priority = context.options.downloadPriority
        downloadTask = sessionDelegate.add(sessionDataTask, url: context.url, callback: callback)
    }
    return downloadTask
}

func append(
    _ task: SessionDataTask,
    url: URL,
    callback: SessionDataTask.TaskCallback) -> DownloadTask
{
    // 对已存在的SessionDataTask,只增加一个callback
    let token = task.addCallback(callback)
    return DownloadTask(sessionTask: task, cancelToken: token)
}

func add(
    _ dataTask: URLSessionDataTask,
    url: URL,
    callback: SessionDataTask.TaskCallback) -> DownloadTask
{
    lock.lock()
    defer { lock.unlock() }

    // 创建新的SessionDataTask
    let task = SessionDataTask(task: dataTask)
    task.onCallbackCancelled.delegate(on: self) { [weak task] (self, value) in
        // ... 取消任务实现
    }
    let token = task.addCallback(callback)
    tasks[url] = task
    return DownloadTask(sessionTask: task, cancelToken: token)
}

我们可以从中发现

  • 创建SessionDataTask需要一个URLSessionDataTask
  • 如果已存在对某个url的下载任务SessionDataTask,则只会添加一个callback到其中,并创建一个新的DownloadTask
  • 如果不存在SessionDataTask,则会创建SessionDataTask还有DownloadTask

添加任务的流程大概是这样

Kingfisher-add-download-task.png

下载完成

从上面的代码我们知道,下载是依靠URLSession实现的,

再来看看SessionDelegate的部分代码

open class SessionDelegate: NSObject {
    //...
    // 这是存储下载任务的地方
    private var tasks: [URL: SessionDataTask] = [:]
}

extension SessionDelegate: URLSessionDataDelegate { 
    open func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
        guard let task = self.task(for: dataTask) else {
            return
        }
        
        // 其中会拼接task的data
        // mutableData.append(data)
        task.didReceiveData(data)
        
        //... 副作用
    }

    open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
            guard let sessionTask = self.task(for: task) else { return }
            
            // ...
            onCompleted(task: task, result: result)
        }
}

主要就是URLSessionDataDelegate的在相关代理方法中,拼接SessionDataTask的data并在complete的代理调用后完成后将结果回调出去。

值得注意的是,SessionDelegatetask属性存放着下载任务,是一个字典,所以一个URL只会对应一个下载任务SessionDataTask

缓存实现原理

图片的缓存是要在下载完成之后由KingfisherManager调用ImageCache是做缓存的。

以下是KingfisherManager的下载并缓存的方法的实现:

@discardableResult
func loadAndCacheImage(
    source: Source,
    context: RetrievingContext,
    completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask.WrappedTask?
{
    let options = context.options
    func _cacheImage(_ result: Result<ImageLoadingResult, KingfisherError>) {
        cacheImage(
            source: source,
            options: options,
            context: context,
            result: result,
            completionHandler: completionHandler
        )
    }

    switch source {
    case .network(let resource):
        let downloader = options.downloader ?? self.downloader
        // completionHandler直接调用_cacheImage
        let task = downloader.
        downloadImage(
            with: resource.downloadURL, options: options, completionHandler: _cacheImage
        )
        // ...
    }
}

可以看到,在下载完成后,就直接调用_cacheImage做缓存了。

cacheImage方法判断下载成功与否来做对应的操作,我们暂时只关注成功的操作。在成功的情况下,会调用ImageCache来进行缓存。

// 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
    coordinator.apply(.cachingImage) {
        completionHandler?(.success(result))
    }
}

以下是store方法的实现

open func store(_ image: KFCrossPlatformImage,
                original: Data? = nil,
                forKey key: String,
                options: KingfisherParsedOptionsInfo,
                toDisk: Bool = true,
                completionHandler: ((CacheStoreResult) -> Void)? = nil)
{
    // ...
    
    // 计算缓存key
    let computedKey = key.computedKey(with: identifier)
    // 缓存到内存
    memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration)
    
    // ...
    
    ioQueue.async {
        let serializer = options.cacheSerializer
        // 图片转data
        if let data = serializer.data(with: image, original: original) {
            // 保存到磁盘
            self.syncStoreToDisk(
                data,
                forKey: key,
                processorIdentifier: identifier,
                callbackQueue: callbackQueue,
                expiration: options.diskCacheExpiration,
                writeOptions: options.diskStoreWriteOptions,
                completionHandler: completionHandler)
        } else {
            // ...
        }
    }
}

以上能看出缓存的步骤大概是这样:

  1. 计算缓存key
  2. 缓存到内存
  3. 缓存到磁盘

上述代码使用了两个成员,memoryStoragesyncStoreToDisk中会调用的diskStorage

public let memoryStorage: MemoryStorage.Backend<KFCrossPlatformImage>
public let diskStorage: DiskStorage.Backend<Data>

这里的Backend可以暂且理解为各自缓存的实现。

内存缓存原理

内存缓存的代码如下:

func storeNoThrow(
        value: T,
        forKey key: String,
        expiration: StorageExpiration? = nil)
    {
        lock.lock()
        defer { lock.unlock() }

        // 过期时间
        let expiration = expiration ?? config.expiration
        guard !expiration.isExpired else { return }
        
        let object: StorageObject<T>
        // 这里决定在app进入后台的时候,是否要清除缓存
        if config.keepWhenEnteringBackground {
            object = BackgroundKeepingStorageObject(value, key: key, expiration: expiration)
        } else {
            object = StorageObject(value, key: key, expiration: expiration)
        }
        // 设置缓存
        storage.setObject(object, forKey: key as NSString, cost: value.cacheCost)
        keys.insert(key)
    }

设置缓存是调用storage的方法,storage的定义如下

let storage = NSCache<NSString, StorageObject<T>>()

所以内存缓存的原理已经很明显了,就是利用NSCache来实现。

磁盘缓存原理

磁盘缓存的代码如下

public func store(
    value: T,
    forKey key: String,
    expiration: StorageExpiration? = nil,
    writeOptions: Data.WritingOptions = []) throws
{
    // ...

    // 过期时间
    let expiration = expiration ?? config.expiration
    // The expiration indicates that already expired, no need to store.
    guard !expiration.isExpired else { return }
    
    let data: Data
    do {
        data = try value.toData()
    } catch {
        throw KingfisherError.cacheError(reason: .cannotConvertToData(object: value, error: error))
    }

    let fileURL = cacheFileURL(forKey: key)
    do {
        try data.write(to: fileURL, options: writeOptions)
    } catch {
        throw KingfisherError.cacheError(
            reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error)
        )
    }

    // ... 过期时间设置
}

磁盘的实现原理也很明显了,就是将图片转为data并使用write写入文件。

文件的路径是Cache文件夹加上一些标识和缓存key。

缓存key的计算

这里说的缓存key是针对默认的情况,缓存key是可以自定义的。

对于内存缓存来说,key是url

extension URL: Resource {
    public var cacheKey: String { return isFileURL ? localFileCacheKey : absoluteString }
    public var downloadURL: URL { return self }
}

对于磁盘缓存来说,key是url的md5值。使用url作为key在内存找不到缓存的时候,就会把url转为md5去磁盘中找。

func cacheFileName(forKey key: String) -> String {
    // ...
    let hashedKey = key.kf.md5
    // ...
    return hashedKey
    /// ...
}

缓存控制

不管是内存缓存还是磁盘缓存,它们都有一定的过期时间,使用它们的时候会延长过期时间,当某些事件发生的时候会清除掉过期的缓存,当然也可以选择自己手动清除。

过期时间与延长

在创建缓存的时候,会给缓存定义一个过期时间。对于内存,默认是300秒,而磁盘则是7天。每次使用它们的时候,会延长过期时间。

内存:

// 过期时间
public var expiration: StorageExpiration = .seconds(300)

// 使用延长
public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? {
    guard let object = storage.object(forKey: key as NSString) else {
        return nil
    }
    if object.expired {
        return nil
    }
    // 延长过期时间
    object.extendExpiration(extendingExpiration)
    return object.value
}

对于磁盘,它的过期时间是写在file的attibutes上的

public func store(
    value: T,
    forKey key: String,
    expiration: StorageExpiration? = nil,
    writeOptions: Data.WritingOptions = []) throws
{
    // ...

    let now = Date()
    let attributes: [FileAttributeKey : Any] = [
        // The last access date.
        .creationDate: now.fileAttributeDate,
        // The estimated expiration date.
        .modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate
    ]
    do {
        try config.fileManager.setAttributes(attributes, ofItemAtPath: fileURL.path)
    } catch {
        // ...
    }

    // ...
}
// 过期时间
public var expiration: StorageExpiration = .days(7)

// 使用延长
func value(
    forKey key: String,
    referenceDate: Date,
    actuallyLoad: Bool,
    extendingExpiration: ExpirationExtending) throws -> T?
{
    // ...

    do {
        let data = try Data(contentsOf: fileURL)
        let obj = try T.fromData(data)
        metaChangingQueue.async {
            meta.extendExpiration(with: fileManager, extendingExpiration: extendingExpiration)
        }
        return obj
    } catch {
        throw KingfisherError.cacheError(reason: .cannotLoadDataFromDisk(url: fileURL, error: error))
    }
}

缓存清除

对于内存缓存

  • 借助NSCache本身的totalCostLimitcountLimit。从createMemoryStorage可知,totalCostLimit默认是1/4的物理内存或者Int最大值,哪个小用哪个。countLimit:默认是Int的最大值。
  • 内存缓存初始化后,会设置一个定时器,定时清除缓存,默认是120秒。
  • 收到内存警告清除全部,通过监听didReceiveMemoryWarningNotification通知。
  • 进入后台会清除,但设置BackgroundKeepingStorageObject实现NSDiscardableContent来防止这一行为。

对于磁盘缓存,未过期的缓存不会被自动清除,清除的都是过期的

  • 程序即将推出清除过期的,监听didReceiveMemoryWarningNotification
  • 即将进入后台是,监听didEnterBackgroundNotification

总结

以上说明了Kingfisher的主要是关于下载与缓存的实现原理,简单介绍了其中的各个组件的职责和实现,可能还会一些遗漏或错误的地方,还希望大家能多多包涵,不吝赐教。

本文首发于我的博客,十分感谢大家的阅读。