【SDWebImage】OC 时代图片加载的事实标准
iOS三方库精读 · 第 7 期
一、一句话介绍
SDWebImage 是一个历史悠久的图片异步下载与缓存框架,从 2009 年 Objective-C 时代延续至今,是 iOS 开发中最经典的图片加载方案之一,以完善的 GIF 支持、精细化并发控制和丰富的图片格式支持著称。
| 属性 | 信息 |
|---|---|
| ⭐ Stars | 25k+ |
| 最新版本 | 5.x(支持 Swift / SwiftUI) |
| License | MIT |
| 支持平台 | iOS 11+ / macOS 10.13+ / tvOS 11+ / watchOS 4+ |
| 作者 | Olivier Poitrey(rs) |
二、为什么选择它
没有 SDWebImage 之前,你的代码是什么样的?
// OC 原生方式:异步下载 + 手动缓存
- (void)loadImageFromURL:(NSURL *)url intoImageView:(UIImageView *)imageView {
// 1. 检查内存缓存
NSString *key = url.absoluteString;
UIImage *cachedImage = [self.memoryCache objectForKey:key];
if (cachedImage) {
imageView.image = cachedImage;
return;
}
// 2. 检查磁盘缓存
NSString *cachePath = [self.diskCachePath stringByAppendingPathComponent:key];
if ([[NSFileManager defaultManager] fileExistsAtPath:cachePath]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
UIImage *image = [UIImage imageWithContentsOfFile:cachePath];
dispatch_async(dispatch_get_main_queue(), ^{
[self.memoryCache setObject:image forKey:key];
imageView.image = image;
});
});
return;
}
// 3. 网络下载
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (data && !error) {
dispatch_async(dispatch_get_main_queue(), ^{
UIImage *image = [UIImage imageWithData:data];
[self.memoryCache setObject:image forKey:key];
[data writeToFile:cachePath atomically:YES];
imageView.image = image;
});
}
}];
[task resume];
}
痛点一目了然:
- 并发请求无去重,同一张图并发发出多个请求
- 并发数无限制,大量图片同时下载可能导致网络拥塞
- 缓存策略手写,内存警告、磁盘清理需要手动处理
- GIF 支持缺失,需要额外集成第三方库
- 现代格式不友好,WebP、HEIC 等格式需要额外解码
- SwiftUI 集成困难,需要额外适配层
有了 SDWebImage,一行代码搞定:
[imageView sd_setImageWithURL:url];
// Swift
imageView.sd_setImage(with: url)
三、核心功能速览
基础层(新手必读)
安装集成
Swift Package Manager(推荐):
// Package.swift
dependencies: [
.package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.18.0")
]
CocoaPods:
pod 'SDWebImage', '~> 5.18'
基础使用(UIKit):
import SDWebImage
// 最简用法
imageView.sd_setImage(with: URL(string: "https://example.com/photo.jpg"))
// 带占位图 + 完成回调
imageView.sd_setImage(
with: URL(string: "https://example.com/photo.jpg"),
placeholderImage: UIImage(named: "placeholder"),
options: [.progressiveLoad]
) { image, error, cacheType, url in
if let image = image {
print("图片加载成功,来源:\(cacheType == .none ? "网络" : "缓存")")
}
}
SwiftUI 集成(需要 SDWebImageSwiftUI):
import SDWebImageSwiftUI
struct AvatarView: View {
let url: URL?
var body: some View {
WebImage(url: url)
.resizable()
.scaledToFill()
.frame(width: 80, height: 80)
.clipShape(Circle())
}
}
进阶层(最佳实践)
并发流量控制(核心特性)
SDWebImage 提供了完善的并发控制机制,这是它区别于其他图片库的核心优势之一:
// 获取下载器配置
let config = SDWebImageDownloader.shared.config
// 设置最大并发数(默认 6)
config.maxConcurrentDownloads = 4
// 查询当前状态
print("当前并发数: \(SDWebImageDownloader.shared.currentDownloadCount)")
print("最大并发数: \(config.maxConcurrentDownloads)")
并发控制关键参数:
| 参数 | 默认值 | 说明 |
|---|---|---|
maxConcurrentDownloads | 6 | 同时进行的最大下载任务数 |
downloadTimeout | 15s | 单个请求超时时间 |
| URL 去重 | 自动 | 相同 URL 自动合并为一个下载任务 |
URL 去重机制详解:
当多个 ImageView 同时请求同一 URL 时,SDWebImage 内部只发一次真实的网络请求,其他请求挂起等待结果:
请求 A ─┐
请求 B ─┼─→ 同一 URL ─→ 单次网络请求 ─→ 广播结果给 A/B/C
请求 C ─┘
这是典型的 Subscriber 聚合 模式,大幅减少重复网络请求。
缓存配置
// 全局缓存配置
let cache = SDImageCache.shared
// 内存缓存配置
cache.config.maxMemoryCountLimit = 100 // 最多 100 张
cache.config.maxMemorySizeLimit = 50 * 1024 * 1024 // 50 MB
// 磁盘缓存配置
cache.config.maxDiskSize = 500 * 1024 * 1024 // 500 MB
cache.config.diskCacheExpireDuration = 7 * 24 * 60 * 60 // 7 天过期
// 主动清除
cache.clearMemory()
cache.clearDisk(onCompletion: nil)
// 查询磁盘缓存大小
let diskSize = cache.totalDiskSize()
let mb = Double(diskSize) / 1024.0 / 1024.0
print("磁盘缓存:\(mb) MB")
进度监听 & 取消
imageView.sd_setImage(
with: url,
placeholderImage: nil,
options: [],
progress: { receivedSize, expectedSize, url in
let progress = Float(receivedSize) / Float(expectedSize)
DispatchQueue.main.async {
progressView.setProgress(progress, animated: true)
}
}
)
// 取消当前加载任务
imageView.sd_cancelCurrentImageLoad()
图片处理(Transformer)
// 使用 SDImageTransformer 进行图片处理
let transformer = SDImagePipelineTransformer(transformers: [
SDImageResizingTransformer(size: CGSize(width: 200, height: 200), scaleMode: .aspectFill),
SDImageRoundCornerTransformer(radius: 20, corners: .allCorners, borderWidth: 0, borderColor: nil)
])
imageView.sd_setImage(
with: url,
placeholderImage: nil,
options: [],
context: [.imageTransformer: transformer]
)
GIF 动图支持(核心优势)
SDWebImage 对 GIF 有原生完整支持,这是它区别于 Kingfisher 的重要优势:
// 使用 SDAnimatedImageView 播放 GIF
let animatedImageView = SDAnimatedImageView()
animatedImageView.sd_setImage(with: gifURL)
// 或者在 UITableView/UICollectionView 中使用 SDAnimatedImageView
class GifCell: UITableViewCell {
let animatedImageView = SDAnimatedImageView()
func configure(with url: URL) {
animatedImageView.sd_setImage(with: url)
}
}
SDAnimatedImageView 特性:
- 自动管理动画播放/暂停
- 内存友好,按需解码帧
- 支持帧率控制
- 支持 Animated WebP
深入层(源码视角)
核心模块划分
SDWebImage 的架构清晰,主要分为四层:
┌─────────────────────────────────────────────┐
│ SDWebImageManager │ ← 统一入口,组织下载+缓存流水线
├─────────────────┬───────────────────────────┤
│ SDWebImageDownloader │ SDImageCache │ ← 下载器 / 多级缓存
├─────────────────┴───────────────────────────┤
│ SDImageTransformer │ ← 图片转换器
├─────────────────────────────────────────────┤
│ SDAnimatedImageView / sd_ Extension │ ← 视图层扩展
└─────────────────────────────────────────────┘
下载流程解析
┌──────────────────────────────────────────────────────────────┐
│ 图片加载流程 │
├──────────────────────────────────────────────────────────────┤
│ ① sd_setImage 调用 │
│ ↓ │
│ ② SDWebImageManager.loadImage │
│ ↓ │
│ ③ 查询内存缓存(同步)→ 命中则返回 │
│ ↓ │
│ ④ 查询磁盘缓存(异步)→ 命中则解码后存入内存并返回 │
│ ↓ │
│ ⑤ 开始下载 → URL 去重检查 │
│ ↓ │
│ ⑥ 下载完成 → 解码 → 存入内存+磁盘缓存 │
│ ↓ │
│ ⑦ 回调所有等待方 │
└──────────────────────────────────────────────────────────────┘
四、实战演示:并发控制 + GIF 列表
模拟一个真实的表情包/动图列表场景,展示 SDWebImage 的并发控制和 GIF 播放能力:
import UIKit
import SDWebImage
struct GIFItem {
let id: String
let url: URL
let title: String
}
// MARK: - Cell
class GIFCell: UITableViewCell {
static let reuseID = "GIFCell"
private let animatedImageView = SDAnimatedImageView()
private let titleLabel = UILabel()
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setupUI()
}
required init?(coder: NSCoder) { fatalError() }
private func setupUI() {
animatedImageView.contentMode = .scaleAspectFill
animatedImageView.clipsToBounds = true
animatedImageView.layer.cornerRadius = 8
contentView.addSubview(animatedImageView)
contentView.addSubview(titleLabel)
// layout 省略...
}
func configure(with item: GIFItem) {
// 使用 SDAnimatedImageView 自动播放 GIF
animatedImageView.sd_setImage(
with: item.url,
placeholderImage: UIImage(named: "gif_placeholder"),
options: [.progressiveLoad]
)
titleLabel.text = item.title
}
override func prepareForReuse() {
super.prepareForReuse()
animatedImageView.sd_cancelCurrentImageLoad()
animatedImageView.image = nil
}
}
// MARK: - ViewController
class GIFListViewController: UITableViewController {
private var items: [GIFItem] = []
override func viewDidLoad() {
super.viewDidLoad()
// 配置并发控制
let config = SDWebImageDownloader.shared.config
config.maxConcurrentDownloads = 4 // 限制并发数,避免网络拥塞
// 配置缓存
SDImageCache.shared.config.maxMemoryCountLimit = 50
SDImageCache.shared.config.maxDiskSize = 200 * 1024 * 1024 // 200 MB
}
}
五、源码亮点
深入层:设计思想解析
1. 并发控制 —— NSOperationQueue
SDWebImage 使用 NSOperationQueue 实现并发控制:
// 源码简化版
@interface SDWebImageDownloader ()
@property (strong, nonatomic) NSOperationQueue *downloadQueue;
@property (assign, nonatomic) NSUInteger maxConcurrentDownloads;
@end
- (void)addOperation:(NSOperation *)operation {
// NSOperationQueue 自动管理并发数
[self.downloadQueue addOperation:operation];
}
亮点:通过 maxConcurrentOperationCount 精确控制并发数,避免网络拥塞。
2. URL 去重 —— NSMutableDictionary
// 内部维护 URL -> callbacks 的映射
@property (strong, nonatomic) NSMutableDictionary<NSURL *, SDWebImageDownloaderOperation *> *URLOperations;
// 新请求到来时检查
- (id)downloadImageWithURL:(NSURL *)url {
SDWebImageDownloaderOperation *operation = self.URLOperations[url];
if (operation) {
// 已存在相同 URL 的下载,附加回调而非重复下载
[operation addHandlersForProgress:progressBlock completed:completedBlock];
return operation;
}
// 创建新的下载任务
operation = [[SDWebImageDownloaderOperation alloc] initWithRequest:request];
self.URLOperations[url] = operation;
return operation;
}
3. GIF 解码 —— SDAnimatedImage
// 使用 ImageIO 解码 GIF 帧
- (UIImage *)animatedImageWithFrames:(NSArray<SDAnimatedImageFrame *> *)frames {
// 按需解码,内存友好
// 支持帧率控制
// 自动处理循环次数
}
亮点:不一次性解码所有帧,而是按需解码,避免内存爆炸。
4. 缓存查询流水线
// 先内存(同步),后磁盘(异步)
- (void)queryCacheOperationForKey:(NSString *)key done:(SDCacheQueryCompletedBlock)doneBlock {
// 1. 内存缓存(同步,纳秒级)
UIImage *image = [self memoryCache objectForKey:key];
if (image) {
doneBlock(image, nil, SDImageCacheTypeMemory);
return;
}
// 2. 磁盘缓存(异步,毫秒级)
dispatch_async(self.ioQueue, ^{
UIImage *diskImage = [self diskImageForKey:key];
if (diskImage) {
// 命中后写入内存缓存
[self.memoryCache setObject:diskImage forKey:key];
}
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, nil, SDImageCacheTypeDisk);
});
});
}
六、踩坑记录
问题 1:Cell 复用导致图片错乱
原因:Cell 复用时旧的下载任务未取消,新图片还没加载完,旧任务回来把新 Cell 的图片覆盖了。
解决:
override func prepareForReuse() {
super.prepareForReuse()
imageView.sd_cancelCurrentImageLoad()
imageView.image = nil
}
问题 2:并发数过高导致网络拥塞
原因:默认并发数为 6,某些场景(如列表快速滚动)可能不够用,导致请求堆积。
解决:根据网络状况动态调整:
// 监听网络状态
let manager = SDWebImageManager.shared
// WiFi 下可以适当提高并发数
SDWebImageDownloader.shared.config.maxConcurrentDownloads = 8
// 蜂窝网络下降低并发数
SDWebImageDownloader.shared.config.maxConcurrentDownloads = 4
问题 3:GIF 内存占用过高
原因:SDAnimatedImageView 默认会缓存所有解码帧,大 GIF 可能导致内存峰值。
解决:
// 使用 SDAnimatedImage 的帧缓存配置
let options: [SDWebImageContextOption: Any] = [
.imageThumbnailPixelSize: CGSize(width: 200, height: 200) // 缩略图尺寸
]
animatedImageView.sd_setImage(with: url, context: options)
问题 4:缓存未按预期清理
原因:SDImageCache 的清理策略基于过期时间和空间限制,可能不会立即生效。
解决:
// 主动触发清理
SDImageCache.shared.clearMemory() // 立即清除内存缓存
SDImageCache.shared.clearDisk {
print("磁盘缓存已清除")
}
// 删除过期缓存
SDImageCache.shared.deleteOldFiles {
print("过期缓存已删除")
}
问题 5:自定义 Header 未生效
原因:默认下载器不携带自定义 header,CDN 鉴权时请求被拦截。
解决:
let modifier = SDWebImageDownloaderRequestModifier { request in
var mutableRequest = request
mutableRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
return mutableRequest
}
SDWebImageDownloader.shared.requestModifier = modifier
问题 6:SwiftUI WebImage 占位图不显示
原因:SDWebImageSwiftUI 的 API 与 KFImage 略有不同。
解决:
// SDWebImageSwiftUI 的正确用法
WebImage(url: url) { image in
image
.resizable()
.scaledToFill()
} placeholder: {
Image(systemName: "photo")
.foregroundStyle(.secondary)
}
.frame(width: 80, height: 80)
七、延伸思考
SDWebImage vs Kingfisher 深度对比
Kingfisher 是 SDWebImage 在图片加载领域最直接的竞品,以下从五个维度深度对比:
1. 语言 & 生态定位
| SDWebImage | Kingfisher | |
|---|---|---|
| 实现语言 | OC 编写 + Swift 桥接层 | 纯 Swift,无 OC 代码 |
| 诞生年份 | 2009(OC 时代王者) | 2015(随 Swift 生态) |
| 包体积 | 较大(OC 运行时 + 桥接层) | 轻量(~500 KB) |
| 定位 | 兼容 OC 遗留项目的首选 | Swift / SwiftUI 原生公民 |
2. SwiftUI 支持
Kingfisher 内置 KFImage,是真正的 SwiftUI 原生组件:
// Kingfisher:原生 KFImage
KFImage(url)
.placeholder { ProgressView() }
.fade(duration: 0.3)
.resizable()
.scaledToFill()
SDWebImage 通过独立子库 SDWebImageSwiftUI 提供 WebImage:
// SDWebImage:适配层 WebImage
WebImage(url: url) { image in
image.resizable().scaledToFill()
} placeholder: {
ProgressView()
}
3. UIKit API 对比
两者 UIKit 用法高度对称,仅前缀不同:
// SDWebImage
imageView.sd_setImage(with: url)
imageView.sd_cancelCurrentImageLoad()
// Kingfisher
imageView.kf.setImage(with: url)
imageView.kf.cancelDownloadTask()
4. 并发控制(SDWebImage 优势)
| SDWebImage | Kingfisher | |
|---|---|---|
| maxConcurrentDownloads | ✅ 完善支持 | ⚠️ 基础支持 |
| 执行顺序(FIFO/LIFO) | ✅ 支持 | ❌ 不支持 |
| URL 去重 | ✅ 自动合并 | ✅ 自动合并 |
| 并发数动态调整 | ✅ 运行时可调 | ⚠️ 需要创建新实例 |
// SDWebImage:完善的并发控制
let config = SDWebImageDownloader.shared.config
config.maxConcurrentDownloads = 4 // 动态调整
// config.executionOrder = .lifo // 后进先出(列表场景推荐)
关键差异:SDWebImage 的并发控制更精细化,适合需要严格管理网络请求的场景。
5. GIF / 动图支持(SDWebImage 核心优势)
| SDWebImage | Kingfisher | |
|---|---|---|
| GIF 支持 | 完整支持 ✅ | 基础支持 |
| 帧率控制 | ✅ 完整 | ⚠️ 有限 |
| 内存策略 | ✅ 专项优化 | ⚠️ 表现不稳定 |
| AnimatedImageView | SDAnimatedImageView | AnimatedImageView |
源码对比:
// SDWebImage:专用 SDAnimatedImageView
let animatedImageView = SDAnimatedImageView()
animatedImageView.sd_setImage(with: gifURL)
// 自动管理帧缓存、播放/暂停、内存优化
// Kingfisher:AnimatedImageView
let animatedImageView = AnimatedImageView()
animatedImageView.kf.setImage(with: gifURL)
// 基础支持,但深度不如 SD
结论:需要深度 GIF 支持时,SDWebImage 更成熟。
6. 图片处理管道
两者都支持链式处理器,但 API 风格截然不同:
// Kingfisher:|> 操作符组合,Swift 惯用风格
let processor = DownsamplingImageProcessor(size: targetSize)
|> RoundCornerImageProcessor(cornerRadius: 20)
|> BlurImageProcessor(blurRadius: 4)
imageView.kf.setImage(with: url, options: [.processor(processor)])
// SDWebImage:SDImagePipelineTransformer 数组风格
let transformer = SDImagePipelineTransformer(transformers: [
SDImageResizingTransformer(size: targetSize, scaleMode: .aspectFill),
SDImageRoundCornerTransformer(radius: 20, corners: .allCorners, borderWidth: 0, borderColor: nil),
SDImageBlurTransformer(radius: 4)
])
imageView.sd_setImage(with: url, context: [.imageTransformer: transformer])
一句话选型
新建 Swift / SwiftUI 项目 → Kingfisher ✅(原生体验更好)
老 OC 项目 / OC+Swift 混编 → SDWebImage ✅(生态更成熟)
重度 GIF / 动图 → SDWebImage ✅(更专业)
需要精细化并发控制 → SDWebImage ✅(更完善)
极致内存优化(海量图片) → Nuke ✅(pipeline 架构占用最低)
推荐使用场景
✅ 适合 SDWebImage 的场景:
- 项目以 OC 为主,或需要从老项目迁移
- 需要完整 GIF 动图支持(核心优势)
- 需要精细化并发控制(并发数调整、FIFO/LIFO)
- 需要 WebP/HEIC/AVIF 等现代格式原生支持
- 需要深度 ObjC 互操作的遗留项目
❌ 不推荐 SDWebImage 的场景:
- 纯 Swift 项目,不想引入 ObjC 依赖(考虑 Kingfisher)
- SwiftUI 为主的新项目(Kingfisher 的 KFImage 体验更好)
- 极致内存优化(考虑 Nuke,内置 pipeline 架构内存占用更低)
八、参考资源
- GitHub 仓库:github.com/SDWebImage/…
- 官方 Wiki:github.com/SDWebImage/…
- SDWebImageSwiftUI:github.com/SDWebImage/…
- 图片缓存库性能对比:Image Caching Libraries in Swift(Medium)
- 系列 Demo 仓库:App 内 iOSLibraries 模块
九、本期互动
小作业
使用 SDWebImage 实现一个 并发可控的表情包列表:
- 使用
UITableView展示 GIF 列表 - 配置
maxConcurrentDownloads = 4,观察网络请求行为 - 每个 Cell 使用
SDAnimatedImageView播放 GIF - 实现上拉加载更多,并在加载时调整并发数(WiFi=8,蜂窝=4)
- 完成标准:滑动流畅,GIF 播放正常,Instruments 中内存不超过 150 MB
欢迎在评论区贴出你的实现思路或关键代码片段。
思考题
SDWebImage 通过 NSOperationQueue 实现并发控制,而 Kingfisher 使用自己的调度机制。如果让你自己实现一个并发控制的下载器,你会选择哪种方案?GCD 的 semaphore、OperationQueue、还是 Swift Concurrency 的 TaskGroup?各有什么优缺点?
读者征集
下一期候选选题:Alamofire / The Composable Architecture(TCA) / swift-collections
欢迎在评论区投票!你在使用 SDWebImage 时踩过哪些坑?优质回答会收录进下一期《踩坑记录》。
📅 本系列每周五晚更新
✅ 第6期:SnapKit · ➡️ 第7期:SDWebImage(本期) · ○ 第8期:待定