Version: 4.2.0

流程

基本流程

  1. 通过对象 (eg: UIImageView) 的 Category (sd_setImageWithURL()) 作为入口
  2. 调用基类 UIViewCategory (sd_internalSetImageWithURL())
  3. 调用管理类 SDWebImageMangerloadImageWithURL:() 方法,由 SDWebImageManger 协调 SDImageCacheSDWebImageDownloader
  4. 调用缓存类 SDImageCachequeryDiskCacheForKey() 先去检查有没缓存(内存+磁盘)
  5. 有缓存,返回缓存图片
  6. 若没有,由 SDWebImageDownloader 发起请求下载图片 downloadImage(url, options, progress, completed)
  7. 返回下载结果回调给 SDWebImageManger
  8. 下载图片成功并用 SDImageCachestoreImage() 缓存图片
  9. 返回图片给基类 UIView,缓存或者下载的
  10. 图片设置到对象上 (eg: UIImageView)

主要文件

  • 入口:Category
    • UIImageView+WebCache
    • UIButton+WebCache
    • UIView+WebCache
    • ……
  • 缓存
    • SDImageCacheConfig:缓存配置
    • SDImageCache:缓存管理
  • 下载
    • SDWebImageDownloaderOperation:实际下载操作
    • SDWebImageDownloader:下载管理
  • 编码/解码
    • SDWebImageCodersManager: 编解码管理
    • SDWebImageCoder:编解码协议
    • SDWebImageImageIOCoder:PNG/JPEG/TIFF 编解码,解压缩,显示大图
    • SDWebImageGIFCoder:GIF 编解码
    • SDWebImageWebPCoder:WebP 编解码
  • 工具
    • SDWebImageManager:协调 SDImageCacheSDWebImageDownloader
    • SDWebImagePrefetcher:预下载图片

解压缩(Decompress)

解压缩指的是,将压缩过的图片(JPEGPNGWebPAPNG)解压成未压缩的位图

加载图片流程

  1. 假设用 +[UIImage imageWithContentsOfFile:] 加载图片,它会创建 UIImage 的一个引用,此时并不会发生解码
  2. 返回的 UIImage 赋值给 UIImageView
  3. 隐式 CATransaction 捕获到图层树(layer tree)的修改
  4. 在主线程 run loop 的下一个循环时,Core Animation 会提交这些隐式 CATransaction,图片可能会创建一个 copy。在 copy 过程中,可能会发生如下一些或者全部步骤:
    1. 分配管理文件 IO、解压缩操作的内存缓冲区
    2. 文件数据从磁盘读取到内存中
    3. 压缩的图片数据解码成未压缩的位图格式,这通常是非常耗时的 CPU 操作
    4. Core Animation 将解压缩后的位图数据渲染到图层(layer)上

其中「流程4步骤3」中图片的解压缩是一个非常耗时的操作,而且默认发生在主线程中。当显示的图片过多时,容易导致性能问题。特别是滚动时,尤为突出。

因此我们可以将图片的解压缩放在后台线程,不堵塞主线程。这就需要提前在后台线程解压缩,之后将解压缩后的图片在主线程赋值给 UIImageView

主要实现

SDWebImageImageIOCoder.m

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
- (nullable UIImage *)sd_decompressedImageWithImage:(nullable UIImage *)image {
...

// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool{

CGImageRef imageRef = image.CGImage;
CGColorSpaceRef colorspaceRef = [[self class] colorSpaceForImageRef:imageRef];

size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
size_t bytesPerRow = kBytesPerPixel * width;

// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (context == NULL) {
return image;
}

// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // Decompress
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];

CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);

return imageWithoutAlpha;
}
}

对比了 YYWebImageFLAnimatedImage 的解压缩代码,iOS 位图上下文支持的像素格式,以及 UIGraphicsBeginImageContextWithOptions 的讨论说明,最终的 CGContextRef CGBitmapContextCreate(void *data, size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow, CGColorSpaceRef space, uint32_t bitmapInfo) 的传入参数应该如下:

1
2
3
4
5
6
7
CGBitmapContextCreate(NULL,
CGImageGetWidth(imageRef),
CGImageGetHeight(imageRef),
8, // bitsPerComponent
0, // bytesPerRow
CGColorSpaceCreateDeviceRGB(),
kCGBitmapByteOrderDefault| hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst)

加载大图

当加载一张很大的图片时,如果直接加载到内存中,很容易超出 App 的最大内存预算,轻则 App 被系统 Kill,重则整个系统重启。详细参数见:iOS App 的最大内存预算

因此我们可以采用 Tiled rendering 的方式渲染图片,每次只加载图片的一小部分,最后再拼成一张图。详细可以看看 SDWebImageImageIOCoder.m 的实现或者苹果示例代码

其他

主队列 vs 主线程(Main Queue vs. Main Thread)

1
2
3
4
5
6
7
8
#ifndef dispatch_main_async_safe
#define dispatch_main_async_safe(block)\
if (strcmp(dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL), dispatch_queue_get_label(dispatch_get_main_queue())) == 0) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}
#endif

这是 SDWebImage 的主线程判断代码,使代码一直在主线程上运行。

这里的主线程判断是用主队列进行判断,而不是主线程判断。为什么呢?

首先在 dispatch_sync文档讨论中有说到:

… As an optimization, this function invokes the block on the current thread when possible.

作为优化,dispatch_sync 有可能会在当前线程中调用 block

假设你调用了如下代码,当然了,尽量不要在主线程调用任意同步操作(容易死锁😔)。

1
2
3
4
5
// main queue environment
dispatch_queue_t nonMainQueue = dispatch_queue_t("com.lavare.nonMainQueue", DISPATCH_QUEUE_CONCURRENT)
dispatch_sync(nonMainQueue) {
// Other tasks
}

dispatch_sync 分配的任务在 nonMainQueue 队列中调用,因为优化问题,nonMainQueue 队列有可能会在当前线程中调用,即主线程。

那主线程中会有 2 种情况,有可能是主队列,有可能是非主队列。

所以对于 UIKit 、需要主线程或者主队列的代码,用主队列判断会更安全。

参考