iOS图片加载优化二三事

前言

一张图片从引入project中,到最后展示在用户面前,经历了许多环节。其中压缩解压缩就是一个值得我们探究的环节。

开始之前,我们需要了解一些基本的图像原理。平时开发中接触的最多的当属png格式的图片,其次就是jpg。这两种文件格式本质上是图片的压缩格式。区别在于png是无损压缩,支持alpha通道,也就是透明,而jpg是有损压缩。事实上,UIKit中就有两个API来生成pngjpg

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

这也符合我们的预期,图片越大,文件越大。

图片加载的工作流

  1. 假设我们使用 +imageWithContentsOfFile: 方法从磁盘中加载一张图片,这个时候的图片并没有解压缩;
  1. 然后将生成的 UIImage 赋值给 UIImageView ;
  2. 接着一个隐式的 CATransaction 捕获到了 UIImageView 图层树的变化;
  3. 在主线程的下一个 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信息提供了:

  1. 是否包含 alpha ;
  1. 如果包含 alpha ,那么 alpha 信息所处的位置,在像素的最低有效位,比如 RGBA ,还是最高有效位比如 ARGB ;
  2. 如果包含 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
即可;
widthheight:位图的宽度和高度,分别赋值为图片的像素宽度和像素高度即可;
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/

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容

  • 知道了那么多关于iOS上界面渲染的理论知识后,终于可以回归最开始的问题,将一张 png/jpg 格式的图片渲染到页...
    巫师学徒阅读 699评论 0 2
  • 绘制像素到屏幕上 answer-huang22 Mar 2014 分享文章 一个像素是如何绘制到屏幕上去的?有很多...
    阿狸旅途T恤阅读 1,619评论 0 7
  • 有很多种framework以及很多种方法的组合可以在屏幕上渲染UI元素,我们在这里讨论这个过程中发生的事情,希望这...
    纵横而乐阅读 4,479评论 4 25
  • 谁吃掉我们的CPU: 方法CA::Render::create_image_from_provider 图片预解码...
    神采飞扬_2015阅读 3,186评论 2 6
  • 卷首语 欢迎来到 objc.io 的第三期! 这一期都是关于视图层的。当然视图层有很多方面,我们需要把它们缩小到几...
    评评分分阅读 1,755评论 0 18