前言
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)
首先,我们可以看到对 UIImageView
的 kf
属性调用 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 去加载。