【Kingfisher】Swift 世界里最优雅的图片加载库
iOS三方库精读 · 第 2 期
一、一句话介绍
Kingfisher 是一个用于 iOS / macOS / watchOS / tvOS 的纯 Swift 图片下载与缓存库,它让异步加载网络图片、管理多级缓存这件繁琐的事情,变成一行代码的极简体验。
| 属性 | 信息 |
|---|---|
| ⭐ Stars | 23k+ |
| 最新版本 | 8.x(支持 Swift 6 / Swift Concurrency) |
| License | MIT |
| 支持平台 | iOS 13+ / macOS 10.15+ / watchOS 6+ / tvOS 13+ |
| 作者 | Wei Wang(onevcat) |
二、为什么选择它
没有 Kingfisher 之前,你的代码是什么样的?
// 原生方式:异步下载 + 手动缓存,至少需要 30 行
let cache = NSCache<NSString, UIImage>()
func loadImage(from url: URL, into imageView: UIImageView) {
let key = url.absoluteString as NSString
if let cached = cache.object(forKey: key) {
imageView.image = cached
return
}
URLSession.shared.dataTask(with: url) { data, _, _ in
guard let data, let image = UIImage(data: data) else { return }
cache.setObject(image, forKey: key)
DispatchQueue.main.async { imageView.image = image }
}.resume()
}
痛点一目了然:
- 内存缓存手写,NSCache 无法自动持久化,App 冷启动图片重新下载
- 磁盘缓存缺失,弱网场景用户体验极差
- 线程切换繁琐,每次手写
DispatchQueue.main.async - 重复请求无去重,同一张图并发发出多个请求
- 图片处理无支持,裁剪/圆角/滤镜需要另外封装
- SwiftUI 集成困难,
AsyncImage功能有限,缺乏缓存层
有了 Kingfisher,上面所有问题,一行代码搞定:
imageView.kf.setImage(with: url)
三、核心功能速览
基础层(新手必读)
安装集成
Swift Package Manager(推荐):
// Package.swift
dependencies: [
.package(url: "https://github.com/onevcat/Kingfisher.git", from: "8.0.0")
]
CocoaPods:
pod 'Kingfisher', '~> 8.0'
基础使用(UIKit):
import Kingfisher
// 最简用法
imageView.kf.setImage(with: URL(string: "https://example.com/photo.jpg"))
// 带占位图 + 完成回调
imageView.kf.setImage(
with: URL(string: "https://example.com/photo.jpg"),
placeholder: UIImage(named: "placeholder")
) { result in
switch result {
case .success(let value):
print("图片加载成功:\(value.source.url?.absoluteString ?? "")")
case .failure(let error):
print("加载失败:\(error.localizedDescription)")
}
}
SwiftUI 集成:
import Kingfisher
struct AvatarView: View {
let url: URL?
var body: some View {
KFImage(url)
.placeholder { Image(systemName: "person.circle") }
.resizable()
.scaledToFill()
.frame(width: 80, height: 80)
.clipShape(Circle())
}
}
进阶层(最佳实践)
图片处理器(ImageProcessor)
Kingfisher 内置丰富的图片处理器,支持链式组合:
// 圆角 + 下采样(性能更优,比 resizable 更省内存)
let processor = DownsamplingImageProcessor(size: CGSize(width: 100, height: 100))
|> RoundCornerImageProcessor(cornerRadius: 12)
|> BlurImageProcessor(blurRadius: 2)
imageView.kf.setImage(
with: url,
options: [
.processor(processor),
.scaleFactor(UIScreen.main.scale),
.cacheOriginalImage // 同时缓存原图,避免重复下载
]
)
缓存配置
// 全局缓存配置
let cache = ImageCache.default
// 内存缓存:最多 100 张
cache.memoryStorage.config.countLimit = 100
// 磁盘缓存:最大 500 MB,7 天过期
cache.diskStorage.config.sizeLimit = 500 * 1024 * 1024
cache.diskStorage.config.expiration = .days(7)
// 主动清除
cache.clearMemoryCache()
cache.clearDiskCache()
// 检查缓存状态
cache.retrieveImageInMemoryCache(forKey: url.absoluteString)
进度监听 & 取消
imageView.kf.setImage(
with: url,
progressBlock: { receivedSize, totalSize in
let progress = Float(receivedSize) / Float(totalSize)
progressView.setProgress(progress, animated: true)
}
)
// 取消当前加载任务
imageView.kf.cancelDownloadTask()
Swift Concurrency(async/await)支持
// Swift 5.5+,直接 await 获取图片
let image = try await KingfisherManager.shared.retrieveImage(
with: ImageResource(downloadURL: url)
).image
// 在 SwiftUI 中配合 task modifier
.task {
let result = try? await KingfisherManager.shared
.retrieveImage(with: .network(url))
self.image = result?.image
}
自定义图片来源(ImageDataProvider)
// 从本地文件、Base64、甚至数据库加载
struct LocalFileProvider: ImageDataProvider {
let fileURL: URL
var cacheKey: String { fileURL.absoluteString }
func data(handler: @escaping (Result<Data, Error>) -> Void) {
DispatchQueue.global().async {
let data = try? Data(contentsOf: fileURL)
handler(data.map { .success($0) } ?? .failure(LoadError()))
}
}
}
imageView.kf.setImage(with: LocalFileProvider(fileURL: localURL))
深入层(源码视角)
核心模块划分
Kingfisher 的架构非常清晰,主要分为五层:
┌─────────────────────────────────────────┐
│ KingfisherManager │ ← 统一入口,组织下载+缓存流水线
├──────────────┬──────────────────────────┤
│ ImageDownloader │ ImageCache │ ← 下载器 / 多级缓存
├──────────────┴──────────────────────────┤
│ ImageProcessor 链 │ ← 责任链模式处理图片
├─────────────────────────────────────────┤
│ KFImage / KF Extension(UIKit) │ ← 视图层扩展 DSL
└─────────────────────────────────────────┘
四、实战演示:带分页的图片列表 + 内存占用优化
模拟一个真实的商品列表场景,使用 Downsampling 避免大图撑爆内存:
// Swift 5.9+ / Kingfisher 8.x
import UIKit
import Kingfisher
struct Product {
let id: String
let imageURL: URL
let name: String
}
// MARK: - Cell
class ProductCell: UICollectionViewCell {
static let reuseID = "ProductCell"
private let imageView = UIImageView()
private let nameLabel = UILabel()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) { fatalError() }
private func setupUI() {
imageView.contentMode = .scaleAspectFill
imageView.clipsToBounds = true
contentView.addSubview(imageView)
contentView.addSubview(nameLabel)
// layout 省略...
}
func configure(with product: Product) {
let targetSize = CGSize(
width: bounds.width * UIScreen.main.scale,
height: bounds.height * UIScreen.main.scale
)
// 关键:DownsamplingImageProcessor 先缩图再解码,大幅降低内存占用
let processor = DownsamplingImageProcessor(size: targetSize)
imageView.kf.cancelDownloadTask() // 复用时先取消旧任务
imageView.kf.setImage(
with: product.imageURL,
placeholder: UIImage(named: "product_placeholder"),
options: [
.processor(processor),
.scaleFactor(UIScreen.main.scale),
.transition(.fade(0.2)), // 淡入过渡
.cacheOriginalImage, // 缓存原图,处理后图片分开缓存
.backgroundDecode // 后台解码,不阻塞主线程
]
)
nameLabel.text = product.name
}
override func prepareForReuse() {
super.prepareForReuse()
imageView.kf.cancelDownloadTask()
imageView.image = nil
}
}
// MARK: - ViewController
class ProductListViewController: UICollectionViewController {
private var products: [Product] = []
override func viewDidLoad() {
super.viewDidLoad()
// 针对列表场景预取
collectionView.prefetchDataSource = self
}
}
extension ProductListViewController: UICollectionViewDataSourcePrefetching {
func collectionView(_ cv: UICollectionView, prefetchItemsAt indexPaths: [IndexPath]) {
let urls = indexPaths.compactMap { products[$0.item].imageURL as URL? }
ImagePrefetcher(urls: urls).start() // Kingfisher 内置预取器
}
func collectionView(_ cv: UICollectionView, cancelPrefetchingForItemsAt indexPaths: [IndexPath]) {
let urls = indexPaths.compactMap { products[$0.item].imageURL as URL? }
ImagePrefetcher(urls: urls).stop()
}
}
五、源码亮点
进阶层:值得借鉴的用法
KF Builder DSL
Kingfisher 8.x 引入了链式 Builder 风格的 API:
// 替代 options 数组,更可读
KF.url(url)
.placeholder(UIImage(named: "placeholder"))
.fade(duration: 0.3)
.resizing(referenceSize: .init(width: 200, height: 200), mode: .aspectFit)
.roundCorner(radius: .point(12))
.onSuccess { result in print("done: \(result.cacheType)") }
.set(to: imageView)
深入层:设计思想解析
1. 责任链模式 —— ImageProcessor
每个 ImageProcessor 实现同一协议,通过 |> 运算符组合成链:
// 源码简化版
public protocol ImageProcessor {
var identifier: String { get }
func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage?
}
// |> 运算符将两个 processor 合并为 GeneralProcessor
public func |> (left: ImageProcessor, right: ImageProcessor) -> ImageProcessor {
return left.append(another: right)
}
亮点:每种处理器的 identifier 被拼接为缓存 key 的一部分,相同 URL + 不同处理器的图片会分别缓存,无需手动管理。
2. 请求去重 —— TaskGroup
当多个视图同时请求同一 URL 时,Kingfisher 内部只发一次真实的网络请求,其他请求挂起等待结果:
// 内部维护 [URL: DownloadTask] 字典
// 新请求到来时,若同 URL 已在下载,直接附加回调而非重复下载
// 下载完成后,批量回调所有等待方
这是典型的 Subscriber 聚合 模式,大幅减少重复网络请求。
3. DownsamplingImageProcessor 的内存优化原理
// 核心:使用 ImageIO 在解码前就缩放,而非解码后再 UIImage.draw
let options: [CFString: Any] = [
kCGImageSourceCreateThumbnailFromImageAlways: true,
kCGImageSourceShouldCacheImmediately: true,
kCGImageSourceCreateThumbnailWithTransform: true,
kCGImageSourceThumbnailMaxPixelSize: maxPixel
]
// 一张 4K 图在内存中从 ~48MB 缩减到 ~1MB(200×200 点)
六、踩坑记录
问题 1:Cell 复用导致图片错乱
原因:Cell 复用时旧的下载任务未取消,新图片还没加载完,旧任务回来把新 Cell 的图片覆盖了。
解决:
override func prepareForReuse() {
super.prepareForReuse()
imageView.kf.cancelDownloadTask()
imageView.image = nil // 清空,防止闪烁
}
问题 2:图片缓存 key 冲突(同 URL 多种尺寸)
原因:默认以 URL 字符串为缓存 key,处理器不同但 URL 相同时缓存互相覆盖。
解决:使用 processor 选项,Kingfisher 会自动将 processor identifier 追加进 key:
// 错误:手动修改 URL 来区分,不优雅
// 正确:让 processor 自动生成差异化 key
imageView.kf.setImage(
with: url,
options: [.processor(DownsamplingImageProcessor(size: thumbnailSize))]
)
问题 3:SwiftUI 列表中图片闪烁
原因:每次 View 重建时 KFImage 重新触发加载流程,即便缓存命中也会有短暂空白。
解决:
KFImage(url)
.fade(duration: 0) // 关闭 fade,缓存命中无需动画
.loadImmediately(true) // 命中内存缓存时同步加载
问题 4:大量图片导致内存峰值过高
原因:未使用 Downsampling,解码后的大图直接存入内存缓存。
解决:始终在列表中使用 DownsamplingImageProcessor,并设置合理的内存缓存上限:
ImageCache.default.memoryStorage.config.totalCostLimit =
50 * 1024 * 1024 // 50 MB 内存上限
问题 5:GIF 动图不播放
原因:Kingfisher 默认不自动播放 GIF,需要使用专用的 AnimatedImageView。
解决:
import Kingfisher
let animatedImageView = AnimatedImageView()
animatedImageView.kf.setImage(with: gifURL)
问题 6:自定义 HTTP Header 未生效
原因:默认下载器不携带自定义 header,CDN 鉴权时请求被拦截。
解决:
let modifier = AnyModifier { request in
var r = request
r.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
return r
}
imageView.kf.setImage(with: url, options: [.requestModifier(modifier)])
七、延伸思考
同类库横向对比
| 维度 | Kingfisher 8.x | SDWebImage 5.x | Nuke 12.x |
|---|---|---|---|
| 语言 | 纯 Swift | ObjC + Swift | 纯 Swift |
| SwiftUI 支持 | ✅ 原生 KFImage | ✅ SDWebImageSwiftUI | ✅ LazyImage |
| async/await | ✅ | ✅ | ✅ |
| 内存占用 | 中等(~118MB 峰值) | 较低(~72MB) | 最低(~72MB) |
| 图片处理器 | 丰富内置 + 自定义 | 插件式 | Pipeline 化 |
| GIF 支持 | AnimatedImageView | ✅ 原生支持 | 通过插件 |
| WebP / AVIF | 插件 | 插件 | 插件 |
| 活跃度 | ⭐ 活跃维护 | ⭐ 活跃维护 | ⭐ 活跃维护 |
| 学习曲线 | 低 | 中 | 中 |
推荐使用场景
✅ 适合 Kingfisher 的场景:
- 纯 Swift 项目,不想引入 ObjC 依赖
- 需要丰富的图片处理器(圆角/模糊/裁剪)
- SwiftUI 为主的新项目
- 快速接入,文档友好,上手成本低
❌ 不推荐 Kingfisher 的场景:
- 极致内存优化(考虑 Nuke,内置 pipeline 架构内存占用更低)
- 需要深度 ObjC 互操作的遗留项目(考虑 SDWebImage)
- 需要视频帧/渐进式 JPEG 复杂场景(Nuke 更专业)
八、参考资源
- GitHub 仓库:github.com/onevcat/Kin…
- 官方 Wiki:github.com/onevcat/Kin…
- 作者博客(onevcat):onevcat.com — 多篇关于 Kingfisher 设计思路的深度好文
- 图片缓存库性能对比:Image Caching Libraries in Swift(Medium)
- 系列 Demo 仓库:
github.com/[你的ID]/ios-lib-demos(可 Star/Fork 跟学)
九、本期互动
小作业
使用 Kingfisher 实现一个 支持预取的瀑布流图片列表:
- 使用
UICollectionView+ 自定义瀑布流 Layout - 实现
UICollectionViewDataSourcePrefetching,在用户滑动前预取下一屏图片 - 每个 Cell 使用
DownsamplingImageProcessor限制内存 - 完成标准:滑动流畅,Instruments 中内存不超过 100 MB
欢迎在评论区贴出你的实现思路或关键代码片段。
思考题
Kingfisher 对相同 URL 的并发请求做了去重(只发一次网络请求,结果广播给所有等待方)。如果让你自己实现这个机制,你会选择什么数据结构来管理"等待者列表"?在 Swift Concurrency(Actor + AsyncStream)的背景下,有没有更优雅的实现方案?
读者征集
下一期候选选题:Alamofire / The Composable Architecture(TCA) / swift-collections
欢迎在评论区投票!你在使用 Kingfisher 时踩过哪些坑?优质回答会收录进下一期《踩坑记录》。
📅 本系列每周五晚更新
✅ 第1期:Alamofire · ➡️ 第2期:Kingfisher(本期) · ○ 第3期:待定