SDWebImage 源码

由浅入深,从基本概念到源码解析,带你全面掌握图片加载框架的设计与实现


一、什么是 SDWebImage?

1.1 为什么需要图片加载库?

在 iOS 开发中,展示网络图片是极其常见的需求。如果自己实现,需要处理:

  • 异步下载:不能阻塞主线程
  • 缓存策略:内存缓存 + 磁盘缓存,避免重复下载
  • 图片解码:在主线程解码大图会导致卡顿
  • 复用与取消:列表滑动时,旧请求应及时取消,避免错乱
  • 格式支持:JPEG、PNG、GIF、WebP 等

SDWebImage 将这些能力封装成一套成熟方案,被广泛应用于 App 中。

1.2 SDWebImage 简介

SDWebImage 是一个异步图片下载与缓存库,支持 iOS、macOS、watchOS、visionOS。核心特性包括:

特性 说明
异步下载 基于 NSURLSession,不阻塞主线程
内存 + 磁盘缓存 支持自定义缓存策略、过期时间
后台解码 避免主线程解码导致的卡顿
渐进式加载 支持 JPEG 等格式的渐进显示
动图支持 GIF、APNG、WebP 动画
缩略图解码 大图可只解码指定尺寸,节省内存
协议化设计 v5.x 起核心组件可插拔、可替换

1.3 版本演进要点

版本 主要变化
4.x Block 回调、FLAnimatedImageView
5.x 协议化:Loader、Cache、Coder 均可自定义;新增 View Indicator、Image Transform
5.6+ 完善协议体系,架构更清晰

二、核心架构与原理

2.1 整体数据流

一次完整的图片加载流程大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
用户调用 sd_setImageWithURL:


┌───────────────────────────────────────┐
│ UIView+WebCache (便捷入口) │
│ sd_internalSetImageWithURL:... │
└───────────────┬───────────────────────┘


┌───────────────────────────────────────┐
│ SDWebImageManager (调度中心) │
│ loadImageWithURL:options:... │
└───────────────┬───────────────────────┘

┌───────────┼───────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Cache │ │ Loader │ │ Coder │
│ 查找缓存 │ │ 下载图片 │ │ 解码图片 │
└─────────┘ └─────────┘ └─────────┘


显示到 UIImageView

2.2 协议化设计(v5.x 核心)

v5.x 将核心能力抽象为协议,实现可替换、可扩展:

协议 默认实现 职责
SDImageCache SDImageCache 内存 + 磁盘缓存
SDImageLoader SDWebImageDownloader 网络/本地图片加载
SDImageCoder SDImageCodersManager 图片编解码
SDWebImageCacheSerializer - 自定义缓存序列化
SDWebImageIndicator SDWebImageActivityIndicator 加载状态指示器

这种设计让开发者可以:

  • 替换默认下载器(如接入自研 CDN SDK)
  • 使用自定义缓存(如接入 YYCache、PINCache)
  • 支持新图片格式(实现 Coder 协议即可)

2.3 主线程检测的演进

SDWebImage 中使用 dispatch_main_async_safe 确保 UI 更新在主线程/主队列执行。v5.x 对主线程检测做了改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 旧版:用 [NSThread isMainThread] 判断
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) { block(); } else {\
dispatch_async(dispatch_get_main_queue(), block);\
}

// 新版:用 dispatch_queue 标签判断
#define dispatch_main_async_safe(block)\
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == \
dispatch_queue_get_label(dispatch_get_main_queue())) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}

原因:主队列 ≠ 主线程。某些场景下,非主队列的任务可能在主线程执行,若依赖 isMainThread,在依赖「主队列」的框架(如 VektorKit)中会出现问题。主队列上的任务一定在主线程执行,反之则需用队列标签判断。


三、源码解析

3.1 入口:UIView + WebCache

所有 View 的图片设置最终汇聚到 sd_internalSetImageWithURL:...

1
2
3
4
5
6
7
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;

核心步骤(精简):

  1. 取消旧任务sd_cancelImageLoadOperationWithKey:,避免同一 View 的多次请求冲突
  2. 设置占位图:立即显示 placeholder
  3. 调用 ManagerloadImageWithURL:options:context:progress:completed:
  4. 保存 Operation:将返回的 id<SDWebImageOperation> 存入 sd_operationDictionary,便于取消

sd_operationDictionary 使用 NSMapTable,key 强引用、value 弱引用,因为 Operation 由 Manager 的 runningOperations 持有,这里仅作取消用。

3.2 SDWebImageManager:调度中心

Manager 负责串联 Cache、Loader、Coder,核心逻辑在 loadImageWithURL:options:context:progress:completed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 生成缓存 Key
NSString *key = [self cacheKeyForURL:url context:context];

// 2. 先查缓存(可选跳过)
if (!(options & SDWebImageFromLoaderOnly)) {
[self callQueryCacheOperationForKey:key ... completed:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
if (cachedImage) {
// 命中缓存,直接回调
completedBlock(cachedImage, cachedData, nil, cacheType, YES, url);
return;
}
// 3. 未命中,执行下载
[self callLoadOperationWithURL:url ... completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
// 4. 下载完成,写入缓存并回调
if (image && (options & SDWebImageCacheMemoryOnly)) {
[self.imageCache storeImage:image forKey:key ...];
}
completedBlock(image, data, error, cacheType, finished, imageURL);
}];
}];
}

流程:查缓存 → 未命中则下载 → 下载完成写缓存 → 回调

3.3 SDWebImageDownloader:下载器

下载器负责发起网络请求,支持并发数、超时、Request/Response 修改等配置:

1
2
3
4
5
6
// 核心下载接口
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

URL 复用:同一 URL 的多次请求会复用同一个 SDWebImageDownloaderOperation,通过 addHandlersForProgress:completed: 累积多个回调,下载完成后依次执行,避免重复下载。

1
2
3
4
5
6
7
8
9
10
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
if (operation) {
@synchronized (operation) {
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}
} else {
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
[self.URLOperations setObject:operation forKey:url];
// ...
}

可插拔扩展

  • SDWebImageDownloaderRequestModifier:修改 Request(如加 Header)
  • SDWebImageDownloaderResponseModifier:修改 Response(如校验 MIME-Type)
  • SDWebImageDownloaderDecryptor:解密(如 Base64)

3.4 SDImageCache:缓存

缓存采用内存 + 磁盘二层结构:

1
2
3
4
5
6
7
8
9
10
11
12
// 查询缓存
- (void)queryImageForKey:(NSString *)key
options:(SDImageCacheOptions)options
context:(nullable SDWebImageContext *)context
completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock;

// 存储
- (void)storeImage:(UIImage *)image
imageData:(NSData *)imageData
forKey:(NSString *)key
cacheType:(SDImageCacheType)cacheType
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
  • 内存缓存:基于 NSCache,受内存压力和系统策略自动回收
  • 磁盘缓存:默认使用 NSFileManager 存储到 Library/Caches/default,可配置自定义路径

缓存 Key 默认为 URL 的绝对字符串,可通过 SDWebImageContextcacheKeyFilter 自定义。

3.5 解码与变换

为避免主线程解码导致卡顿,SDWebImage 在后台队列解码:

1
2
3
// 解码在 SDImageIOCoder / SDImageGIFCoder 等中完成
// 通过 SDImageCoder 协议统一
- (UIImage *)decodedImageWithData:(NSData *)data options:(SDImageCoderOptions *)options;

v5.x 支持 Image Transform:下载后可对图片做缩放、旋转、圆角等处理,结果再缓存,避免重复计算。


四、示例

4.1 基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <SDWebImage/SDWebImage.h>

// 最简单的用法
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"https://example.com/photo.jpg"]];

// 带占位图和完成回调
[self.imageView sd_setImageWithURL:url
placeholderImage:[UIImage imageNamed:@"placeholder"]
completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
if (error) {
NSLog(@"加载失败: %@", error);
} else {
NSLog(@"加载成功,来源: %@", cacheType == SDImageCacheTypeMemory ? @"内存" :
cacheType == SDImageCacheTypeDisk ? @"磁盘" : @"网络");
}
}];

4.2 进度与选项

1
2
3
4
5
6
7
[self.imageView sd_setImageWithURL:url
placeholderImage:placeholder
options:SDWebImageRetryFailed | SDWebImageProgressiveLoad
progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
CGFloat progress = expectedSize > 0 ? (CGFloat)receivedSize / expectedSize : 0;
self.progressView.progress = progress;
} completed:nil];

常用 options

  • SDWebImageRetryFailed:失败后重试
  • SDWebImageProgressiveLoad:渐进式加载
  • SDWebImageRefreshCached:忽略缓存强制刷新
  • SDWebImageFromLoaderOnly:只从网络加载,不查缓存

4.3 预加载

1
2
3
4
5
6
7
SDWebImagePrefetcher *prefetcher = [SDWebImagePrefetcher sharedImagePrefetcher];
prefetcher.maxConcurrentPrefetches = 4;
[prefetcher prefetchURLs:imageURLs progress:^(NSUInteger no, NSUInteger total) {
NSLog(@"预加载进度: %lu/%lu", (unsigned long)no, (unsigned long)total);
} completed:^(NSUInteger finishedCount, NSUInteger skippedCount) {
NSLog(@"完成: %lu, 跳过: %lu", (unsigned long)finishedCount, (unsigned long)skippedCount);
}];

4.4 自定义缓存 Key

1
2
3
4
5
6
SDWebImageContext *context = @{
SDWebImageContextCacheKeyFilter: [SDWebImageCacheKeyFilter cacheKeyFilterWithBlock:^NSString * _Nullable(NSURL * _Nullable url) {
return [url.absoluteString stringByAppendingString:@"_suffix"]; // 自定义 key
}]
};
[self.imageView sd_setImageWithURL:url placeholderImage:nil context:context];

4.5 图片变换(圆角、缩放)

1
2
3
4
[self.imageView sd_setImageWithURL:url
placeholderImage:placeholder
options:0
context:@{SDWebImageContextImageTransformer: [SDImageRoundCornerTransformer transformerWithRadius:10 corners:UIRectCornerAllCorners borderWidth:1 borderColor:[UIColor whiteColor]]}];

五、实际项目中的应用案例

5.1 列表图片加载与复用

场景:TableView/CollectionView 中加载头像或商品图。

做法

  • 使用 sd_setImageWithURL: 即可,SDWebImage 会自动取消不可见 cell 的请求
  • 可选 SDWebImageAvoidAutoSetImage,在 completed 中手动设置,便于加入过渡动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCell"];
NSURL *avatarURL = self.avatars[indexPath.row];
[cell.avatarView sd_setImageWithURL:avatarURL
placeholderImage:[UIImage imageNamed:@"default_avatar"]
options:SDWebImageAvoidAutoSetImage
completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
if (image) {
[UIView transitionWithView:cell.avatarView duration:0.2 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
cell.avatarView.image = image;
} completion:nil];
}
}];
return cell;
}

5.2 详情页大图预加载

场景:从列表进入详情时,希望大图尽快展示。

做法:在列表滑动到某条数据时,对详情大图 URL 做预加载:

1
2
3
4
5
6
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *detailImageURL = self.detailImageURLs[indexPath.row];
[[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:@[[NSURL URLWithString:detailImageURL]]];
// 再 push 到详情页
[self.navigationController pushViewController:detailVC animated:YES];
}

5.3 统一添加请求头(如 Token)

场景:图片 URL 需要鉴权 Header。

做法:实现 SDWebImageDownloaderRequestModifier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface MyRequestModifier : NSObject <SDWebImageDownloaderRequestModifier>
@end

@implementation MyRequestModifier
- (NSURLRequest *)modifiedRequestWithRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutable = [request mutableCopy];
[mutable setValue:[MyAuthManager shared].token forHTTPHeaderField:@"Authorization"];
return [mutable copy];
}
@end

// 配置
SDWebImageDownloader *downloader = [SDWebImageManager sharedManager].imageLoader;
downloader.requestModifier = [[MyRequestModifier alloc] init];

5.4 加载状态指示器

场景:图片加载时显示 UIActivityIndicator。

1
2
self.imageView.sd_imageIndicator = SDWebImageActivityIndicator.grayIndicator;
[self.imageView sd_setImageWithURL:url];

或自定义实现 SDWebImageIndicator 协议,适配项目 UI 规范。

5.5 自定义缓存路径与策略

场景:头像与普通图片使用不同缓存目录和过期策略。

1
2
3
4
5
6
7
// 头像缓存:30 天
SDImageCache *avatarCache = [[SDImageCache alloc] initWithNamespace:@"avatar" diskCacheDirectory:[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"AvatarCache"]]];
avatarCache.config.maxDiskAge = 30 * 24 * 60 * 60;

// 通过 Context 指定使用 avatarCache
SDWebImageContext *context = @{SDWebImageContextCustomCache: avatarCache};
[self.avatarView sd_setImageWithURL:avatarURL placeholderImage:nil context:context];

六、小结

SDWebImage 通过协议化设计清晰的职责划分,将图片加载、缓存、解码、展示等环节解耦,既易用又易扩展。掌握其架构与源码,有助于:

  • 合理选用 optionscontext,优化体验与性能
  • 在需要时自定义 Loader、Cache、Coder,适配业务
  • 理解异步加载、缓存、取消等通用模式,迁移到其他场景

建议结合官方 GitHubWiki 阅读源码,逐层从 Category → Manager → Loader/Cache 跟踪调用链,会有更深理解。

Author

Felix Tao

Posted on

2018-02-03

Updated on

2020-05-22

Licensed under