前言
一张图片从引入project中,到最后展示在用户面前,经历了许多环节。其中压缩解压缩就是一个值得我们探究的环节。
开始之前,我们需要了解一些基本的图像原理。平时开发中接触的最多的当属png
格式的图片,其次就是jpg
。这两种文件格式本质上是图片的压缩格式。区别在于png
是无损压缩,支持alpha通道,也就是透明,而jpg
是有损压缩。事实上,UIKit中就有两个API来生成png
和jpg
// return image as PNG. May return nil if image has no CGImageRef or invalid bitmap format
UIKIT_EXTERN NSData * __nullable UIImagePNGRepresentation(UIImage * __nonnull image);
// return image as JPEG. May return nil if image has no CGImageRef or invalid bitmap format. compression is 0(most)..1(least)
UIKIT_EXTERN NSData * __nullable UIImageJPEGRepresentation(UIImage * __nonnull image, CGFloat compressionQuality);
compressionQuality
指的就是压缩质量。
通过以下方法我们可以获取到原始图像的数据,经过比较远大于图片本身大小。也就是说经过解压缩以后,图片的大小得到了进一步的增大。
UIImage *image = [UIImage imageNamed:@"xxx"];
CFDataRef origenal = CGDataProviderCopyData(CGImageGetDataProvider(image.CGImage));
事实上,图片是由一个个像素点组成的一个集合,所以我们可以很容易得出:
解压缩后的图片大小 = 图片的像素宽 * 图片的像素高 * 每个像素所占的字节数 4
这也符合我们的预期,图片越大,文件越大。
图片加载的工作流
- 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
- 然后将生成的 UIImage 赋值给 UIImageView ;
- 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
- 在主线程的下一个 run loop 到来时,Core Animation 提交了这个隐式的 transaction ,这个过程可能会对图片进行 copy 操作,而受图片是否字节对齐等因素的影响,这个 copy 操作可能会涉及以下部分或全部步骤:
- 分配内存缓冲区用于管理文件 IO 和解压缩操作;
- 将文件数据从磁盘读到内存中;
- 将压缩的图片数据解码成未压缩的位图形式,这是一个非常耗时的 CPU 操作;
- 最后 Core Animation 使用未压缩的位图数据渲染 UIImageView 的图层。
在上面的步骤中,我们提到了图片的解压缩是一个非常耗时的 CPU 操作,并且它默认是在主线程中执行的。那么当需要加载的图片比较多时,就会对我们应用的响应性造成严重的影响,尤其是在快速滑动的列表上,这个问题会表现得更加突出。
强制解压缩
通过图片加载的工作流我们发现,在执行图片解压缩这一步不可避免骤时,主线程会有不同程度的阻塞,从而影响到程序的流畅性。
很容易能够联想到的解决方法就是通过将这一耗时操作放到子线程中由我们自己接管解压缩的流程,从而避免在主线程中去执行这个耗时的操作。
如果图片已经解压缩,系统就不会再对图片进行解压缩,因此这个问题能够得到优化。
而强制解压缩的原理就是对图片进行重新绘制,得到一张新的解压缩后的位图。其中,用到的最核心的函数是:
/* 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 :位图中的每一行使用的字节数。```
像素格式的组合并不是随机的,而是存在一定的模式,在目前MacOS & iOS环境下,支持[十七种像素格式的组合](https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB)
![Supported Pixel Formats.png](http://upload-images.jianshu.io/upload_images/1677365-114f0fcfe703a605.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
从上图可知,对于 iOS 来说,只支持 8 种像素格式。其中颜色空间为 Null 的 1 种,Gray 的 2 种,RGB 的 5 种,CMYK 的 0 种。换句话说,iOS 并不支持 CMYK 的颜色空间。另外,在表格的第 2 列中,除了像素格式外,还指定了 `bitmap information constant `
####[Color and Color Spaces](https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_color/dq_color.html#//apple_ref/doc/uid/TP30001066-CH205-TPXREF101)
在 Quartz 中,一个颜色是由一组值来表示的,如图所示,而颜色空间则是用来说明如何解析这些值的,离开了颜色空间,它们将变得毫无意义。
![](http://upload-images.jianshu.io/upload_images/1677365-6747f6b7bb55fe12.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
表中的值最后的结果都是蓝色,但是我们可以看到相同的颜色在不同的色彩空间中所需要的值是不同的。
![左边BGR,右边RGB](http://upload-images.jianshu.io/upload_images/1677365-22d9c1798f6930dc.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
这张图很形象的展示了相同的值在不同颜色空间下的结果是截然不同的。
####[Bitmap Layout](https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_images/dq_images.html#//apple_ref/doc/uid/TP30001066-CH212-CJBHEGIB)
```objectivec
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);
位图布局信息,是为了让Quartz正确的解释每个像素的信息。
其中主要包含了三点内容:
- 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信息提供了:
- 是否包含 alpha ;
- 如果包含 alpha ,那么 alpha 信息所处的位置,在像素的最低有效位,比如 RGBA ,还是最高有效位比如 ARGB ;
- 如果包含 alpha ,那么每个颜色分量是否已经乘以 alpha 的值,这种做法可以加速图片的渲染时间,因为它避免了渲染时的额外乘法运算。比如,对于 RGB 颜色空间,用已经乘以 alpha 的数据来渲染图片,每个像素都可以避免 3 次乘法运算,红色乘以 alpha ,绿色乘以 alpha 和蓝色乘以 alpha 。
根据 Which CGImageAlphaInfo should we use和官方文档中对UIGraphicsBeginImageContextWithOptions
函数的讨论:
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
。
而颜色分量上,kCGBitmapFloatComponents
或者直接使用值就行
上文官方文档中提到的字节顺序应该使用 32 位的主机字节顺序 kCGBitmapByteOrder32Host
,这个参数来源于官方准备的宏,虽然iPhone用的是16位小端模式,但是通过此使用此宏来适配不管什么设备,字节顺序始终是对的。
#ifdef __BIG_ENDIAN__
#define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Big
#define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Big
#else /* Little endian. */
#define kCGBitmapByteOrder16Host kCGBitmapByteOrder16Little
#define kCGBitmapByteOrder32Host kCGBitmapByteOrder32Little
#endif
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);
在这里,大家可以参考官方文档-字节顺序
总结:
data
:如果不为 NULL,那么它应该指向一块大小至少为bytesPerRow * height
字节的内存;如果 为 NULL,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL
即可;
width
和height
:位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
bitsPerComponent
:像素的每个颜色分量使用的 bit 数,在 RGB 颜色空间下指定 8 即可;
bytesPerRow
:位图的每一行使用的字节数,大小至少为width * bytes per pixel
字节。有意思的是,当我们指定 0 时,系统不仅会为我们自动计算,而且还会进行 cache line alignment 的优化,更多信息可以查看 what is byte alignment (cache line alignment) for Core Animation? Why it matters? 和 Why is my image’s Bytes per Row more than its Bytes per Pixel times its Width? ,亲测可用;
space
:就是我们前面提到的颜色空间,一般使用 RGB 即可;
bitmapInfo
:就是我们前面提到的位图的布局信息。
最后
图片解压缩的仅仅在于这一函数方法的使用,重点在于理解位图包含的信息以及构成,在程序中合理使用函数,将有助于我们缩短图片加载时间,更优化App的性能。
=================
REFRENCE
https://www.cocoanetics.com/2011/10/avoiding-image-decompression-sickness/ https://developer.apple.com/library/content/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/Introduction/Introduction.html https://github.com/path/FastImageCache http://stackoverflow.com/questions/23790837/what-is-byte-alignment-cache-line-alignment-for-core-animation-why-it-matters
http://blog.leichunfeng.com/blog/2017/02/20/talking-about-the-decompression-of-the-image-in-ios/