由浅入深,从基本概念到源码解析,带你全面掌握图片加载框架的设计与实现
一、什么是 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
| #define dispatch_main_async_safe(block)\ if ([NSThread isMainThread]) { block(); } else {\ dispatch_async(dispatch_get_main_queue(), block);\ }
#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;
|
核心步骤(精简):
- 取消旧任务:
sd_cancelImageLoadOperationWithKey:,避免同一 View 的多次请求冲突
- 设置占位图:立即显示 placeholder
- 调用 Manager:
loadImageWithURL:options:context:progress:completed:
- 保存 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
| NSString *key = [self cacheKeyForURL:url context:context];
if (!(options & SDWebImageFromLoaderOnly)) { [self callQueryCacheOperationForKey:key ... completed:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) { if (cachedImage) { completedBlock(cachedImage, cachedData, nil, cacheType, YES, url); return; } [self callLoadOperationWithURL:url ... completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { 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 的绝对字符串,可通过 SDWebImageContext 的 cacheKeyFilter 自定义。
3.5 解码与变换
为避免主线程解码导致卡顿,SDWebImage 在后台队列解码:
1 2 3
|
- (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"]; }] }; [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]]]; [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
| SDImageCache *avatarCache = [[SDImageCache alloc] initWithNamespace:@"avatar" diskCacheDirectory:[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"AvatarCache"]]]; avatarCache.config.maxDiskAge = 30 * 24 * 60 * 60;
SDWebImageContext *context = @{SDWebImageContextCustomCache: avatarCache}; [self.avatarView sd_setImageWithURL:avatarURL placeholderImage:nil context:context];
|
六、小结
SDWebImage 通过协议化设计和清晰的职责划分,将图片加载、缓存、解码、展示等环节解耦,既易用又易扩展。掌握其架构与源码,有助于:
- 合理选用
options 与 context,优化体验与性能
- 在需要时自定义 Loader、Cache、Coder,适配业务
- 理解异步加载、缓存、取消等通用模式,迁移到其他场景
建议结合官方 GitHub 与 Wiki 阅读源码,逐层从 Category → Manager → Loader/Cache 跟踪调用链,会有更深理解。