移动端中图片的那些事(二)
在我们的APP运行中,图片资源一直都是最消耗性能的因素之一,一张图片从磁盘加载到UIImageView
上面要经历以下步骤:
1.从磁盘拷贝数据到内核缓冲区
2.从内核缓冲区复制数据到用户空间
3.生成UIImageView
,把图像数据赋值给UIImageView
4.CATransaction
捕获到UIImageView layer树
的变化
5.主线程Runloop提交CATransaction
,开始进行图像渲染
5.1如果数据没有字节对齐,
Core Animation
会再拷贝一份数据,进行字节对齐。
5.2GPU
处理位图数据,进行渲染。
其中第5.1步的拷贝过程受字节对齐的影响很有可能还会产生以下部分或者全部步骤
a.将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
b.最后Core Animation
使用未压缩的位图数据渲染UIImageView
的图层。
我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的流畅性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。
为什么要进行图片的解压缩
首先我们要理解一个概念,什么是位图(bitmap)
,位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点,上一篇说到的JPEG和PNG
都是位图,而且是压缩过的位图。
通过下面这个方法可以得到图片的原始像素数据
UIImage *image = [UIImage imageNamed:@"check_green"];
CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
原始像素数据
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
01020102 032c023c 0567048c 078d06bf 08a006d9 09b307f3 09b307f3 08a006d9 078d06bf
0567048c 032c023c 01020102 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 01060108 05570476 09ab07e9 09bb07ff 09bb07ff 09bb07ff 09bb07ff 09bb07ff
09bb07ff 09bb07ff 09bb07ff 09bb07ff 09bb07ff 09ab07e9 05570476 01060108 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 033d0353 08a607e2 09bb07ff 09bb07ff 09bb07ff 09bb07ff
...
09bb07ff 09bb07ff 09bb07ff 09bb07ff 08a607e2 033d0353 00000000 00000000 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 01060108 05570476 09ab07e9 09bb07ff 09bb07ff 09bb07ff 09bb07ff 09bb07ff
09bb07ff 09bb07ff 09bb07ff 09bb07ff 09bb07ff 09ab07e9 05570476 01060108 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 00000000 00000000 01020102 032c023c 0567048c
078d06bf 08a006d9 09b307f3 09b307f3 08a006d9 078d06bf 0567048c 032c023c 01020102
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
解压后的位图要比之前大很多,但是解压后的图片大小与之前原始的图片文件大小没有关系,只与图片像素有关
解压缩后的图片大小 = 图片的像素宽 30 * 图片的像素高 30 * 每个像素所占的字节数 4
不管是 JPEG 还是 PNG
图片,都是一种压缩的位图图形格式。只不过 PNG
图片是无损压缩,并且支持 alpha 通道,而 JPEG
图片则是有损压缩,可以指定 0-100% 的压缩比,这些我在第一部分已经提到过,iOS的机制是如果想在UIImageView
上显示图像,则必须要得到图片的原始像素数据,才能进行后续的绘制操作,这就是为什么一定要解压。
强制解压缩的原理
现在我们已经知道,图片在绘制到UIImageView
的时候要在主线程进行解压缩,而这就是造成APP性能消耗的主要原因,那么如果图片已经解压缩过了呢?系统就不会在主线程中对其进行解压操作,因此目前我们所熟知的第三方开源库都是采用了在子线程中提前解压的操作来进行优化。
而强制解压缩的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数是 CGBitmapContextCreate
/* Create a bitmap context. The context draws into a bitmap which is `width'
pixels wide and `height' pixels high. The number of components for each
pixel is specified by `space', which may also specify a destination color
profile. The number of bits for each component of a pixel is specified by
`bitsPerComponent'. The number of bytes per pixel is equal to
`(bitsPerComponent * number of components + 7)/8'. Each row of the bitmap
consists of `bytesPerRow' bytes, which must be at least `width * bytes
per pixel' bytes; in addition, `bytesPerRow' must be an integer multiple
of the number of bytes per pixel. `data', if non-NULL, points to a block
of memory at least `bytesPerRow * height' bytes. If `data' is NULL, the
data for context is allocated automatically and freed when the context is
deallocated. `bitmapInfo' specifies whether the bitmap should contain an
alpha channel and how it's to be generated, along with whether the
components are floating-point or integer. */
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
这个函数用于创建一个位图上下文,用来绘制一张宽 width
像素,高height
像素的位图。这个函数的注释比较长,参数也比较难理解,但是先别着急,我们先来了解下相关的知识,然后再回过头来理解这些参数,就会比较简单了。
Pixel Format
位图其实就是由很多像素组成的数组,而像素格式
则是用来描述每个像素的组成格式。
Bits per component :一个像素中每个独立的颜色分量使用的 bit 数;
Bits per pixel :一个像素使用的总 bit 数;
Bytes per row :位图中的每一行使用的字节数。
对于位图来说像素格式并不是随意组合的,目前苹果只支持17中特定组合。
对于 iOS 来说,只支持 8 种像素格式。其中颜色空间为Null
的 1 种,Gray
的 2 种,RGB
的 5 种,CMYK
的 0 种。换句话说,iOS 并不支持 CMYK
的颜色空间。另外,在表格的第 2 列中,除了像素格式外,还指定了bitmap information constant
,我们在后面会详细介绍。
Color and Color Spaces
上面我们提到了颜色空间
,什么是颜色空间
呢?在我们日常的开发中经常会用到RGB
,RGB
就是一个颜色空间
,在Quartz
中,一个颜色是由一组数字来表示,而颜色空间是用来解析这组数字,比如(0,0,1)在RGB和BGR
中解析出来是完全不同的颜色。
Color Spaces and Bitmap Layout
我们已经知道像素格式是用来描述每个像素的,比如每个像素所占用的bit值,而要想确保 Quartz 能够正确地解析这些 bit 所代表的含义,我们还需要提供位图的布局信息 CGBitmapInfo
。
typedef CF_OPTIONS(uint32_t, CGBitmapInfo) {
kCGBitmapAlphaInfoMask = 0x1F,
kCGBitmapFloatInfoMask = 0xF00,
kCGBitmapFloatComponents = (1 << 8),
kCGBitmapByteOrderMask = kCGImageByteOrderMask,
kCGBitmapByteOrderDefault = (0 << 12),
kCGBitmapByteOrder16Little = kCGImageByteOrder16Little,
kCGBitmapByteOrder32Little = kCGImageByteOrder32Little,
kCGBitmapByteOrder16Big = kCGImageByteOrder16Big,
kCGBitmapByteOrder32Big = kCGImageByteOrder32Big
} CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
根据字面的意思我们可以看出,它主要提供了三个方面的布局信息
alpha 的信息;
颜色分量是否为浮点数;
像素格式的字节顺序
alpha
的信息由枚举值 CGImageAlphaInfo
来表示
typedef CF_ENUM(uint32_t, CGImageAlphaInfo) {
kCGImageAlphaNone, /* For example, RGB. */
kCGImageAlphaPremultipliedLast, /* For example, premultiplied RGBA */
kCGImageAlphaPremultipliedFirst, /* For example, premultiplied ARGB */
kCGImageAlphaLast, /* For example, non-premultiplied RGBA */
kCGImageAlphaFirst, /* For example, non-premultiplied ARGB */
kCGImageAlphaNoneSkipLast, /* For example, RBGX. */
kCGImageAlphaNoneSkipFirst, /* For example, XRGB. */
kCGImageAlphaOnly /* No color data, alpha data only */
};
它也提供了三个方面的alpha信息
1.是否包含 alpha ;
2.如果包含 alpha ,那么 alpha 信息所处的位置,在像素的最低有效位,比如 RGBA ,还是最高有效位,比如 ARGB ;
3.如果包含 alpha ,那么每个颜色分量是否已经乘以 alpha 的值,这种做法可以加速图片的渲染时间,因为它避免了渲染时的额外乘法运算。比如,对于 RGB 颜色空间,用已经乘以 alpha 的数据来渲染图片,每个像素都可以避免 3 次乘法运算,红色乘以 alpha ,绿色乘以 alpha 和蓝色乘以 alpha 。
那我们解压的时候使用哪个值呢?根据官方文档的说法
You use this function to configure the drawing environment for rendering into a bitmap. The format for the bitmap is a ARGB 32-bit integer pixel format using host-byte order. If the opaque parameter is YES, the alpha channel is ignored and the bitmap is treated as fully opaque (kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Host). Otherwise, each pixel uses a premultipled ARGB format (kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host).
我们可以知道,当图片不包含 alpha的时候使用kCGImageAlphaNoneSkipFirst
,否则使用 kCGImageAlphaPremultipliedFirst
。另外,这里也提到了字节顺序应该使用 32 位的主机字节顺序 kCGBitmapByteOrder32Host
。
至于颜色分量是否为浮点数,这个就比较简单了,直接逻辑或 kCGBitmapFloatComponents
就可以了。更详细的内容就不展开了,因为我们一般用不上这个值。
我们来简单地了解下像素格式的字节顺序,它是由枚举值 CGImageByteOrderInfo
来表示的:
typedef CF_ENUM(uint32_t, CGImageByteOrderInfo) {
kCGImageByteOrderMask = 0x7000,
kCGImageByteOrder16Little = (1 << 12),
kCGImageByteOrder32Little = (2 << 12),
kCGImageByteOrder16Big = (3 << 12),
kCGImageByteOrder32Big = (4 << 12)
} CG_AVAILABLE_STARTING(__MAC_10_12, __IPHONE_10_0);
它主要提供了两个方面的信息
小端模式还是大端模式
数据是16位还是32位
对于 iPhone 来说,采用的是小端模式,但是为了保证应用的向后兼容性,我们可以使用系统提供的宏。
#ifdef __BIG_ENDIAN__
#define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
#define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else /* Little endian. */
#define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
#define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif
根据前面的讨论,我们知道字节顺序的值应该使用的是 32 位的主机字节顺序 kCGBitmapByteOrder32Host
,这样的话不管当前设备采用的是小端模式还是大端模式,字节顺序始终与其保持一致。
我们从图中可以看出,在 32 位像素格式下,每个颜色分量使用 8 位;而在 16 位像素格式下,每个颜色分量则使用 5 位。
好我们再来看看CGBitmapContextCreate
函数中,每个参数所表达的意义。
CG_EXTERN CGContextRef __nullable CGBitmapContextCreate(void * __nullable data,
size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
CG_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0);
data:如果不为 NULL ,那么它应该指向一块大小至少为 bytesPerRow * height 字节的内存;如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可。
width 和 height:位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可。
bitsPerComponent :像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可。
bytesPerRow:位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化。
space:就是指颜色空间,指定RGB即可。
bitmapInfo:就是我们前面提到的位图的布局信息。
以上就是图片强制解压最核心的函数,要多看几遍,好好理解,下一篇我会进行源码解析,来看看SDWebImage
的源码是如何进行图片的加载的,以及它的优化方式。
参考文章:http://blog.leichunfeng.com/blog/2017/02/20/talking-about-the-decompression-of-the-image-in-ios/