【SDWebImage】OC 时代图片加载的事实标准

0 阅读10分钟

【SDWebImage】OC 时代图片加载的事实标准

iOS三方库精读 · 第 7 期


一、一句话介绍

SDWebImage 是一个历史悠久的图片异步下载与缓存框架,从 2009 年 Objective-C 时代延续至今,是 iOS 开发中最经典的图片加载方案之一,以完善的 GIF 支持、精细化并发控制和丰富的图片格式支持著称。

属性信息
⭐ Stars25k+
最新版本5.x(支持 Swift / SwiftUI)
LicenseMIT
支持平台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)")

并发控制关键参数:

参数默认值说明
maxConcurrentDownloads6同时进行的最大下载任务数
downloadTimeout15s单个请求超时时间
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. 语言 & 生态定位
SDWebImageKingfisher
实现语言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 优势)
SDWebImageKingfisher
maxConcurrentDownloads✅ 完善支持⚠️ 基础支持
执行顺序(FIFO/LIFO)✅ 支持❌ 不支持
URL 去重✅ 自动合并✅ 自动合并
并发数动态调整✅ 运行时可调⚠️ 需要创建新实例
// SDWebImage:完善的并发控制
let config = SDWebImageDownloader.shared.config
config.maxConcurrentDownloads = 4  // 动态调整
// config.executionOrder = .lifo   // 后进先出(列表场景推荐)

关键差异:SDWebImage 的并发控制更精细化,适合需要严格管理网络请求的场景。

5. GIF / 动图支持(SDWebImage 核心优势)
SDWebImageKingfisher
GIF 支持完整支持基础支持
帧率控制✅ 完整⚠️ 有限
内存策略✅ 专项优化⚠️ 表现不稳定
AnimatedImageViewSDAnimatedImageViewAnimatedImageView

源码对比

// 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 架构内存占用更低)

八、参考资源


九、本期互动


小作业

使用 SDWebImage 实现一个 并发可控的表情包列表

  1. 使用 UITableView 展示 GIF 列表
  2. 配置 maxConcurrentDownloads = 4,观察网络请求行为
  3. 每个 Cell 使用 SDAnimatedImageView 播放 GIF
  4. 实现上拉加载更多,并在加载时调整并发数(WiFi=8,蜂窝=4)
  5. 完成标准:滑动流畅,GIF 播放正常,Instruments 中内存不超过 150 MB

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

思考题

SDWebImage 通过 NSOperationQueue 实现并发控制,而 Kingfisher 使用自己的调度机制。如果让你自己实现一个并发控制的下载器,你会选择哪种方案?GCD 的 semaphoreOperationQueue、还是 Swift Concurrency 的 TaskGroup?各有什么优缺点?

读者征集

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

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


📅 本系列每周五晚更新
✅ 第6期:SnapKit · ➡️ 第7期:SDWebImage(本期) · ○ 第8期:待定