【Kingfisher】Swift 世界里最优雅的图片加载库

2 阅读7分钟

【Kingfisher】Swift 世界里最优雅的图片加载库

iOS三方库精读 · 第 2 期


一、一句话介绍

Kingfisher 是一个用于 iOS / macOS / watchOS / tvOS 的纯 Swift 图片下载与缓存库,它让异步加载网络图片、管理多级缓存这件繁琐的事情,变成一行代码的极简体验。

属性信息
⭐ Stars23k+
最新版本8.x(支持 Swift 6 / Swift Concurrency)
LicenseMIT
支持平台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.xSDWebImage 5.xNuke 12.x
语言纯 SwiftObjC + 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 更专业)

八、参考资源


九、本期互动


小作业

使用 Kingfisher 实现一个 支持预取的瀑布流图片列表

  1. 使用 UICollectionView + 自定义瀑布流 Layout
  2. 实现 UICollectionViewDataSourcePrefetching,在用户滑动前预取下一屏图片
  3. 每个 Cell 使用 DownsamplingImageProcessor 限制内存
  4. 完成标准:滑动流畅,Instruments 中内存不超过 100 MB

欢迎在评论区贴出你的实现思路或关键代码片段。

思考题

Kingfisher 对相同 URL 的并发请求做了去重(只发一次网络请求,结果广播给所有等待方)。如果让你自己实现这个机制,你会选择什么数据结构来管理"等待者列表"?在 Swift Concurrency(Actor + AsyncStream)的背景下,有没有更优雅的实现方案?

读者征集

下一期候选选题:Alamofire / The Composable Architecture(TCA) / swift-collections

欢迎在评论区投票!你在使用 Kingfisher 时踩过哪些坑?优质回答会收录进下一期《踩坑记录》。


📅 本系列每周五晚更新
✅ 第1期:Alamofire · ➡️ 第2期:Kingfisher(本期) · ○ 第3期:待定