[toc]
SDWebImage③解码
好文推荐:
超大图的缩放解码:设置了SDWebImageScaleDownLargeImages
1
2
3
4
5
6
7
8
9
10
11[cell.shopImage sd_setImageWithURL:[NSURL URLWithString:model.shopNearPic] placeholderImage:[UIImage imageNamed:@"无商家图正方形"] options:SDWebImageScaleDownLargeImages];
// SDWebImage 内部
if (shouldDecode) {
BOOL shouldScaleDown = options & SDWebImageScaleDownLargeImages;
if (shouldScaleDown) {// 将原图按照固定大小分割,然后依次绘制到目标画布
image = [SDImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:0];
} else {// 直接解码到位图生成新图
image = [SDImageCoderHelper decodedImageWithImage:image];
}
}
图片常见的3种编码方式 见本文附录
万象:
1 | CGSize thumbnailSize = CGSizeMake(200, 200); // Thumbnail will bounds to (200,200) |
一、为什么图像在显示到屏幕上之前要进行解码
我们知道,一般我们都是使用形如以下方式加载图片:
1 | + (nullable UIImage *)imageNamed:(NSString *)name; // load from main bundle // 通过图片的文件名从bundle 获取这个图片,注意该图片已经导入到工程中 |
问1:解码是个啥?不编码是不是可以不解码?
答:那是因为一般下载的图片或者我们手动拖进主bundle 的图片都是PNG 或者JPG 其他格式的图片,这些图片都是经过编码压缩后的图片数据,并不是控件可以直接显示的位图。所以需要先将它解码转成位图数据,然后才能把位图渲染到屏幕上。这就是解码!(附:图像可以分为矢量图和位图,我们通常使用的图像为位图格式)
1.1、手动解码效果对比
1 | - (void)queryImageCache:(NSString *)filename block:(void(^)(UIImage *image))block |
1.1、图片不编码的话,是不是可以显示时候就可以不用去解码,从而能更快了?
实际上编码工作不可能少了。其目的可能是为了压缩、优化、添加元数据(如拍摄信息、版权信息、色彩配置文件等)。
1.2、手动解码的原理
自己手动解码的原理就是对图片进行重新绘制,得到一张新的解码后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate 。
问2:没解码显示不了图片,那为什么直接使用上面的方式进行图片的加载可以正常显示?
答案是:从实际开发中,我们基本都是使用上面的两种方式直接在主线程加载图片,然后显示在UIImageView上,并且并没有发现什么问题。是不是代表显示图片不用解码?其实不是的,因为系统会为我们进行解码的操作。所以,解码图片的工作在图片显示流程中还是必不可少的。
问3:系统是在什么时候为我们进行图片的解码操作的?图片加载的工作流?
当你用 UIImage 或 CGImageSource 的那几个方法创建图片时,图片数据并不会立刻解码。图片设置到 UIImageView 或者 CALayer.contents 中去,并且 CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码。这一步是发生在主线程的,并且不可避免。
概括来说,从磁盘中加载一张图片,并将它显示到屏幕上,中间的主要工作流/图片加载的工作流如下:
- 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
- 然后将生成的 UIImage 赋值给 UIImageView ;
- 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
- 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
①分配内存缓冲区用于管理文件 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中使用以下策略:
- 当图片从网络中获取到的时候就进行解压缩。
- 当图片从磁盘缓存中获取到的时候立即解压缩。
二、SDWebImage是怎么进行图片Decoder解码的UIImage+ForceDecode.h
1、UIImage+ForceDecode.h 的 sd_decodedImageWithImage:
下面首先看解码的方法,UIImage+ForceDecode.h
1 | + (nullable UIImage *)sd_decodedImageWithImage:(nullable UIImage *)image; |
其实现为:
1 | + (nullable UIImage *)sd_decodedImageWithImage:(nullable UIImage *)image { |
2、[SDImageCoderHelper decodedImageWithImage:image]
SDImageCoderHelper中的实现代码:
这个方法传入一副图片对该图片进行解码,解码结果是另一幅图片。
2.1、[SDImageCoderHelper shouldDecodeImage:image]
[SDImageCoderHelper shouldDecodeImage:image]
用来判断要不要解码,并不是所有的image 都要解码。其函数实现如下:
1 |
|
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 | + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage { |
2.3、解码的核心代码
使用
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源码阅读前的准备(三)UIImage.h
附1:图片常见的3种编码方式
类型 | 演示示例 | |
---|---|---|
baseline 逐行扫描 |
默认情况下,JPEG、PNG、GIF 都是这种保存方式。 | ![]() |
interlaced 隔行扫描 |
PNG 和 GIF 在保存时可以选择这种格式。 | ![]() |
progressive 渐进式 |
JPEG 在保存时可以选择这种方式。 | ![]() |