iOS图片-位图信息与图片解码

介绍下几种iOS中位图信息 与相关解压缩
ref 谈谈 iOS 中图片的解压缩
ref Quartz 2D Programming Guide

我们从网络下载或者从本地磁盘加载一张图片到屏幕上显示,要经过图片的解码过程,为什么呢?因为我们一般的图片格式例如 JPEG,PNG都是经过压缩后的图片,而显示在屏幕上的图片叫做位图(bitmap),所谓的解码就是把压缩后的图片变成位图。

为什么非要解码成位图才能显示呢?因为位图又被叫做点阵图像,也就是说位图包含了一大堆的像素点信息,这些像素点就是该图片中的点,有了图片中每个像素点的信息,就可以在屏幕上渲染整张图片了。

那我们为什么还需要不同格式的各种图片呢?直接全部用位图不就好了?那不就不需要每次解码了?那些JPEG以及PNG等其实都是图像的压缩格式,我们都知道压缩的意思就是减小空间,所以我们可以想到,使用这些格式的原因就是位图实在太大了。

图片解压后的数据变化

我们来使用这张图片和以下代码做例子:


该图片的二进制信息为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452  .PNG........IHDR
00000010: 0000 0022 0000 0022 0803 0000 000d 99fb ..."..."........
00000020: f000 0000 5d50 4c54 4500 0000 ffff ffff ....]PLTE.......
00000030: ffff ffff ffff ffff ffff ffff ffff ffff ................
00000040: ffff ffff ffff ffff ffff ffff ffff ffff ................
00000050: ffff ffff ffff ffff ffff ffff ffff ffff ................
00000060: ffff ffff ffff ffff ffff ffff ffff ffff ................
00000070: ffff ffff ffff ffff ffff ffff ffff ffff ................
00000080: ffff ffff ffff 512a fbe5 0000 001e 7452 ......Q*......tR
00000090: 4e53 001c b890 7103 faa9 580d f7c8 09f0 NS....q...X.....
000000a0: be42 1685 78e2 d4c5 b1a1 604d 492d 1e0f .B..x.....`MI-..
000000b0: 5dbe 24b2 0000 0084 4944 4154 38cb edcf ].$.....IDAT8...
000000c0: cb0a c240 1044 d1d2 38ad 8e19 4de2 fb55 ...@.D..8...M..U
000000d0: ffff 99d2 4d70 971a 5097 de5d c381 a630 ....Mp..P..]...0
000000e0: d113 952e 7b96 be55 a2a1 b76d b538 981b ....{..U...m.8..
000000f0: 2516 d865 374a 6034 428c a611 228c 3127 %..e7J`4B...".1'
00000100: 21bc 23f9 d062 4596 a485 9143 552c d35f !.#..bE....CU,._
00000110: 7c27 faaa 4856 1358 939b f7d1 e510 9298 |'..HV.X........
00000120: 0b49 70bf 2548 127d 4aca 7cba d335 46cb .Ip.%H.}J.|..5F.
00000130: 0a80 4193 73bc 9a89 3afc a817 5dd5 1a7c ..A.s...:...]..|
00000140: 84ac 48f9 0000 0000 4945 4e44 ae42 6082 ..H.....IEND.B`.

获取图片解压信息的代码为:

1
2
3
4
5
6
7
8
NSString * path = [[NSBundle mainBundle]pathForResource:@"testImg2" ofType:@"png"];
NSURL * url = [NSURL fileURLWithPath:path];
CGImageSourceRef myImageSource = CGImageSourceCreateWithURL((CFURLRef)url, NULL);
CGImageRef myImage = CGImageSourceCreateImageAtIndex(myImageSource,0,NULL);
CFRelease(myImageSource);

CFDataRef rawData = CGDataProviderCopyData(CGImageGetDataProvider(myImage));
NSLog(@"rawData %@",rawData);

这张图片像素尺寸为34x34,文件大小为 336Byte ;而获取到图片原始像素数据rawData,大小为4624byte。以下为rawData信息 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 
...

00000000 ffffff90 ffffffff ffffffff ffffffff ffffff71 00000000 ffffff42 fffffff7
ffffffff ffffffff ffffffbe ffffff09 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 ffffff90 ffffffff
ffffffff ffffffff ffffff71 00000000 00000000 00000000 ffffff42 fffffff7 ffffffff
ffffffff ffffffbe ffffff09 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 00000000 00000000 00000000 ffffff90 ffffffff ffffffff ffffffff ffffff71
00000000 00000000 00000000 00000000 00000000 ffffff42 fffffff7 ffffffff ffffffff
ffffff90 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000
00000000 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
2
3
4
5
6
7
8
9
10
11
12
13
typedef CF_OPTIONS(uint32_t, CGBitmapInfo) {
kCGBitmapAlphaInfoMask = 0x1F,

kCGBitmapFloatInfoMask = 0xF00,
kCGBitmapFloatComponents = (1 << 8),

kCGBitmapByteOrderMask = kCGImageByteOrderMask,
kCGBitmapByteOrderDefault = kCGImageByteOrderDefault,
kCGBitmapByteOrder16Little = kCGImageByteOrder16Little,
kCGBitmapByteOrder32Little = kCGImageByteOrder32Little,
kCGBitmapByteOrder16Big = kCGImageByteOrder16Big,
kCGBitmapByteOrder32Big = kCGImageByteOrder32Big
} CG_AVAILABLE_STARTING(10.0, 2.0);

它主要提供了三个方面的布局信息:

  • alpha 的信息 CGImageAlphaInfo;
  • 颜色分量是否为浮点数;
  • 像素格式的字节顺序 Byte Ordering。

透明信息 CGImageAlphaInfo

其中,alpha 的信息由枚举值 CGImageAlphaInfo 来表示:

1
2
3
4
5
6
7
8
9
10
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 信息:

  • 是否包含 alpha ;
  • 如果包含 alpha ,那么 alpha 信息所处的位置,在像素的最低有效位,比如 RGBA ,还是最高有效位,比如 ARGB ;
  • 如果包含 alpha ,那么每个颜色分量是否已经乘以 alpha 的值,这种做法可以加速图片的渲染时间,因为它避免了渲染时的额外乘法运算。比如,对于 RGB 颜色空间,用已经乘以 alpha 的数据来渲染图片,每个像素都可以避免 3 次乘法运算,红色乘以 alpha ,绿色乘以 alpha 和蓝色乘以 alpha 。

字节顺序 CGImageByteOrderInfo

字节的排列方式有两个通用规则。例如,一个多位的整数,按照存储地址从低到高排序的字节中,如果该整数的最低有效字节(类似于最低有效位)在最高有效字节的前面,则称小端序;反之则称大端序。

字节顺序的信息由枚举值 CGImageByteOrderInfo 来表示:

1
2
3
4
5
6
7
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 位为单位。

下图表示了 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/* 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 像素的位图。各参数使用如下:

  • 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
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

CGImageRef YYCGImageCreateDecodedCopy(CGImageRef imageRef, BOOL decodeForDisplay) {
...
if (decodeForDisplay) { //decode with redraw (may lose some precision)
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(imageRef) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// BGRA8888 (premultiplied) or BGRX8888
// same as UIGraphicsBeginImageContext() and -[UIView drawRect:]
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, YYCGColorSpaceGetDeviceRGB(), bitmapInfo);
if (!context) return NULL;
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef); // decode
CGImageRef newImage = CGBitmapContextCreateImage(context);
CFRelease(context);
return newImage;

} else {
...
}
}

SDWebImage中使用

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
+ (nullable UIImage *)decodedImageWithImage:(nullable UIImage *)image {
if (![UIImage shouldDecodeImage:image]) {
return image;
}

// autorelease the bitmap context and all vars to help system to free memory when there are memory warning.
// on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory];
@autoreleasepool{

CGImageRef imageRef = image.CGImage;
CGColorSpaceRef colorspaceRef = [UIImage colorSpaceForImageRef:imageRef];

size_t width = CGImageGetWidth(imageRef);
size_t height = CGImageGetHeight(imageRef);
size_t bytesPerRow = kBytesPerPixel * width;

// kCGImageAlphaNone is not supported in CGBitmapContextCreate.
// Since the original image here has no alpha info, use kCGImageAlphaNoneSkipLast
// to create bitmap graphics contexts without alpha info.
CGContextRef context = CGBitmapContextCreate(NULL,
width,
height,
kBitsPerComponent,
bytesPerRow,
colorspaceRef,
kCGBitmapByteOrderDefault|kCGImageAlphaNoneSkipLast);
if (context == NULL) {
return image;
}

// Draw the image into the context and retrieve the new bitmap image without alpha
CGContextDrawImage(context, CGRectMake(0, 0, width, height), imageRef);
CGImageRef imageRefWithoutAlpha = CGBitmapContextCreateImage(context);
UIImage *imageWithoutAlpha = [UIImage imageWithCGImage:imageRefWithoutAlpha
scale:image.scale
orientation:image.imageOrientation];

CGContextRelease(context);
CGImageRelease(imageRefWithoutAlpha);

return imageWithoutAlpha;
}
}

+ (BOOL)shouldDecodeImage:(nullable UIImage *)image {
// Prevent "CGBitmapContextCreateImage: invalid context 0x0" error
if (image == nil) {
return NO;
}

// do not decode animated images
if (image.images != nil) {
return NO;
}

CGImageRef imageRef = image.CGImage;

CGImageAlphaInfo alpha = CGImageGetAlphaInfo(imageRef);
BOOL anyAlpha = (alpha == kCGImageAlphaFirst ||
alpha == kCGImageAlphaLast ||
alpha == kCGImageAlphaPremultipliedFirst ||
alpha == kCGImageAlphaPremultipliedLast);
// do not decode images with alpha
if (anyAlpha) {
return NO;
}

return YES;
}

SDWebImage 中和其他不一样的地方,就是如果一张图片有 alpha 分量,那就直接返回原始图片,不再进行解码操作。我猜测作者这样写,是不是因为觉得对于有 alpha 分量的图片,因为上下文创建中那些参数的缘故,解码之后可能会与原始图像有偏差,干脆就不解码了。

SDWebImage 在解码操作外面包了 autoreleasepool,这样在大量图片需要解码的时候,可以使得局部变量尽早释放掉,不会造成内存峰值过高。其他创建上下文,然后调用 CGContextDrawImage,再调用CGBitmapContextCreateImage获取创建后的位图,和 YYImage 基本一样,就是个别参数设置不同。