上一篇讲到了如何下载。这篇将探究下载后是如何解码的。
外部调动
外部会在 disk 中获取到缓存 data 后调用解码方法,也会在 download 到 data 后调用解码方法。两者的解码操作基本一致,但是前者调用的 SDImageCacheDefine.m
中的方法,而后者调用的是 SDImageLoader.m
中的方法
Cache 中的解码
在从 disk 中拿到 NSData
之后,就会调用 SDImageCacheDefine.m
中的 SDImageCacheDecodeImageData()
方法,进行解码:
1 | UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context) { |
如果是一个动图,并且不只解码第一帧,那么就会调用 SDAnimatedImage
的初始化方法把所有帧都解码出来。
如果不是动图,那么就会通过 SDImageCodersManager
先用 NSData
创建 UIImage
,再对 UIImage
解码。整个过程会在下文详解。
Download 中的解码
download 方式下的解码方法在 SDImageLoader.m
中。有两个方法 SDImageLoaderDecodeImageData()
和 SDImageLoaderDecodeProgressiveImageData()
。
前者和 Cache 中的方法是一样的,都是将下载或者内存加载的 NSData
转为 UIImage
,再解码。后者则提供了一种渐进式加载图片的方式:
1 | UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, BOOL finished, id<SDWebImageOperation> _Nonnull operation, SDWebImageOptions options, SDWebImageContext * _Nullable context) { |
和 cache 中做法相同的 SDImageLoaderDecodeImageData()
方法会在下载图片完成回调时进行;而渐进式解码 SDImageLoaderDecodeProgressiveImageData()
方法则会在下载过程中接收到一边接收数据一边进行。
编解码
SDImageCodersManager:解码器的管理者
SDImageCodersManager
是一个单例类,内部在初始化的时候增加了多个实际的编解码类,用于将 NSData 转化为 UIImage。每次在使用到的时候都会循环每一个实际的编解码类,来对图片进行 data → image 的转换
你可能会有疑问,NDData 到 UIImage 不是系统内置的方法就可以解决嘛?一般情况下是的,对于 png, jpeg 等格式,系统自带了转换方法。但是对于苹果不是默认支持的格式,比如 webp,就需要自己通过解码器将 NSData → UIImage。
由此可见,NSData → UIImage 的过程也是一个解码的过程
初始化方法
初始化方法如下所示。在单例的初始化过程中,默认增加了 SDImageIOCode
,SDImageGIFCoder
和 SDImageAPNGCoder
三个实际的编解码类。
1 | + (nonnull instancetype)sharedManager { |
除了这三个内置的编解码类,我们还可以通过它提供的方法动态的添加和移除编解码器相关的类。
能否支持编解码
解码要通过 NSData
进行解码。编码则是把 UIImage
根据 SDImageFormat
表示的图片类型转变为 NSData
。和上面所说的一样,SDImageCodersManager
会调用内部的所有编解码类分别判断:
1 | /// 遍历所有 coder 看是否能解码 |
解码:从 NSData → UIImage
在判断能否进行编解码之后,就会调用实际的编解码的类的相关方法:
1 |
|
以上就是 SDImageCodersManager
的全部内容了。本生作为一个 Manager,并没有太多的任务,所有的操作都是遍历内部的编解码类的列表,让它们来完成的。
SDImageCoderHelper:UIImage 到 bitmap 的转换者
上面 manager 中管理的是从 data 到 image 的解码,这个 helper 则是要把 UIImage 提前转化为 bitmap 以贡显示。
SDImageFrame 和 UIImage 的互相转化
SDImageFrame
是 GIF 或者 APNG 的每一帧的模型化。每一个 SDImageFrame
实例包括一个 UIImage
实例和一个时间间隔 duration
:
1 | @interface SDImageFrame : NSObject |
GIF 和 APNG 不能直接转为 UIImage
的 animatedImages
的原因是后者的两帧之间的时间间隔是固定的,而前者则是不同的。因此两者相互转化的时候就要找到 GIF 和 APNG 两帧之间时间的最大公约数,以此作为 UIImage
的时间间隔。如果 GIF 和 APNG 的间隔较长则需要往其中插帧。
1 |
|
获取最大公约数的过程通过辗转相除的方式完成:
1 | static NSUInteger gcd(NSUInteger a, NSUInteger b) { |
获取设备的 rgb空间
这个方法没什么特别的地方,就是 Core Graphic api的调用。它会在创建 Bitmap 的时候使用
1 |
|
是否具有alpha通道
判断是否含有 alpha 通道,在创建爱你 bitmap 的时候需要传入的属性:
1 |
|
在UI渲染的时候,实际上是把多个图层按像素叠加计算的过程,需要对每一个像素进行 RGBA 的叠加计算。当某个 layer 的是不透明的,也就是 opaque 为 YES 时,GPU 可以直接忽略掉其下方的图层,这就减少了很多工作量。这也是调用 CGBitmapContextCreate 时 bitmapInfo 参数设置为忽略掉 alpha 通道的原因。
解码:从 image 到 bitmap
image 要显示在屏幕上需要转化为 bitmap,这个操作往往是显示的时候在主线程中做的。为了提高效率,可以通过 decodedImageWithImage
方法把 image 提前转化为 bitmap,这样这张新图片就不再需要重复渲染了,提高了渲染效率:
1 | + (UIImage *)decodedImageWithImage:(UIImage *)image { |
将 UIImage 按比例减小到 bytes 内
每一个像素点由于有 RGBA 四个通道,每个通道占用 1 个字节,整个像素点需要占用 4 个字节。对于一个高分辨率的图片来说,会占用大量内存资源,甚至产生 OOM。
SDWebImage 的做法是,使用分块绘制的方式,读取一部分图片的data绘制成图,再把图绘制到缩放过的 context 中,然后释放掉之前读取的 data。循环往复,将整张图都绘制出来。这样能够解决内存爆炸问题。
1 |
|
SDImageIOCoder:内置图片类型的解码器
SDImageIOCoder
支持 png,jpeg ,以及部分机型支持 HEIC HEIF 的解码以及渐进式加载。
能否解码
1 | - (BOOL)canDecodeFromData:(nullable NSData *)data { |
能否解码主要依据与图片的类型是否是支持的类型。获取图片数据格式的方式如下:
1 | + (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data { |
通过读取 data 的第一个字节就能知道图片的类型。
判断判断是否支持某一个格式的编码,通过看是否能生成 CGImageDestination 来判断:
1 | + (BOOL)canEncodeToFormat:(SDImageFormat)format { // 判断是否支持某一个格式的编码,通过看是否能生成 CGImageDestination 来判断 |
直接解码:完整的 NSData → UIImage
直接解码的方式非常简单粗暴,直接通过 UIImage
的方法:
1 | - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options { |
渐进式解码:部分 NSData → UIImage
渐进式解码提供了边下载边解码的功能。
1 |
|
主要还是使用 Core Graphic 的 API。前一个方法通过 CGImageSourceUpdateData()
往 CGImageSourceRef
中增加 data,后一个方法把 CGImageSourceRef
生成为 UIImage
编码
编码是从 UIImage
到 NSData
的转换:
1 | - (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format options:(nullable SDImageCoderOptions *)options { |
UIImageJPEGRepresentation()
类似的 API 也可以将 UIImage
转为 NSData
,但是上面这种通过 imageIO api 的方式生成的 NSData 无论是从内存使用还是物理内存的占用上都有更好的表现。
总结
从本篇的解码过程中我们可以知道,不论是从 cache 还是 download,都需要通过 data 转化为通过 bitmap 展示的 image。其中解码过程会分为两步:
- 从 data → image
- 从 image → bitmap 形式的 image
第一步由 SDImageCodersManager
管理,它通过内置的多个编解码器完成;第二部由 SDImageCoderHelper
完成,它会创建一个 bitmap,并把前面的 image 画到上面去生成一个新的 image。
在编解码阶段我们能获取到的知识点有如下:
- 获取图片的类型可以读取其 data 的第一个字节判断
- GIF 的 duration 是不固定的,转为 iOS 中显示时要插帧
- 如果图片没有 alpha 通道,创建 bitmap 的时候就不要创建有 alpha 通道的。没有 alpha 通道能增快渲染效率
- UIImage 只有在显示的时候才会在主线程中解码,浪费时间。可以通过在子线程中创建 bitmap 的方式提前将图片加载到内存中。
- 对于超大的 UIImage,为了降低内存消耗,可以按比例缩放。生成缩放图片的时候可以通过分片渲染的方式,每加载一部分,渲染一部分到缩放图片上。
- 渐进式解码主要使用 Core Graphic API,获取一部分数据就往
CGImageSourceRef
中丢一部分,然后生成 UIImage - 使用
UIImageJPEGRepresentation()
类似 API 将 UIImage → NSData 不如使用 ImageIO 的 API 效率高,占用内存小。