SDWebImage③解码

[toc]

SDWebImage③解码

好文推荐:

图片常见的3种编码方式 见本文附录

万象:

1
2
CGSize thumbnailSize = CGSizeMake(200, 200); // Thumbnail will bounds to (200,200)
[imageView sd_setImageWithURL:url placeholderImage:nil options:0 context:@{SDWebImageContextImageThumbnailPixelSize : @(thumbnailSize)];

一、为什么图像在显示到屏幕上之前要进行解码

我们知道,一般我们都是使用形如以下方式加载图片:

1
2
+ (nullable UIImage *)imageNamed:(NSString *)name;      // load from main bundle // 通过图片的文件名从bundle 获取这个图片,注意该图片已经导入到工程中
+ (nullable UIImage *)imageWithContentsOfFile:(NSString *)path; // 通过文件加载指定路径下的文件内容创建图片

问1:解码是个啥?不编码是不是可以不解码?

答:那是因为一般下载的图片或者我们手动拖进主bundle 的图片都是PNG 或者JPG 其他格式的图片,这些图片都是经过编码压缩后的图片数据,并不是控件可以直接显示的位图。所以需要先将它解码转成位图数据,然后才能把位图渲染到屏幕上。这就是解码!(附:图像可以分为矢量图和位图,我们通常使用的图像为位图格式)

一张图片引发的深思

1.1、手动解码效果对比

iOS 列表性能优化-图片解码性能优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)queryImageCache:(NSString *)filename block:(void(^)(UIImage *image))block
{
//从内存去取,如果没取到,就直接读取文件,在缓存起来
UIImage *image = [self.memCache objectForKey:filename];
if(image) {
dispatch_async(dispatch_get_main_queue(), ^{
if(block) block(image);
});
} else {
//把解压操作放到子线程
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *path = [[NSBundle mainBundle] pathForResource:filename ofType:@"jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:path];
image = [UIImage decodedImageWithImage:image]; // 解码图片
[self.memCache setObject:image forKey:filename]; // 保存解码的图片
// 同步主线程
dispatch_async(dispatch_get_main_queue(), ^{
if(block) block(image);
});
});
}
}

1.1、图片不编码的话,是不是可以显示时候就可以不用去解码,从而能更快了?

实际上编码工作不可能少了。其目的可能是为了压缩、优化、添加元数据(如拍摄信息、版权信息、色彩配置文件等)。

1.2、手动解码的原理

自己手动解码的原理就是对图片进行重新绘制,得到一张新的解码后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate 。

image-20240905220154620

image-20240905210236393

问2:没解码显示不了图片,那为什么直接使用上面的方式进行图片的加载可以正常显示?

答案是:从实际开发中,我们基本都是使用上面的两种方式直接在主线程加载图片,然后显示在UIImageView上,并且并没有发现什么问题。是不是代表显示图片不用解码?其实不是的,因为系统会为我们进行解码的操作。所以,解码图片的工作在图片显示流程中还是必不可少的。

问3:系统是在什么时候为我们进行图片的解码操作的?图片加载的工作流?

当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。

概括来说,从磁盘中加载一张图片,并将它显示到屏幕上,中间的主要工作流/图片加载的工作流如下:

  1. 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
  2. 然后将生成的 UIImage 赋值给 UIImageView ;
  3. 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
  4. 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
  5. ①分配内存缓冲区用于管理文件 IO 和解压缩操作;

    ②将文件数据从磁盘读到内存中;

    ③将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;

    ④最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。

问4:使用系统解码有什么问题?或者说SDWebImage还要对图片进行解码是为了什么?

①、使用系统解码有什么问题?

答:如果我们直接使用 + (nullable UIImage *)imageNamed:(NSString *)name 来加载图片,系统默认会在主线程立即进行图片的解码工作,这个过程就是把图片数据解码成可供控件直接显示的位图数据。由于这个解码/解压缩操作是一个比较耗时的CPU操作,并且默认是在主线程进行的。所以当在主线程调用了大量的 + (nullable UIImage *)imageNamed:(NSString *)name 方法后就会产生卡顿,尤其是在快速滑动的列表上,这个问题会表现得更加突出。

②、SDWebImage还要对图片进行解码的目的是

优化1:默认是在主线程解码,SDWebImage将解码这个过程放到子线程。

图片解码是耗时的,而且iOS系统默认是在主线程执行解码,所以业界通常有一种做法是,异步强制解压,也就是在异步线程主动将二进制图片数据解压成位图数据,使用CGBitmapContextCreate(…)系列方法就能实现。
该处理方式在众多图片处理框架下都有体现。

所以SDWebImage 为了提高图片的加载效率,会提前去进行解码图片到内存,即把图片数据解码成可供控件直接显示的位图数据。同时由于位图体积较大,所以在磁盘缓存中不会直接缓存位图数据,而是编码压缩过的PNG 或者JPG 数据。

在我们使用 UIImage 的时候,创建的图片通常不会直接加载到内存,而是在渲染的时候再进行解压并加载到内存。这就会导致 UIImage 在渲染的时候效率上不是那么高效。为了提高效率通过 decodedImageWithImage方法把图片提前解压加载到内存,这样这张新图片就不再需要重复解压了,提高了渲染效率。这是一种空间换时间的做法。

问5:SDWebImage解压的策略是怎样的?

SDWebImage中使用以下策略:

  1. 当图片从网络中获取到的时候就进行解压缩。
  2. 当图片从磁盘缓存中获取到的时候立即解压缩。

二、SDWebImage是怎么进行图片Decoder解码的UIImage+ForceDecode.h

1、UIImage+ForceDecode.h 的 sd_decodedImageWithImage:

下面首先看解码的方法,UIImage+ForceDecode.h

1
2
+ (nullable UIImage *)sd_decodedImageWithImage:(nullable UIImage *)image;
+ (nullable UIImage *)sd_decodedAndScaledDownImageWithImage:(nullable UIImage *)image limitBytes:(NSUInteger)bytes;

image-20211201013634575

其实现为:

1
2
3
4
5
6
+ (nullable UIImage *)sd_decodedImageWithImage:(nullable UIImage *)image {
if (!image) {
return nil;
}
return [SDImageCoderHelper decodedImageWithImage:image];
}

2、[SDImageCoderHelper decodedImageWithImage:image]

SDImageCoderHelper中的实现代码:

image-20211201014559478

这个方法传入一副图片对该图片进行解码,解码结果是另一幅图片。

2.1、[SDImageCoderHelper shouldDecodeImage:image]

[SDImageCoderHelper shouldDecodeImage:image]用来判断要不要解码,并不是所有的image 都要解码。其函数实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma mark - Helper Function
+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
// Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
if (image == nil) {
return NO;
}
// Avoid extra decode
if (image.sd_isDecoded) {
return NO;
}
// do not decode animated images
if (image.sd_isAnimated) {
return NO;
}
// do not decode vector images
if (image.sd_isVector) {
return NO;
}

return YES;
}

1.如果 image 等于 nil,返回 NO。 // 防止 “CGBitmapContextCreateImage: invalid context 0X0” 的错误

2.如果已经解码过,返回NO。

3.如果 image 是动效图片,返回 NO。// 不要解码动画图像

4.如果sd_isVector,返回NO。

2.2、[SDImageCoderHelper CGImageCreateDecoded:cgImage]

其中+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage的实现如下:

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
+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage {
return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp];
}

+ (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation {
if (!cgImage) {
return NULL;
}
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
if (width == 0 || height == 0) return NULL;
size_t newWidth;
size_t newHeight;
switch (orientation) {
case kCGImagePropertyOrientationLeft:
case kCGImagePropertyOrientationLeftMirrored:
case kCGImagePropertyOrientationRight:
case kCGImagePropertyOrientationRightMirrored: {
// These orientation should swap width & height
newWidth = height;
newHeight = width;
}
break;
default: {
newWidth = width;
newHeight = height;
}
break;
}

BOOL hasAlpha = [self CGImageContainsAlpha:cgImage];
// iOS prefer BGRA8888 (premultiplied) or BGRX8888 bitmapInfo for screen rendering, which is same as `UIGraphicsBeginImageContext()` or `- [CALayer drawInContext:]`
// Though you can use any supported bitmapInfo (see: https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB ) and let Core Graphics reorder it when you call `CGContextDrawImage`
// But since our build-in coders use this bitmapInfo, this can have a little performance benefit
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo);
if (!context) {
return NULL;
}

// Apply transform
CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight));
CGContextConcatCTM(context, transform);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height
CGImageRef newImageRef = CGBitmapContextCreateImage(context);
CGContextRelease(context);

return newImageRef;
}

2.3、解码的核心代码

image-20211201023244869

  • 使用CGBitmapContextCreate()创建图片上下文

  • 使用CGContextDrawImage()将图片绘制到上下文中

  • 使用CGBitmapContextCreateImage()通过上下文生成图片

    其他参考:SDWebImage 中的图片解码

3、其他

3.1、两个静态不可变的类型是 size_t 的变量

①、kBytesPerPixel:每个像素占内存多少字节(Byte),赋值为4,表示每个像素占4个字节(RGBA各一个字节)。
1
static const size_t kBytesPerPixel = 4;

(图像在iOS 设备上是以像素为单位显示的)

②、kBitsPerComponent:每个组件占多少位(Bit),赋值为8
1
static const size_t kBitsPerComponent = 8;

kBitsPerComponent:每个组件占多少位(Bit),这个不好理解,举个例子,比如RGBA,其中 R (红色)G(绿色)B(蓝色)A(透明度)总共4个组件,每个像素由这4个组件组成,且该变量被赋值为8,所以一个 RGBA 像素就是8 * 4 = 32 Bits。

知道了 kBitsPerComponent 和每个像素有多少组件组成就能计算 kBytesPerPixel 了。计算公式是: (bitsPerComponent * number of components + 7)/ 8。

3.2、其他

SDWebImage源码阅读(四)Decoder,有点抽象

三、其他参考文章

SDWebImage源码阅读前的准备(三)UIImage.h

附1:图片常见的3种编码方式

类型 演示示例
baseline
逐行扫描
默认情况下,JPEG、PNG、GIF 都是这种保存方式。 img
interlaced
隔行扫描
PNG 和 GIF 在保存时可以选择这种格式。 img
progressive
渐进式
JPEG 在保存时可以选择这种方式。 img

imageIO完成渐进加载图片