李峰峰博客

SDWebImage 实现分析

2019-10-03

一、概述

SDWebImage 是 iOS 开发中比较常用的一个开源库,支持加载网络图片到 UIImageView / UIButton 等 UI 控件上。

其使用方式也比较简单,以 UIImageView 加载网络图片为例:

1
[self.imageView sd_setImageWithURL:self.imageURL placeholderImage:nil];

SDWebImage(3.8.3) 核心类可以按照下图进行分层:

各层职责:

  • UIImageView+WebCache / UIButton+WebCache
    • 提供给开发者使用的接口,简化图像加载的调用。
  • SDWebImageManager
    • 协调下载、缓存等操作,是图像加载的核心调度层。
  • SDWebImageDownloader / SDWebImageDownloaderOperation
    • 负责从网络上下载图像,处理下载进度和完成回调。
  • SDImageCache
    • 负责管理图像的内存缓存和磁盘缓存,提高图像加载的效率。
  • SDWebImageDecoder
    • 将下载的图像数据解码为可以直接使用的 UIImage 对象。
  • SDWebImageCompat / SDWebImagePrefetcher
    • 提供一些辅助功能和工具类,增强框架的功能性和灵活性。

SDWebImage 在 GitHub 中给出的时序图如下:

二、源码解读

1、UIImageView+WebCache/UIButton+WebCache

UIImageView 加载网络图片为例:

1
[self.imageView sd_setImageWithURL:self.imageURL placeholderImage:nil];

该方法最终调用的是如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock {
// 取消当前图像加载操作
[self sd_cancelCurrentImageLoad];
// 将 URL 与 UIImageView 关联起来
objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

// 如果选项中不包含 SDWebImageDelayPlaceholder,则立即设置占位图像
if (!(options & SDWebImageDelayPlaceholder)) {
dispatch_main_async_safe(^{
self.image = placeholder;
});
}

// 检查 URL 是否存在
if (url) {

// 检查是否启用了活动指示器,如果启用则添加活动指示器
if ([self showActivityIndicatorView]) {
[self addActivityIndicator];
}

__weak __typeof(self) wself = self;
// 使用 SDWebImageManager 下载图像
id <SDWebImageOperation> operation = [SDWebImageManager.sharedManager downloadImageWithURL:url options:options progress:progressBlock completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
// 移除活动指示器
[wself removeActivityIndicator];
if (!wself) return;
dispatch_main_sync_safe(^{
if (!wself) return;
// 如果下载成功且包含 SDWebImageAvoidAutoSetImage 选项,则调用完成回调并返回
if (image && (options & SDWebImageAvoidAutoSetImage) && completedBlock) {
completedBlock(image, error, cacheType, url);
return;
} else if (image) {
// 如果下载成功,则设置图像并更新布局
wself.image = image;
[wself setNeedsLayout];
} else {
// 如果下载失败且包含 SDWebImageDelayPlaceholder 选项,则设置占位图像并更新布局
if ((options & SDWebImageDelayPlaceholder)) {
wself.image = placeholder;
[wself setNeedsLayout];
}
}
// 如果完成回调存在且下载完成,则调用完成回调
if (completedBlock && finished) {
completedBlock(image, error, cacheType, url);
}
});
}];
// 将下载操作与 UIImageView 关联起来
[self sd_setImageLoadOperation:operation forKey:@"UIImageViewImageLoad"];
} else {
// 如果 URL 为空,立即调用完成回调并返回错误
dispatch_main_async_safe(^{
[self removeActivityIndicator];
if (completedBlock) {
NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:-1 userInfo:@{NSLocalizedDescriptionKey : @"Trying to load a nil url"}];
completedBlock(nil, error, SDImageCacheTypeNone, url);
}
});
}
}

该方法主要逻辑如下:

  • 取消当前正在进行的图片加载
    • 调用 sd_cancelCurrentImageLoad 方法,确保当前没有正在进行的图片加载操作
  • 设置占位图
    • 在主线程中设置占位图
  • 图片下载
    • 通过 SDWebImageManager 单例,执行图片下载的操作
  • 图片下载完成
    • 将下载完成的 UIImage 设置到当前 UIImageView

2、SDWebImageManager

SDWebImageManager 是个单例,主要负责协调下载、缓存等操作,是图像加载的核心调度层,其中持有了 SDImageCacheSDWebImageDownloader,且两者都是单例:

1
2
3
4
5
6
7
8
9
10
@interface SDWebImageManager : NSObject

@property (weak, nonatomic) id <SDWebImageManagerDelegate> delegate;

@property (strong, nonatomic, readonly) SDImageCache *imageCache;
@property (strong, nonatomic, readonly) SDWebImageDownloader *imageDownloader;

// ...

@end

SDWebImageManager 还有个 delegate 属性,SDWebImageManagerDelegate 声明了两个可选实现的方法:

1
2
3
4
5
// 该方法用于控制在缓存未命中时,是否应该下载 该 URL 对应的图片
- (BOOL)imageManager:(SDWebImageManager *)imageManager shouldDownloadImageForURL:(NSURL *)imageURL;

// 该方法允许在图片下载完成后,缓存到磁盘和内存之前对图片进行转换
- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL;

前述的图片下载通过 SDWebImageManager 单例中的如下方法实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url
options:(SDWebImageOptions)options
progress:(SDWebImageDownloaderProgressBlock)progressBlock
completed:(SDWebImageCompletionWithFinishedBlock)completedBlock {
// ...

// 根据 URL 生成缓存 Key
NSString *key = [self cacheKeyForURL:url];

// 检查缓存是否存在
operation.cacheOperation = [self.imageCache queryDiskCacheForKey:key done:^(UIImage *image, SDImageCacheType cacheType) {

// ...

// 如果缓存中不存在或需要刷新,并且 delegate 允许下载
if ((!image || options & SDWebImageRefreshCached) && (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url])) {
// 如果缓存中存在且需要刷新,则先返回缓存的图片
if (image && options & SDWebImageRefreshCached) {
dispatch_main_sync_safe(^{
// 在主线程先回调缓存的图片
completedBlock(image, nil, cacheType, YES, url);
});
}


// ...

// 开始下载图片
id <SDWebImageOperation> subOperation = [self.imageDownloader downloadImageWithURL:url options:downloaderOptions progress:progressBlock completed:^(UIImage *downloadedImage, NSData *data, NSError *error, BOOL finished) {
__strong __typeof(weakOperation) strongOperation = weakOperation;
if (!strongOperation || strongOperation.isCancelled) {

} else if (error) {

// ...

} else {

// ...

BOOL cacheOnDisk = !(options & SDWebImageCacheMemoryOnly);

// 如果 delegate 想要转换图片,则先执行 delegate 的图片转换逻辑
if (downloadedImage && (!downloadedImage.images || (options & SDWebImageTransformAnimatedImage)) && [self.delegate respondsToSelector:@selector(imageManager:transformDownloadedImage:withURL:)]) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
UIImage *transformedImage = [self.delegate imageManager:self transformDownloadedImage:downloadedImage withURL:url];

if (transformedImage && finished) {
BOOL imageWasTransformed = ![transformedImage isEqual:downloadedImage];
// 存储图片到缓存
[self.imageCache storeImage:transformedImage recalculateFromImage:imageWasTransformed imageData:(imageWasTransformed ? nil : data) forKey:key toDisk:cacheOnDisk];
}

dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
// 回调 UIImage
completedBlock(transformedImage, nil, SDImageCacheTypeNone, finished, url);
}
});
});
} else {
// 如果 delegate 不需要转换图片
if (downloadedImage && finished) {
// 存储到缓存
[self.imageCache storeImage:downloadedImage recalculateFromImage:NO imageData:data forKey:key toDisk:cacheOnDisk];
}

dispatch_main_sync_safe(^{
if (strongOperation && !strongOperation.isCancelled) {
// 回调 UIImage
completedBlock(downloadedImage, nil, SDImageCacheTypeNone, finished, url);
}
});

}
}


}];

// ...

}

// ...

}];

return operation;
}

该方法主要逻辑如下:

  • 使用 URL 生成缓存 Key
    • [url absoluteString] 作为 key
  • 使用缓存 KeySDImageCache 中读取缓存
    • 如果缓存存在,先将缓存的 UIImage 回调出去
  • 判断是否需要下载
    • 如果缓存中不存在图片或需要刷新,并且 delegate 允许下载图片,则通过 SDWebImageDownloader 下载,下载完成后更新到缓存并回调
    • 如果缓存中存在图片且需要刷新,则先返回缓存中的图片,然后继续通过 SDWebImageDownloader 下载以刷新图片,下载完成后更新到缓存并回调
    • 如果缓存中不存在图片且不允许下载,则空回调

3、SDImageCache

(1)缓存的查找

SDImageCache 中,缓存读取方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
- (NSOperation *)queryDiskCacheForKey:(NSString *)key done:(SDWebImageQueryCompletedBlock)doneBlock {
if (!doneBlock) {
return nil;
}

if (!key) {
doneBlock(nil, SDImageCacheTypeNone);
return nil;
}

// 先从内存中查找缓存
UIImage *image = [self imageFromMemoryCacheForKey:key];
if (image) {
// 如果内存缓存中存在图片,则回调内存中图片,缓存类型为 SDImageCacheTypeMemory
doneBlock(image, SDImageCacheTypeMemory);
return nil;
}


// 再查找磁盘缓存
NSOperation *operation = [NSOperation new];
// 异步执行
dispatch_async(self.ioQueue, ^{
// 如果操作被取消,则直接返回
if (operation.isCancelled) {
return;
}

@autoreleasepool {
// 从磁盘缓存中获取图片
UIImage *diskImage = [self diskImageForKey:key];

if (diskImage && self.shouldCacheImagesInMemory) {
// 计算图片的缓存大小
NSUInteger cost = SDCacheCostForImage(diskImage);
// 将图片存储到内存缓存中
[self.memCache setObject:diskImage forKey:key cost:cost];
}

// 在主线程回调,返回图片和缓存类型为 SDImageCacheTypeDisk
dispatch_async(dispatch_get_main_queue(), ^{
doneBlock(diskImage, SDImageCacheTypeDisk);
});
}
});

return operation;
}

该方法主要逻辑如下:

  • 先从内存中查找缓存
    • 内存中如果存在缓存,则回调内存中的缓存
    • 内存中不存在缓存,则从磁盘查找缓存
    • 内存缓存使用 NSCache 存储
      • 可以通过 SDImageCache 单例的 maxMemoryCountLimit 属性设置最大存储图片数量,默认为 0,即无限制。
      • 可以通过 SDImageCache 单例的 maxMemoryCost 属性设置最大存储图片内存大小,默认为 0,即无限制,系统内存不足时系统自动清理。
  • 再从磁盘中查找缓存
    • 异步查找缓存
    • 从磁盘查到缓存后,将缓存写入到内存缓存中一份
    • 磁盘缓存使用 NSFileManager 实现,缓存到 Library/Caches
      • 可以通过 SDImageCache 单例的 maxCacheAge 属性设置缓存有效期,默认有效期为 7 天。
      • 可以通过 SDImageCache 单例的 maxCacheSize 属性设置最大存储图片内存大小,默认为 0,即无限制。

(2)磁盘缓存的清理

SDImageCache 实例初始化时,注册了三个通知用于缓存的清理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (id)initWithNamespace:(NSString *)ns diskCacheDirectory:(NSString *)directory {
if ((self = [super init])) {

// ...

// 内存警告时清理内存缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(clearMemory)
name:UIApplicationDidReceiveMemoryWarningNotification
object:nil];

// 应用即将终止时清理磁盘缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(cleanDisk)
name:UIApplicationWillTerminateNotification
object:nil];

// 应用进入后台创建后台任务清理缓存
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(backgroundCleanDisk)
name:UIApplicationDidEnterBackgroundNotification
object:nil];
}

return self;
}

即缓存清理时机如下:

  • 内存警告时清理内存缓存
  • 应用即将终止时清理磁盘缓存
  • 应用进入后台创建后台任务清理缓存

磁盘清理具体逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
- (void)cleanDiskWithCompletionBlock:(SDWebImageNoParamsBlock)completionBlock {
// 在后台线程中执行磁盘清理操作
dispatch_async(self.ioQueue, ^{
// 获取磁盘缓存目录的 URL
NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES];
// 需要预取的文件属性键
NSArray *resourceKeys = @[NSURLIsDirectoryKey, NSURLContentModificationDateKey, NSURLTotalFileAllocatedSizeKey];

// 创建目录枚举器,预取缓存文件的属性
NSDirectoryEnumerator *fileEnumerator = [self->_fileManager enumeratorAtURL:diskCacheURL
includingPropertiesForKeys:resourceKeys
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:NULL];

// 计算过期日期
NSDate *expirationDate = [NSDate dateWithTimeIntervalSinceNow:-self.maxCacheAge];
NSMutableDictionary *cacheFiles = [NSMutableDictionary dictionary];
NSUInteger currentCacheSize = 0;

// 枚举缓存目录中的所有文件。这个循环有两个目的:
//
// 1. 删除超过过期日期的文件。
// 2. 存储文件属性以便后续基于大小的清理。
NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init];
for (NSURL *fileURL in fileEnumerator) {
NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:NULL];

// 跳过目录
if ([resourceValues[NSURLIsDirectoryKey] boolValue]) {
continue;
}

// 删除超过过期日期的文件
NSDate *modificationDate = resourceValues[NSURLContentModificationDateKey];
if ([[modificationDate laterDate:expirationDate] isEqualToDate:expirationDate]) {
[urlsToDelete addObject:fileURL];
continue;
}

// 存储文件引用并记录其总大小
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize += [totalAllocatedSize unsignedIntegerValue];
[cacheFiles setObject:resourceValues forKey:fileURL];
}

// 删除过期文件
for (NSURL *fileURL in urlsToDelete) {
[self->_fileManager removeItemAtURL:fileURL error:nil];
}

// 如果剩余的磁盘缓存超过了配置的最大大小,则执行基于大小的清理
if (self.maxCacheSize > 0 && currentCacheSize > self.maxCacheSize) {
// 目标是将缓存大小减少到最大缓存大小的一半
const NSUInteger desiredCacheSize = self.maxCacheSize / 2;

// 按文件的最后修改时间排序(最旧的文件排在前面)
NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent
usingComparator:^NSComparisonResult(id obj1, id obj2) {
return [obj1[NSURLContentModificationDateKey] compare:obj2[NSURLContentModificationDateKey]];
}];

// 删除文件直到缓存大小降到目标大小以下
for (NSURL *fileURL in sortedFiles) {
if ([self->_fileManager removeItemAtURL:fileURL error:nil]) {
NSDictionary *resourceValues = cacheFiles[fileURL];
NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey];
currentCacheSize -= [totalAllocatedSize unsignedIntegerValue];

if (currentCacheSize < desiredCacheSize) {
break;
}
}
}
}

// 在主线程中回调
if (completionBlock) {
dispatch_async(dispatch_get_main_queue(), ^{
completionBlock();
});
}
});
}

该方法主要逻辑如下:

  • 按有效期 maxCacheAge 清理过期缓存
  • 按大存储内存 maxCacheSize 清理缓存
    • 按照文件最后修改时间的逆序,移除过早的缓存,仅保留 maxCacheSize 一半大小的缓存

(3)缓存的写入

SDWebImageManager 中,图片下载完成后会调用 SDImageCache 的如下方法将图片写入到缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
- (void)storeImage:(UIImage *)image recalculateFromImage:(BOOL)recalculate imageData:(NSData *)imageData forKey:(NSString *)key toDisk:(BOOL)toDisk {
// 检查图片和键是否有效
if (!image || !key) {
return;
}

// 如果启用了内存缓存
if (self.shouldCacheImagesInMemory) {
// 计算图片的缓存成本
NSUInteger cost = SDCacheCostForImage(image);
// 将图片存储到内存缓存中
[self.memCache setObject:image forKey:key cost:cost];
}

// 如果需要存储到磁盘
if (toDisk) {
// 异步执行磁盘存储操作
dispatch_async(self.ioQueue, ^{
NSData *data = imageData;

if (image && (recalculate || !data)) {
#if TARGET_OS_IPHONE
// 检查图片是否具有 Alpha 通道
int alphaInfo = CGImageGetAlphaInfo(image.CGImage);
BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone ||
alphaInfo == kCGImageAlphaNoneSkipFirst ||
alphaInfo == kCGImageAlphaNoneSkipLast);
BOOL imageIsPng = hasAlpha;

// 如果图片数据存在,则检查其前缀以确定是否为 PNG
if ([imageData length] >= [kPNGSignatureData length]) {
imageIsPng = ImageDataHasPNGPreffix(imageData);
}

// 根据图片类型生成相应的 NSData
if (imageIsPng) {
data = UIImagePNGRepresentation(image);
} else {
data = UIImageJPEGRepresentation(image, (CGFloat)1.0);
}
#else
// 在 macOS 上,将图片表示转换为 JPEG 数据
data = [NSBitmapImageRep representationOfImageRepsInArray:image.representations usingType: NSJPEGFileType properties:nil];
#endif
}

// 将图片数据存储到磁盘
[self storeImageDataToDisk:data forKey:key];
});
}
}

该方法主要逻辑如下:

  • 先存到内存缓存
    • 先检查确认启用了内存缓存,再存储到内存缓存
  • 再存到磁盘缓存(异步)
    • 先检查图片是否是 PNG
      • 先通过 CGImageGetAlphaInfo 获取 UIImageAlpha 通道信息,推测是否是 PNG
        • PNG 格式支持透明度,因此具有 Alpha 通道的图像通常被认为是 PNG
        • 但这并不排除其他格式(如 TIFF 或 WebP)也可能支持透明度
      • 如果 imageData 存在,则检查其 NSData 前缀以确定是否为 PNG 格式
        • PNG 文件具有独特的签名(前八个字节),通过检查这些字节可以准确判断图像是否为 PNG 格式
        • 根据是否是 PNG,决定是通过 UIImagePNGRepresentation 还是 UIImageJPEGRepresentationUIImage 转成 NSData
    • 使用 NSFileManager 将上一步得到的 NSData 写入沙盒(Library/Caches

4、SDWebImageDownloader/SDWebImageDownloaderOperation

SDWebImageDownloaderSDWebImageDownloaderOperation 共同协作来实现图片的下载。

SDWebImageDownloader 是一个负责管理图片下载任务的类。它提供了一个全局的入口来启动、取消和管理图片下载请求。主要职责包括:

  • 管理下载队列
    • SDWebImageDownloader 维护一个下载队列,用于管理所有的下载任务。
  • 配置下载选项、
    • 可以配置下载的并发数、超时时间、缓存策略等。
  • 处理下载回调
    • 提供下载进度、完成、失败等回调接口。
  • 创建下载操作
    • 为每个下载请求创建一个 SDWebImageDownloaderOperation 实例,并将其添加到下载队列中。

SDWebImageDownloaderOperation 是具体执行图片下载任务的类。它继承自 NSOperation,因此可以利用 NSOperationQueue 来管理并发下载。主要职责包括:

  • 执行下载任务
    • 具体的图片下载逻辑在这个类中实现,包括发起网络请求、处理响应数据等。
  • 管理下载状态
    • 维护下载任务的状态,如正在进行、已完成、已取消等。
  • 处理下载回调
    • 在下载过程中,通过回调将进度、完成、失败等信息传递给 SDWebImageDownloader

SDWebImageManager 中调用了 SDWebImageDownloader 的如下方法进行图片的下载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
- (id <SDWebImageOperation>)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock {
__block SDWebImageDownloaderOperation *operation;
__weak __typeof(self)wself = self;

// 添加进度和完成回调,并创建下载操作
[self addProgressCallback:progressBlock completedBlock:completedBlock forURL:url createCallback:^{
NSTimeInterval timeoutInterval = wself.downloadTimeout;
if (timeoutInterval == 0.0) {
timeoutInterval = 15.0;
}

// 创建请求,配置缓存策略和超时时间
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:(options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData) timeoutInterval:timeoutInterval];
request.HTTPShouldHandleCookies = (options & SDWebImageDownloaderHandleCookies);
request.HTTPShouldUsePipelining = YES;

// 配置 HTTP 头字段
if (wself.headersFilter) {
request.allHTTPHeaderFields = wself.headersFilter(url, [wself.HTTPHeaders copy]);
} else {
request.allHTTPHeaderFields = wself.HTTPHeaders;
}

// 初始化下载操作
operation = [[wself.operationClass alloc] initWithRequest:request
inSession:self.session
options:options
progress:^(NSInteger receivedSize, NSInteger expectedSize) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
});
for (NSDictionary *callbacks in callbacksForURL) {
dispatch_async(dispatch_get_main_queue(), ^{
SDWebImageDownloaderProgressBlock callback = callbacks[kProgressCallbackKey];
if (callback) callback(receivedSize, expectedSize);
});
}
}
completed:^(UIImage *image, NSData *data, NSError *error, BOOL finished) {
SDWebImageDownloader *sself = wself;
if (!sself) return;
__block NSArray *callbacksForURL;
dispatch_barrier_sync(sself.barrierQueue, ^{
callbacksForURL = [sself.URLCallbacks[url] copy];
if (finished) {
[sself.URLCallbacks removeObjectForKey:url];
}
});
for (NSDictionary *callbacks in callbacksForURL) {
SDWebImageDownloaderCompletedBlock callback = callbacks[kCompletedCallbackKey];
if (callback) callback(image, data, error, finished);
}
}
cancelled:^{
SDWebImageDownloader *sself = wself;
if (!sself) return;
dispatch_barrier_async(sself.barrierQueue, ^{
[sself.URLCallbacks removeObjectForKey:url];
});
}];
operation.shouldDecompressImages = wself.shouldDecompressImages;

// 配置认证信息
if (wself.urlCredential) {
operation.credential = wself.urlCredential;
} else if (wself.username && wself.password) {
operation.credential = [NSURLCredential credentialWithUser:wself.username password:wself.password persistence:NSURLCredentialPersistenceForSession];
}

// 配置操作优先级
if (options & SDWebImageDownloaderHighPriority) {
operation.queuePriority = NSOperationQueuePriorityHigh;
} else if (options & SDWebImageDownloaderLowPriority) {
operation.queuePriority = NSOperationQueuePriorityLow;
}

// 将操作添加到下载队列
[wself.downloadQueue addOperation:operation];
if (wself.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) {
// 模拟 LIFO 执行顺序,通过将新操作添加为最后一个操作的依赖项
[wself.lastAddedOperation addDependency:operation];
wself.lastAddedOperation = operation;
}
}];

return operation;
}


- (void)addProgressCallback:(SDWebImageDownloaderProgressBlock)progressBlock completedBlock:(SDWebImageDownloaderCompletedBlock)completedBlock forURL:(NSURL *)url createCallback:(SDWebImageNoParamsBlock)createCallback {
// URL 将用作回调字典的键,因此不能为 nil。如果为 nil,立即调用完成回调并返回。
if (url == nil) {
if (completedBlock != nil) {
completedBlock(nil, nil, nil, NO);
}
return;
}

// 使用 dispatch_barrier_sync 确保对 URLCallbacks 字典的访问是线程安全的
dispatch_barrier_sync(self.barrierQueue, ^{
BOOL first = NO;
// 如果 URLCallbacks 字典中没有该 URL,则创建一个新的回调数组,并标记为第一次请求
if (!self.URLCallbacks[url]) {
self.URLCallbacks[url] = [NSMutableArray new];
first = YES;
}

// 处理同一 URL 的多个同时下载请求
NSMutableArray *callbacksForURL = self.URLCallbacks[url];
NSMutableDictionary *callbacks = [NSMutableDictionary new];
if (progressBlock) callbacks[kProgressCallbackKey] = [progressBlock copy];
if (completedBlock) callbacks[kCompletedCallbackKey] = [completedBlock copy];
[callbacksForURL addObject:callbacks];
self.URLCallbacks[url] = callbacksForURL;

// 如果是第一次请求该 URL,则调用 createCallback 创建下载操作
if (first) {
createCallback();
}
});
}

在该方法中,可以看出是使用 NSURLSession 实现图片的下载。

其中,下载回调是保存在 NSMutableDictionary 中:

  • keyURL
  • value 是一个数组,数组元素为字典,字典中保存了 progressBlockcompletedBlock 两个回调

针对每一个 URL,只会在这个 URL 首次下载时才会真正执行下载,其余的只会将回调保存到字典中。在对应阶段,使用 URL 取出保存的回调数组,遍历执行回调,并使用 dispatch_barrier_sync 确保线程安全。

5、SDWebImageDecoder

SDWebImageDecoder 主要负责图片的解码和解压缩,SDWebImageDecoder 中只提供了如下一个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
+ (UIImage *)decodedImageWithImage:(UIImage *)image {
// 下载大量图片时,使用 autorelease 释放位图上下文和所有变量,帮助系统在内存警告时释放内存。
// 在 iOS7 上,不要忘记调用 [[SDImageCache sharedImageCache] clearMemory];

// 防止 "CGBitmapContextCreateImage: invalid context 0x0" 错误
if (image == nil) {
return nil;
}

@autoreleasepool {
// 不解码动画图片
if (image.images != nil) {
return image;
}

CGImageRef imageRef = image.CGImage;

// 获取图像的 alpha 信息
CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);
// 如果图像包含 alpha 通道,直接返回原图
if (anyAlpha) {
return image;
}

// 获取当前图像的颜色空间模型和颜色空间引用
CGColorSpaceModel imageColorSpaceModel = CGColorSpaceGetModel(CGImageGetColorSpace(imageRef));
CGColorSpaceRef colorspaceRef = CGImageGetColorSpace(imageRef);

// 检查是否为不支持的颜色空间
BOOL unsupportedColorSpace = (imageColorSpaceModel == kCGColorSpaceModelUnknown ||
imageColorSpaceModel == kCGColorSpaceModelMonochrome ||
imageColorSpaceModel == kCGColorSpaceModelCMYK ||
imageColorSpaceModel == kCGColorSpaceModelIndexed);
// 如果是不支持的颜色空间,则创建一个 RGB 颜色空间
if (unsupportedColorSpace) {
colorspaceRef = CGColorSpaceCreateDeviceRGB();
}

size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
NSUInteger bytesPerPixel = 4;
NSUInteger bytesPerRow = bytesPerPixel * width;
NSUInteger bitsPerComponent = 8;

// kCGImageAlphaNone 在 CGBitmapContextCreate 中不支持。
// 由于原始图像没有 alpha 信息,使用 kCGImageAlphaNoneSkipLast 创建没有 alpha 信息的位图图形上下文。
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
bitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast);

// 将图像绘制到上下文中,并检索没有 alpha 通道的新位图图像
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];

// 如果创建了新的颜色空间,则释放它
if (unsupportedColorSpace) {
CGColorSpaceRelease(colorspaceRef);
}

// 释放上下文和图像引用
CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);

return imageWithoutAlpha;
}
}

decodedImageWithImage: 方法主要是通过 CGBitmapContextCreate 进行图片的解压缩,该方法均是在子线程被调用。

decodedImageWithImage: 方法调用时机如下:

  • SDWebImageDownloaderOperation 中图片下载完成后
  • SDImageCache 中从磁盘读取缓存后

decodedImageWithImage: 方法的入参已经是 UIImage 了,为什么还需要对 UIImage 进行解压缩呢?

UIImage 展示到 UIImageView 上,要经过以下流程:

  • UIImage 赋值给 UIImageView
    • UIImage 对象赋值给 UIImageView,准备显示图像。
  • 图片解码(CPU
    • 解码即解压缩,将压缩的图片数据(如 JPEG、PNG)解码为未压缩的位图数据。
    • 解码是一个非常耗时的 CPU 操作,默认情况下在主线程中执行。
  • 图片绘制(GPU
    • 解码后的位图数据提交给 GPU 进行渲染。
    • GPU 负责将位图数据渲染到帧缓冲区。
  • 显示到屏幕(GPU
    • GPU 渲染完成后,将渲染后的位图放到帧缓冲区。
    • 帧缓冲区的内容被视频控制器读取并显示到屏幕上。

decodedImageWithImage: 的作用就是进行 UIImage 的解压缩操作,在图片下载完成或从磁盘缓存读取后,先利用 CGBitmapContextCreate 在子线程提前对 UIImage 进行解压缩,这样就避免了主线程的解压缩的操作,加快了图片的显示速度和流畅度,尤其是在快速滑动的列表中。

Tags: 源码