源码阅读系列之图片加载框架: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 去加载。
相关推荐
struggle20251 天前
适用于 iOS 的 开源Ultralytics YOLO:应用程序和 Swift 软件包,用于在您自己的 iOS 应用程序中运行 YOLO
yolo·ios·开源·app·swift
一丝晨光2 天前
数值溢出保护?数值溢出应该是多少?Swift如何让整数计算溢出不抛出异常?类型最大值和最小值?
java·javascript·c++·rust·go·c·swift
Swift社区2 天前
Swift实战:如何优雅地从二叉搜索树中挑出最接近的K个值
开发语言·ios·swift
fydw_7152 天前
大语言模型RLHF训练框架全景解析:OpenRLHF、verl、LLaMA-Factory与SWIFT深度对比
语言模型·swift·llama
文件夹__iOS3 天前
深入浅出 iOS 对象模型:isa 指针 与 Swift Metadata
ios·swift
I烟雨云渊T4 天前
iOS实名认证模块的具体实现过程(swift)
ios·cocoa·swift
Swift社区5 天前
LeetCode 270:在二叉搜索树中寻找最接近的值(Swift 实战解析)
算法·leetcode·swift
I烟雨云渊T5 天前
iOS瀑布流布局的实现(swift)
开发语言·ios·swift
Pythonliu77 天前
启智平台调试 qwen3 4b ms-swift
开发语言·swift
画个大饼7 天前
iOS启动优化:从原理到实践
macos·ios·objective-c·swift·启动优化