源码阅读系列之图片加载框架:Kingfisher (第一篇)

前言

iOS 开发中,图片网络加载功能几乎是每个开发者都会碰到的需求。由于在 iOS 里面,做这种耗时操作的时候是不建议阻塞主线程的。所以,我们需要异步下载,下载完成后再回到主线程更新 UI。一般情况下,我们还需要缓存下载的图片来提高性能节约损耗。

如果这个功能我们自己实现的话还是比较麻烦的。幸运的是,我们可以使用 Kingfisher 来通过一句代码实现图片异步加载的功能。

在阅读源码之前,希望读者能熟练的使用该框架,这样才能更加高效的理解代码。接下来,我将根据该框架 READEME 描述中的 Features 一项的顺序去梳理代码。让我们开始吧!

Asynchronous image downloading and caching

异步加载图片并缓存,这是该框架的主要功能,也是它存在的意义。主要的使用方式如下:

csharp 复制代码
import Kingfisher

let url = URL(string: "https://example.com/image.png")
imageView.kf.setImage(with: url)

首先,我们可以看到对 UIImageViewkf 属性调用 setImage 函数,传入相应的 URL 就可以加载图片资源了。

为什么这里需要设计一个 kf 扩展属性来去调用 setImage 函数,而不是直接为 UIImageView 提供一个扩展函数去调用呢?这样做的好处就是防止函数重名。比如你自己也想为 UIImageView 提供一个名字为 setImage 函数去实现自己的逻辑,用扩展属性这种方式就不会影响。该做法类似 Objc 的 XXX 前缀的作用。

kf 实现主要代码:

swift 复制代码
public struct KingfisherWrapper<Base> {
    public let base: Base
    public init(_ base: Base) {
        self.base = base
    }
}

public protocol KingfisherCompatibleValue {}

extension KingfisherCompatible {
    /// Gets a namespace holder for Kingfisher compatible types.
    public var kf: KingfisherWrapper<Self> {
        get { return KingfisherWrapper(self) }
        set { }
    }
}

extension KFCrossPlatformImage: KingfisherCompatible { }
  • KingfisherWrapper 用来包装一下 UIImageView 示例对象,base 存放的就是我们需要加载图片的 imageView 实例对象。
  • 声明 KingfisherCompatibleValue 协议,并为协议扩展一个可读不可写的 kf 属性。
  • KFCrossPlatformImage(对应 iOS 即 UIImageView) 遵守该协议。

setImage 实现的主要代码:

swift 复制代码
public func setImage(
	with provider: ImageDataProvider?,
	placeholder: Placeholder? = nil,
	options: KingfisherOptionsInfo? = nil,
	completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
	{
	return setImage(
	    with: provider,
	    placeholder: placeholder,
	    options: options,
	    progressBlock: nil,
	    completionHandler: completionHandler
	)
	}


	func setImage(
	with source: Source?,
	placeholder: Placeholder? = nil,
	parsedOptions: KingfisherParsedOptionsInfo,
	progressBlock: DownloadProgressBlock? = nil,
	completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
	{
	var mutatingSelf = self
	guard let source = source else {
	    mutatingSelf.placeholder = placeholder
	    mutatingSelf.taskIdentifier = nil
	    completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
	    return nil
	}

	var options = parsedOptions

	let isEmptyImage = base.image == nil && self.placeholder == nil
	if !options.keepCurrentImageWhileLoading || isEmptyImage {
	    // Always set placeholder while there is no image/placeholder yet.
	    mutatingSelf.placeholder = placeholder
	}

	let maybeIndicator = indicator
	maybeIndicator?.startAnimatingView()

	let issuedIdentifier = Source.Identifier.next()
	mutatingSelf.taskIdentifier = issuedIdentifier

	if base.shouldPreloadAllAnimation() {
	    options.preloadAllAnimationData = true
	}

	if let block = progressBlock {
	    options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
	}

	let task = KingfisherManager.shared.retrieveImage(
	    with: source,
	    options: options,
	    downloadTaskUpdated: { mutatingSelf.imageTask = $0 },
	    progressiveImageSetter: { self.base.image = $0 },
	    referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier },
	    completionHandler: { result in
	        CallbackQueue.mainCurrentOrAsync.execute {
	            maybeIndicator?.stopAnimatingView()
	            guard issuedIdentifier == self.taskIdentifier else { ... }

	            mutatingSelf.imageTask = nil
	            mutatingSelf.taskIdentifier = nil

	            switch result {
	            case .success(let value):
	                guard self.needsTransition(options: options, cacheType: value.cacheType) else {
	                    mutatingSelf.placeholder = nil
                            // 为目标 imageView 设置 image
	                    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 {
	                    mutatingSelf.placeholder = nil
	                    self.base.image = image
	                }
	                completionHandler?(result)
	            }
	        }
	    }
	)
	mutatingSelf.imageTask = task
	return task
}
  • 首先判断了一下 source 是否为空,为空直接返回错误。接着又设置了 placeholder/taskIdentifier/preloadAllAnimationData 等属性。
  • 重点来了:调用 retrieveImage 函数来异步获取图片,然后在回调中的 success case 中,通过 self.base.image = value.image 为 imageView 设置 image。

retrieveImage 实现的主要代码:

php 复制代码
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
    }
}
  • 如果 forceRefresh 为真,则直接下载图片并缓存。
  • 如果 loadedFromCache 为真,则代表该图片资源已缓存,直接使用缓存的图片。

主题流程图如下:

Loading image from either URLSession-based networking or local provided data

Kingfisher 不仅可以加载网络图片,它也支持从本地文件 URL 中获取图片。

具体实现:

swift 复制代码
extension Resource {
    public func convertToSource(overrideCacheKey: String? = nil) -> Source {
        let key = overrideCacheKey ?? cacheKey
        return downloadURL.isFileURL ?
            .provider(LocalFileImageDataProvider(fileURL: downloadURL, cacheKey: key)) :
            .network(KF.ImageResource(downloadURL: downloadURL, cacheKey: key))
    }
}

kf 接受的图片来源参数必须是一个遵守了 Resource 的协议,该协议的扩展函数里对 URL 类型进行了判断:

  • 如果是本地文件,则调用 provider 去加载。
  • 如果是网络文件,则调用 network 去加载。
相关推荐
大熊猫侯佩10 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(三)
数据库·swiftui·swift
大熊猫侯佩10 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(二)
数据库·swiftui·swift
大熊猫侯佩10 小时前
用异步序列优雅的监听 SwiftData 2.0 中历史追踪记录(History Trace)的变化
数据库·swiftui·swift
大熊猫侯佩11 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(一)
数据库·swiftui·swift
season_zhu1 天前
iOS开发:关于日志框架
ios·架构·swift
大熊猫侯佩1 天前
SwiftUI 中如何花样玩转 SF Symbols 符号动画和过渡特效
swiftui·swift·apple
大熊猫侯佩1 天前
SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的原因和解决
swiftui·swift·apple
大熊猫侯佩1 天前
使用令牌(Token)进一步优化 SwiftData 2.0 中历史记录追踪(History Trace)的使用
数据库·swift·apple
大熊猫侯佩1 天前
SwiftUI 在 iOS 18 中的 ForEach 点击手势逻辑发生改变的解决
swiftui·swift·apple