源码阅读系列之图片加载框架: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 去加载。
相关推荐
Keya20 小时前
lipo 命令行指南
ios·xcode·swift
zhangmeng20 小时前
SwiftUI中如何实现子视图向父视图传递数据?
ios·swiftui·swift
Saafo20 小时前
迁移至 Swift Actors
ios·swift
杂雾无尘2 天前
告别构建错误, iOS 开发架构难题全面解析, 避免 CPU 架构陷阱
ios·swift·客户端
大熊猫侯佩2 天前
探秘 WWDC 25 全新 #Playground 宏:提升 Swift 开发效率的超级神器
xcode·swift·wwdc
移动端小伙伴3 天前
10.推送的扩展能力 — 打造安全的通知体验
swift
移动端小伙伴3 天前
推送的扩展能力 — 打造个性化的通知体验
swift
移动端小伙伴3 天前
远程推送(Remote Push Notification)
swift
移动端小伙伴3 天前
本地通知的精准控制三角:时间、位置、情境
swift
移动端小伙伴3 天前
本地通知内容深度解析 — 打造丰富的通知体验
swift