介绍下几种iOS中位图信息 与相关解压缩
ref 谈谈 iOS 中图片的解压缩
ref Quartz 2D Programming Guide
我们从网络下载或者从本地磁盘加载一张图片到屏幕上显示,要经过图片的解码过程,为什么呢?因为我们一般的图片格式例如 JPEG,PNG都是经过压缩后的图片,而显示在屏幕上的图片叫做位图(bitmap),所谓的解码就是把压缩后的图片变成位图。
为什么非要解码成位图才能显示呢?因为位图又被叫做点阵图像,也就是说位图包含了一大堆的像素点信息,这些像素点就是该图片中的点,有了图片中每个像素点的信息,就可以在屏幕上渲染整张图片了。
那我们为什么还需要不同格式的各种图片呢?直接全部用位图不就好了?那不就不需要每次解码了?那些JPEG以及PNG等其实都是图像的压缩格式,我们都知道压缩的意思就是减小空间,所以我们可以想到,使用这些格式的原因就是位图实在太大了。
图片解压后的数据变化
我们来使用这张图片和以下代码做例子:
该图片的二进制信息为:
1 | 00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR |
获取图片解压信息的代码为:
1 | NSString * path = [[NSBundle mainBundle]pathForResource:@"testImg2" ofType:@"png"]; |
这张图片像素尺寸为34x34,文件大小为 336Byte ;而获取到图片原始像素数据rawData,大小为4624byte。以下为rawData信息 :
1 | 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 |
也就是说,这张PNG 图片解压缩后的大小是 4624byte ,是原始文件大小的 4.27 倍。那么这个4624byte是怎么得来的呢?与图片的文件大小或者像素有什么必然的联系吗?事实上,解压缩后的图片大小与原始文件大小之间没有任何关系,而只与图片的像素有关:
解压缩后的图片大小 = 图片的像素宽 34 图片的像素高 34 每个像素所占的字节数 4
我们常见接触到的图片格式, JPEG 还是 PNG 图片,都是一种压缩的位图图形格式。只不过 PNG 图片是无损压缩,并且支持 alpha 通道,而 JPEG 图片则是有损压缩,可以指定 0-100% 的压缩比。在将磁盘中的图片渲染到屏幕之前,必须先要得到图片的原始像素数据,才能执行后续的绘制操作。
位图信息
其实,位图就是一个像素数组,数组中的每个像素就代表着图片中的一个点。Bitmap Images and Image Masks中是这么定义的:
A bitmap image (or sampled image) is an array of pixels (or samples). Each pixel represents a single point in the image. JPEG, TIFF, and PNG graphics files are examples of bitmap images.
Each sample in a bitmap contains one or more color components in a specified color space, plus one additional component that specifies the alpha value to indicate transparency. Each component can be from 1 to as many as 32 bits. In Mac OS X, Quartz also provides support for floating-point components. The supported formats in Mac OS X and iOS are described in “Pixel formats supported for bitmap graphics contexts”. ColorSync provides color space support for bitmap images.
Quartz在创建位图图像(CGImageRef)时使用以下信息:
- data source 位图数据源,可以是Quartz数据提供程序或Quartz图像源。
- Pixel Format 像素格式,包括每个组件的位数,每个像素的位数和每行的字节数。
- Color Spaces and Bitmap Layout 对于图像,颜色空间和位图布局(颜色空间和位图布局)信息用于描述alpha的位置以及位图是否使用浮点值。
- Decode Array 可选的解码数组。
- 插值设置,它是一个布尔值,它指定在调整图像大小时,Quartz是否应应用插值算法。
- 一个渲染意图,指定如何映射位于图形上下文的目标颜色空间内的颜色。
- 图像尺寸。
解码数组 Decode Array
解码数组将图像颜色值映射到其他颜色值,这对于诸如使图像去饱和或反转颜色之类的任务很有用。该数组包含每个颜色分量的一对数字。Quartz渲染图像时,它将应用线性变换将原始分量值映射到适合目标色彩空间的指定范围内的相对数字。例如,RGB颜色空间中图像的解码数组包含六个条目,每个红色,绿色和蓝色分量一对。
像素格式Pixel Format
像素格式包含以下信息:
- Bits per component每个分量的位数,即像素中每个单独颜色分量的位数。对于图像掩模,此值是源像素中有效掩模位的数量。例如,如果源图像是8位掩码,则每个组件指定8位。
- Bits per pixel每个像素的位数,即源像素的位数。该值必须至少是每个组件的位数乘以每个像素的组件数。
- Bytes per row每行字节数。图像中每水平行的字节数。
颜色与颜色空间 Color and Color Spaces
Color and Color Spaces中说明了Quartz中的颜色由一组值表示。如果没有颜色空间指示如何解释颜色信息,则这些值将毫无意义。例如,表4-1中的值全部代表全强度的蓝色。但是,如果不知道颜色空间或每种颜色空间的允许值范围,就无法知道每组值代表哪种颜色。
表4-1 不同颜色空间中的颜色值
values | color space | compoents |
---|---|---|
240 degrees, 100%, 100% | HSB | Hue, saturation, brightness |
0, 0, 1 | RGB | Red, green, blue |
1, 1, 0, 0 | CMYK | Cyan, magenta, yellow, black |
1, 0, 0 | BGR | Blue, green, red |
如果提供错误的色彩空间,则可能会出现很大的差异,如图4-1所示。尽管绿色在BGR和RGB颜色空间中的解释相同,但是红色和蓝色值却被翻转了。
位图布局 Bitmap Layout
想确保 Quartz 能够正确地解析像素格式中各个bit所代表的含义,我们还需要提供位图的布局信息 CGBitmapInfo
:
1 | typedef CF_OPTIONS(uint32_t, CGBitmapInfo) { |
它主要提供了三个方面的布局信息:
- alpha 的信息 CGImageAlphaInfo;
- 颜色分量是否为浮点数;
- 像素格式的字节顺序 Byte Ordering。
透明信息 CGImageAlphaInfo
其中,alpha 的信息由枚举值 CGImageAlphaInfo 来表示:
1 | typedef CF_ENUM(uint32_t, CGImageAlphaInfo) { |
上面的注释其实已经比较清楚了,它同样也提供了三个方面的 alpha 信息:
- 是否包含 alpha ;
- 如果包含 alpha ,那么 alpha 信息所处的位置,在像素的最低有效位,比如 RGBA ,还是最高有效位,比如 ARGB ;
- 如果包含 alpha ,那么每个颜色分量是否已经乘以 alpha 的值,这种做法可以加速图片的渲染时间,因为它避免了渲染时的额外乘法运算。比如,对于 RGB 颜色空间,用已经乘以 alpha 的数据来渲染图片,每个像素都可以避免 3 次乘法运算,红色乘以 alpha ,绿色乘以 alpha 和蓝色乘以 alpha 。
字节顺序 CGImageByteOrderInfo
字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)在最高有效字节的前面,则称小端序;反之则称大端序。
字节顺序的信息由枚举值 CGImageByteOrderInfo 来表示:
1 | typedef CF_ENUM(uint32_t, CGImageByteOrderInfo) { |
它主要提供了两个方面的字节顺序信息:
- 小端序还是大端序;
- 数据以 16 位还是 32 位为单位。
下图表示了 Quartz 2D中CMYK和RGB颜色空间的32位和16位像素格式
支持的像素格式
下表表示了位图图形上下文支持的像素格式,关联的色彩空间 Supported Pixel Formats:
cs | Pixel format and bitmap information constant | Availability |
---|---|---|
Null | 8 bpp, 8 bpc, kCGImageAlphaOnly | Mac OS X, iOS |
Gray | 8 bpp, 8 bpc,kCGImageAlphaNone | Mac OS X, iOS |
Gray | 8 bpp, 8 bpc,kCGImageAlphaOnly | Mac OS X, iOS |
Gray | 16 bpp, 16 bpc, kCGImageAlphaNone | Mac OS X |
Gray | 32 bpp, 32 bpc, kCGImageAlphaNone或kCGBitmapFloatComponents` | Mac OS X |
RGB | 16 bpp, 5 bpc, kCGImageAlphaNoneSkipFirst | Mac OS X, iOS |
RGB | 32 bpp, 8 bpc, kCGImageAlphaNoneSkipFirst | Mac OS X, iOS |
RGB | 32 bpp, 8 bpc, kCGImageAlphaNoneSkipLast | Mac OS X, iOS |
RGB | 32 bpp, 8 bpc, kCGImageAlphaPremultipliedFirst | Mac OS X, iOS |
RGB | 32 bpp, 8 bpc, kCGImageAlphaPremultipliedLast | Mac OS X, iOS |
RGB | 64 bpp, 16 bpc, kCGImageAlphaPremultipliedLast | Mac OS X |
RGB | 64 bpp, 16 bpc, kCGImageAlphaNoneSkipLast | Mac OS X |
RGB | 128 bpp, 32 bpc, kCGImageAlphaNoneSkipLast 或kCGBitmapFloatComponents | Mac OS X |
RGB | 128 bpp, 32 bpc, kCGImageAlphaPremultipliedLast 或kCGBitmapFloatComponents | Mac OS X |
CMYK | 32 bpp, 8 bpc, kCGImageAlphaNone | Mac OS X |
CMYK | 64 bpp, 16 bpc, kCGImageAlphaNone | Mac OS X |
CMYK | 128 bpp, 32 bpc, kCGImageAlphaNone 或kCGBitmapFloatComponents | Mac OS X |
对于 iOS 来说,只支持 8 种像素格式。其中颜色空间为 Null 的 1 种,Gray 的 2 种,RGB 的 5 种,CMYK 的 0 种。换句话说,iOS 并不支持 CMYK 的颜色空间
CGBitmapContextCreate
上面说了这么多参数,那么那个函数会用到呢?答案是 CGBitmapContextCreate
:
1 | /* Create a bitmap context. The context draws into a bitmap which is `width' |
顾名思义,这个函数用于创建一个位图上下文,用来绘制一张宽 width 像素,高 height 像素的位图。各参数使用如下:
- data :如果不为 NULL ,那么它应该指向一块大小至少为 bytesPerRow * height 字节的内存;如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可;
- width 和 height :位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
- bitsPerComponent :像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;
- bytesPerPixel:表示一个像素点有多少个字节组成,上面的方法注释中提到了一个公式
(bitsPerComponent * number of components + 7)/8
,即一个像素点的字节数量与表示当前图像的颜色的颜色分量数量和每个分量的位数有关 - bytesPerRow :位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化
- space :就是我们前面提到的颜色空间,一般使用 RGB 即可;
- bitmapInfo :就是我们前面提到的位图的布局信息。
文章开头说 解压缩后的图片大小 = 图片的像素宽 34 * 图片的像素高 34 * 每个像素所占的字节数4
; 我们的手机一般支持 RGB 颜色空间,现在就是知道了 RGB 颜色空间中,实际上是有 4 个分量,所以我们可以算出 bytesPerPixel = (8 * 4 + 7)/8 = 4B。所以我们现在知道为什么最开始的时候我们计算位图的大小的时候,每个像素的大小我们使用的值是 4B 了。事实上不同的颜色空间下,上面这些值都是不同的,但是一般在手机上,我们使用 RGB 颜色空间,所以差不多就是上面的值。
YYImage中使用
现在来看下YYImage中的解码代码 ,先使用 CGBitmapContextCreate
函数创建一个位图上下文;再用 CGContextDrawImage
函数将原始位图绘制到上下文中;最后使用 CGBitmapContextCreateImage
函数创建一张新的解压缩后的位图。
1 |
|
SDWebImage中使用
1 | + (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image { |
SDWebImage 中和其他不一样的地方,就是如果一张图片有 alpha 分量,那就直接返回原始图片,不再进行解码操作。我猜测作者这样写,是不是因为觉得对于有 alpha 分量的图片,因为上下文创建中那些参数的缘故,解码之后可能会与原始图像有偏差,干脆就不解码了。
SDWebImage 在解码操作外面包了 autoreleasepool,这样在大量图片需要解码的时候,可以使得局部变量尽早释放掉,不会造成内存峰值过高。其他创建上下文,然后调用 CGContextDrawImage,再调用CGBitmapContextCreateImage获取创建后的位图,和 YYImage 基本一样,就是个别参数设置不同。