Kingfisher是一个纯 Swift 编写的图片加载库,支持图片的下载、缓存、处理(如分辨率、圆角、缩放)及设置时动画、占位图等等。充分利用了语言的特性实现丰富易用的 API ,良好的实践了 POP 的编程范式,且支持了最新的 SwiftUI。

从一行调用开始

Kingfisher 的使用非常便捷:

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

当然也能处理更复杂的需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let processor = DownsamplingImageProcessor(size: imageView.bounds.size)
|> RoundCornerImageProcessor(cornerRadius: 20)
imageView.kf.indicatorType = .activity
imageView.kf.setImage(
with: url,
placeholder: UIImage(named: "placeholderImage"),
options: [
.processor(processor),
.scaleFactor(UIScreen.main.scale),
.transition(.fade(1)),
.cacheOriginalImage
])
{
result in
switch result {
case .success(let value):
print("Task done for: \(value.source.url?.absoluteString ?? "")")
case .failure(let error):
print("Job failed: \(error.localizedDescription)")
}
}

同时也支持链式调用:

1
2
3
4
5
6
7
8
9
10
11
KF.url(url)
.placeholder(placeholderImage)
.setProcessor(processor)
.loadDiskFileSynchronously()
.cacheMemoryOnly()
.fade(duration: 0.25)
.lowDataModeSource(.network(lowResolutionURL))
.onProgress { receivedSize, totalSize in }
.onSuccess { result in }
.onFailure { error in }
.set(to: imageView)

Kingfisher 的 API 涉及兼顾了易用与实用性,并且有着非常详尽的注释。下面就从最常用的 imageView.kf.setImage() 方法来看看 KingFisher 的具体实现(以下简称 KF, 与构造器 KF 注意区分)。

.kf.setImage()

ImageView 为例,KF 通过协议 KingfisherCompatible 封装了一个名为 kf 的变量并在其get方法中通过向泛型传入self来方便对不同类型的拓展。忽略夸平台的代码后简化如下:

1
2
3
4
5
6
7
8
9
// ImageView 实例变量 kf 的封装
protocol KingfisherCompatible: AnyObject { }
extension KingfisherCompatible {
public var kf: KingfisherWrapper<Self> {
get { return KingfisherWrapper(self) }
set { }
}
}
extension ImageView: KingfisherCompatible{}

注意计算属性kf是一个KingfisherWrapper类型的结构体,这个结构体是对原生对象的封装,目的是为原生类拓展相应的自定义方法,通过泛型可以锁定原生类型,并通过 base实例对象持有该原生对象以供后续使用,取自 ImageView+KingFisher.swift 文件(忽略跨平台别名):

1
2
3
4
extension KingfisherWrapper where Base: ImageView {
public func setImage(){}
...
}

以上便是 KF 利用协议和泛型对 ImageView 进行封装与能力的拓展,至此即可调用imageView.kf.setImage()了。

下面就通过setImage()方法的参数来分析下 KF 的一些功能封装与思想

Source

1
func setImage(with source: Source?)

Source是一个用来表示图片“来源”的枚举,一共两个 case :

1
2
3
4
5
6
enum Source {
// 图片来自网络或本地(取决于 url 格式)
case network(Resource)
// 图片来自本地或其它编码格式
case provider(ImageDataProvider)
}

network

先看.network,它的关联值是遵守Resource协议的对象,该协议的目的较简单,作用是通过convertToSource方法将一些通用资源转为 Source以方便进行 cache 和 download 。当然最常用的资源就是 url :

1
2
3
4
extension URL: Resource {
public var cacheKey: String { return absoluteString }
public var downloadURL: URL { return self }
}

当然你也可以通过遵守Resource协议定义自己的资源类,只要提供缓存 key 和下载 url 即可交给 KF 处理。

provider

Source的另一个 case 是 provider ,它的关联值类型是ImageDataProvider。主要封装了本地图片的资源,该协议除了 cacheKey 和 fileURL 外,还要求提供一个处理图片 data 的方法供后续处理使用。ImageDataProviderResource一样,提供了将资源转为Source的默认实现。

此外,KF 还提供了四个默认的 Provider ,分别为:

1
2
3
4
5
6
7
8
// 用于本地文件路径下的图片资源
struct LocalFileImageDataProvider: ImageDataProvider { ... }
// 用于 Base64 编码的图片,需提供 Base64 字符串
struct Base64ImageDataProvider: ImageDataProvider { ... }
// 用于图片 data
struct RawImageDataProvider: ImageDataProvider { ... }
// 用于处理 AVAsset 资源截图
struct AVAssetImageDataProvider: ImageDataProvider { ... }

通过Source可以看到协议的优势,具有高拓展性、更好的抽象能力,另外也可以看到 Swift 中 Enum 的强大。

Placeholder

Placeholder 的作用是在图片加载时或图片加载失败的情况下显示的默认图,KF通过协议Placeholder声明了两个方法并针对UIViewUIImage做了默认实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public protocol Placeholder {
func add(to imageView: KFCrossPlatformImageView)
func remove(from imageView: KFCrossPlatformImageView)
}
extension Placeholder where Self: UIView {
public func add(to imageView: KFCrossPlatformImageView) {
imageView.addSubview(self)
translatesAutoresizingMaskIntoConstraints = false

centerXAnchor.constraint(equalTo: imageView.centerXAnchor).isActive = true
centerYAnchor.constraint(equalTo: imageView.centerYAnchor).isActive = true
heightAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true
widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true
}
public func remove(from imageView: KFCrossPlatformImageView) {
removeFromSuperview()
}
}

通过这个 extension 自定义的 View 也可以作为默认图被添加到 ImageView 上。

1
2
3
4
extension UIImage: Placeholder {
public func add(to imageView: KFCrossPlatformImageView) { imageView.image = self }
public func remove(from imageView: KFCrossPlatformImageView) { imageView.image = nil }
}

若默认图为UIImage则会被直接设置。

之后通过KingfisherWrapper封装的计算属性placeholderset/get方法来调用其add()方法,并通过setRetainedAssociatedObject()来为 imageView 动态绑定属性。

Associated Objects 是 ObjC Runtime 提供的运行时绑定属性的能力,内部通过两层 Hash Map 来维护所有对象动态绑定的变量,但需注意同名覆盖问题

Indicator

Indicator 的作用是提示图片正在下载,同样是在setImage()方法内进行设置,通过IndicatorType提供了四种类型:

1
2
3
4
5
6
enum IndicatorType {
case none
case activity
case image(imageData: Data)
case custom(indicator: Indicator)
}

但比较不解的是设置的方式,KF 并未在setImage()系列方法中提供相应参数,因此使用时需提前设置imageView.kf.indicatorType = .activity

Indicator 的行为通过Indicator协议来抽象,并提供两种默认的实现,ActivityIndicator封装了系统的 progress view 和菊花两种样式,ImageIndicator封装的是图片样式,支持 Gif 格式。

KingfisherOptionsInfo

setImage()之后的工作便是整合options这个参数,这个参数的类型是[KingfisherOptionsInfoItem]KingfisherOptionsInfoItem是一个枚举,提供了丰富的配置项,涉及缓存、下载、图片处理、动画以及一些行为开关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
enum KingfisherOptionsInfoItem {
case targetCache(ImageCache)
case originalCache(ImageCache)
case downloader(ImageDownloader)
case transition(ImageTransition)
case downloadPriority(Float)
case forceRefresh
case fromMemoryCacheOrRefresh
case forceTransition
case cacheMemoryOnly
case waitForCache
case onlyFromCache
case backgroundDecode
case callbackQueue(CallbackQueue)
case scaleFactor(CGFloat)
case preloadAllAnimationData
case requestModifier(AsyncImageDownloadRequestModifier)
case redirectHandler(ImageDownloadRedirectHandler)
case processor(ImageProcessor)
case cacheSerializer(CacheSerializer)
case imageModifier(ImageModifier)
case keepCurrentImageWhileLoading
case onlyLoadFirstFrame
case cacheOriginalImage
case onFailureImage(KFCrossPlatformImage?)
case alsoPrefetchToMemory
case loadDiskFileSynchronously
case memoryCacheExpiration(StorageExpiration)
case memoryCacheAccessExtendingExpiration(ExpirationExtending)
case diskCacheExpiration(StorageExpiration)
case diskCacheAccessExtendingExpiration(ExpirationExtending)
case processingQueue(CallbackQueue)
case progressiveJPEG(ImageProgressive)
case alternativeSources([Source])
case retryStrategy(RetryStrategy)
case lowDataMode(Source?)
}

之后KFoptions连同sourcecompletion等作为参数传至
KingfisherManager.shared.retrieveImage()方法并返回一个 task 。至此我们通过setImage()方法见到了 KF 核心功能的调度者KingfisherManager,它的工作包括下载、存取(缓存)、处理。下面通过KingfisherManager来具体看下这些核心功能的实现。


KingfisherManager

这个类的核心是名为retrieveImage()的系列方法,除了供外部调用的两个方法,还包括内部对参数的处理,以及对重试、成功及失败的各种 handle ,这里不具体展开了,着重看下最终的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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
}
}

其中的调用的两个方法loadAndCacheImage()retrieveImageFromCache()分别负责加载并缓存图片以及从缓存中读取。通过之前传入的options来判断。

Download

KF 通过ImageDownloader类用来管理图片的下载,提供相应的接口,下载任务是由封装了URLSessionDataTaskSessionDataTask实现,并对并发任务做了优化。另外需要注意的是SessionDelegate这个类,除了作为URLSessionDataDelegate的 handler ,也承担了下载任务的维护。

ImageDownloader 是对外暴露的图片下载管理类,提供图片下载和取消的接口。接受SessionDelegate的回调,并通过ImageDownloaderDelegate来供外部处理下载结果及在处理下载周期中各时间点的时机。

此外在 downloadImage 方法的option参数中提供了一个requestModifier,在 request 发出之前支持对其进行修改,如添加 header 、map URL 等,体现了良好的开放性。

前面提到SessionDataTask是对URLSessionDataTask的封装,其内部声明了一个结构体TaskCallback,保存 downloadImage 方法中传入的回调以及 options ,用于后续下载流程使用。其中比较重要的是在图片下载完成后,通过ImageDataProcessor来将下载下来的 data 处理为图片,处理时会从TaskCallback对象中的 option 取出 ImageProcessor对象并调用其process方法,处理完成后通过 callback 返回结果。如果你不想对图片进行额外处理,KF 默认提供了DefaultImageProcessor

从这个简单的 downloadImage 系列方法可以看到 SDK 设计的重要原则,为调用者提供充分的拓展性,同时在内部约束这些开放的行为,良好的实践了开闭原则。其它的一些细节包括封装了一个Delegate类来帮助管理 closure 中对self的弱引用(感觉作用不大),一个名为CallbackQueue的枚举来方便任务派发的线程等。

Cache

KF 提供的缓存同时支持内存缓存与磁盘缓存,可分别设置缓存过期时间及占用大小,同时维护了 IO 线程以保证读写安全。在 init 方法中分别初始化内存与磁盘缓存类,内存缓存的默认大小是设备物理内存大小的 1/4 ,接着初始化线程,注册通知:收到didReceiveMemoryWarningNotification时去清理内存缓存,收到willTerminateNotificationdidEnterBackgroundNotification时清理磁盘空间。

存储图片时会首先将图片存入内存缓存,如果toDisk参数为 true ,则在 IO 线程中将图片写入磁盘,删除图片时也会同时检查内存缓存与磁盘缓存。

从缓存中取图片的流程是首先检查内存缓存,查到后直接返回,否则去查磁盘缓存,若查到则先将图片写入内存,写入完成后再将图片通过回调返回。KF也提供了直接从内存或磁盘取图片的接口。

清空缓存没有什么特别,同样提供的统一清理和分别清理内存和磁盘缓存的接口,已经后台清理磁盘缓存的功能。

此外还包括查询图片是否已被缓存、缓存的类型、缓存的 key 对应的文件名,以及查询已用的磁盘缓存空间等。下面来看下内存缓存与磁盘缓存的具体实现。

Memory cache

首先不太理解的是KFMemoryStorage本身是个遵守Storage协议的类。但后面被改为了 enum ,仅作用为一个命名空间,内部封装的Backend才是真正实现缓存逻辑的类。此外还封装了Congfig以及StorageObject

Backend是一个泛型类,即只保存该泛型的数据,同时CacheCostCalculable要求提供一个cacheCost变量:

1
2
3
4
5
6
class Backend<T: CacheCostCalculable> { ... }

extension KFCrossPlatformImage: CacheCostCalculable {
/// Cost of an image
public var cacheCost: Int { return kf.cost }
}

其内部持有了一个NSCache对象,key 为String,value 为StorageObject。所以KF的内存缓存就是通过NSCache实现的,虽然NSCache是号称线程安全,但KF还是给读写操作加了锁。

此外在内存的清理上,KF起了一个 timer 来定时清理过期的缓存对象,默认间隔为 2 分钟,过期时间默认为 5 分钟。而且提到的ImageCache类在接受到didReceiveMemoryWarningNotification时会直接将内存缓存清空。

Disk cache

磁盘缓存是通过FileManager进行数据读写的,但KF额外的做了几件事。

MemoryStorage一样,DiskStorage也是一个没有 case 的枚举,内部同样声明了一个泛型类Backend来实现缓存的主要逻辑,但泛型约束协议与MemoryStorage不同:

1
2
3
4
5
public protocol DataTransformable {
func toData() throws -> Data
static func fromData(_ data: Data) throws -> Self
static var empty: Self { get }
}

该协议要求实现 Data 与 Self 的相互转换,并提供一个空数据的实现。主要用于从 File system 存取数据时的类型转换。

为了提高查找效率,KF维护了一个Set用来保存已存入缓存的文件名,在查找文件时优先检查该 set 中是否有对应文件名,如果没有直接返回 nil 已减少对 File system 的访问次数。


总结

本文从一行调用开始,分析了KF提供的各种特性及实现,并重点关注了下载和缓存的实现,当然还有很多细节没有覆盖到,比如图片的绘制(圆角、裁切、效果)、图片格式的处理等等。

KF充分的利用了 Swift 语言的特性,在 API 的设计上非常实用且规范,同时保留了足够的可拓展性,在具体实现层面,KF并没有采用特别复杂的技术方案,但功能完善且实用性强,是一个值得学习的开源项目。