源码阅读系列之图片加载框架: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 去加载。
相关推荐
m0_748238922 天前
webgis入门实战案例——智慧校园
开发语言·ios·swift
Swift社区3 天前
Excel 列名称转换问题 Swift 解答
开发语言·excel·swift
东坡肘子4 天前
肘子的 Swift 周报 #063|异种肾脏移植取得突破
swiftui·swift·apple
威化饼的一隅5 天前
【多模态】swift-3框架使用
人工智能·深度学习·大模型·swift·多模态
opentogether7 天前
Swift 的动态性
开发语言·ssh·swift
苍墨穹天7 天前
SWIFT基本使用
linux·swift
SchneeDuan8 天前
从源码分析swift GCD_DispatchGroup
ios·swift·源码分析·gcd
请叫我飞哥@9 天前
iOS在项目中设置 Dev、Staging 和 Prod 三个不同的环境
ios·xcode·swift
Cedric_Anik12 天前
iOS渲染概述
ui·ios·swift
hxx22112 天前
iOS swift开发系列--如何给swiftui内容视图添加背景图片显示
ios·swiftui·swift