WWDC2018-219 图片与图像的最佳处理

429 阅读7分钟

Session 219 Image and Graphics Best Practices

内存与CPU使用量

  1. 使用 CPU 越多,会减少电池使用时间和增大app响应时间
  2. 使用 memory 越多,会导致 Fragmentation,系统会进行 Memory Compression,需要调用CPU进行。进而使用 CPU 也增多,从而同样地导致电池和响应时间

UIImage & UIImageView

首先我们需要了解一下Buffer的概念,因为图片被加载后都是以buffer的形式存在于内存中。

什么是Buffer

Buffer: 一段连续的内存空间。 Image Buffer: 图片的在内存中的保存方式,保存了图片每一个像素的颜色。大小和图片的大小成正比。 Frame Buffer: Frame Buffer 保存的是屏幕上展示的实际内容,保存的是屏幕上每一个像素的颜色。当UI改变时,会改变Frame Buffer的内容,硬件会读取Frame Buffer的内容,并展示在屏幕上,以60Hz-120Hz的频率刷新。 Data Buffer: 下载的图片或者从磁盘加载的图片会以 Data Buffer 的形式存在,Data Buffer 保存的图片是被编码过的,例如, png,jpeg等,所以data buffer的内容不是直接描述像素信息 根据 MVC 的架构分析,UIImage 就是 Model,负责加载图片;UIImageView 就是 View 负责展示渲染图片,

图片加载过程

这里需要注意的两点是:

  1. 渲染是一个持续的行为,而不是一个一次性的动作。
  2. 渲染和加载之间存在一个 Decode 的过程,这个过程是把图片加载成 image buffer 并保存在内存中。
  3. 当UI没变时,Frame buffer 不会改变。当 UI 改变时,UIKit 会重新给Frame buffer 赋值,从而硬件重新读取frame buffer 内容,加载到屏幕上。

Decode 的过程

当我们加载一个图片时,会将其以 Data Buffer 的形容加载出来,但是因为我们的 Frame Buffer 需要的是像素信息,所以我们需要将 Data Buffer 解码成 Image Buffer。 最后,当我们把这段 Image Buffer 赋值给 ImageView 并添加到视图时,会把这段 Image Buffer 加载到 Frame Buffer 并进行相应的缩放处理。最后由硬件读取 Frame Buffer 并展示到屏幕上。

解码过程的几个问题

  1. 解码过程耗费CPU
  2. 因为需要重复渲染,所以需要持有image buffer避免重复进行decode的过程
  3. 大量的内存被image buffer 占用
  4. image buffer 的大小和原始图片尺寸有关,而不是最终展示的view的大小
  5. 在scrollable view中的解码问题,图片解码的过程很耗费CPU。所以在collectionview之类的view上,当用户滑动时,当CPU开始解码下一阶段所需的图片时,会导致CPU峰值突然上涨,然后下降,这样的处理对电量的影响很大。同时,在主线程进行大量这些操作会堵塞主线程,影响app响应时间。

综上,当app内加载大量的图片时,可能会导致内存里存在大量的image buffer。最终导致memory fragmentation,此时,系统会进行内存压缩,需要CPU参与。最终导致影响电池时间和app响应时间。

解决方案

Downsampling:向下采样

前面提到的,CoreAnimation 最后在渲染时会把图片缩放到实际显示的尺寸,但是由于我们加载的 image buffer 是原始图片的尺寸,所以我们的内存消耗的还是原始图片的尺寸。对于这种情况我们可以在解码时,按照实际显示尺寸取图片的 thumbnail 并加载到image buffer,这样做的好处就是,加载的image buffer对应的是我实际要显示的图片大小,而不是原始图片大小,不会过多的消耗内存。

下面是 downsampling 的代码示例:

2. Prefetching: 预加载

在collectionview/tableView中,预加载可以让datasource 在空闲时预先加载下一阶段所需的内容,这样,我们进而将CPU的使用分散开,不形成较大的峰值,同时,优化app的响应时间,因为内容可以提前加载完成

可以通过 collectionView(prefetchItems) 进行实现:

3. Background decoding/downsampling:在后台线程进行图片解码工作

同样的,我们可以把编码的CPU损耗分散在后台线程,进而不影响主线程。

通过上面的代码,我们把解码分配到了后台线程,但是这里可能会产生一个问题。 当我们实际要处理的任务数大于设备有的CPU数时,每次有新任务时,CPU会创建一个新的Thread,然后CPU会在这些thread中切换处理任务。最终导致每一个thread处理图片的时间减少了。

总体来上,降低了我们预处理图片的效率,因为在预处理阶段,我们需要的是以最快的速度,尽量预先处理好一个图片。从而优化app响应时间。

这种任务大于CPU数量情况,叫做Thread Explosion:

对于这种情况,我们可以通过创建同步线程来处理:

确实,通过这样的处理,我们的每次新增的任务需要等待上一个任务结束后才能进行,但是这样我们能尽可能的快速将图片预加载出来。

Image Source: 图片来源

项目中图片的来源有很多种,包括:

  1. 保存在 asset catalog
  2. 保存在 application/framework bundle
  3. 保存在 Documents 或 Caches 目录
  4. 从网络上下载

保存在项目里的图片,强烈建议使用 asset catalog 进行保存,原因是:

  1. Optimized name- and trait-based lookup: 读取加载更快
  2. Smarter buffer caching: 运行时有更好的buffer管理机制
  3. Per-device thinning:不同的设备只会加载所需的图片类型,有效降低包体
  4. Vector artwork: 矢量图的优化

Vector Artwork Optimizations: 矢量图优化

从iOS11开始,asset catalog 支持 “Preserve Vector Data”,当矢量图别拉伸时,不会变的模糊。因为矢量图会对图片进行光栅化。

  1. 当渲染原始尺寸矢量图时,不会进行光栅化,因为asset catalog在编译时,已经预先对原始尺寸进行了光栅化,使用时只需要进行解码工作
  2. 如果你使用的是已知固定尺寸的图片,不应该使用矢量图优化,而应该添加多个不同尺寸的图片资源。因为光栅化的过程耗费CPU

Custom drawing with UIKit: 自定义绘制

当需要自定义UI控件时,有时候我们会复写view 的 draw() 方法进行绘制,但是这样的操作会导致内存上涨。

上图是UIKit渲染的过程。每个UIView都是结合一个CALayer进行图层内容的展示,正常情况下,UIImageView会加载出图片的image buffer 然后提供给CALyer进行展示。

然而当复写了draw()方法,情况就不一样了。这时候,CALayer会先生成一个 Backing store,提供给UIView 进行内容的绘制。也就是说,这时候需要创建一段Backing store 的内存,内存的大小是和view的大小成正比的

所以,基于上述的讨论,我们需要减少我们对draw()的复用,以下是一些可行的办法。

还有几点小技巧:

  1. 设置background不会耗费多余内存,但是如果使用的是patternColor,会导致创建backing store
  2. 使用layer.cornerRadius设置圆角,使用maskView或maskLayer的话会导致消耗多余内存,因为要创建mask。
  3. 使用 resizableImage,而不是maskView,来制作透明的背景图层。
  4. 使用UILabel会减少75%的backingStore内存。

其他

Session中还提到了其他的一些小技巧,包括离屏渲染,结合UIImage和CIImage的使用等,但是都是一笔带过,所以这里不赘述了。

总结

  1. 使用预加载来异步准备好UI内容,同时使用同步线程来避免thread explosion
  2. 减少draw()的复用和backing store的创建,来减少内存消耗
  3. 推荐使用asset catalog,而不是把图片直接存在bundle中
  4. 不要太依赖使用 Preserve Vector Data

Take Away

  1. 使用内存过多时,系统会进行压缩内存,涉及CPU
  2. 图片的读取加载占用内存和原始图片大小有关,而不是显示大小
  3. 使用prefetching优化app响应时间
  4. 当多线程任务数大于CPU数时,会导致任务被处理的时间减少,可以使用同步线程解决 4.复写draw()会导致layer创建一层backing store,耗费内存

引用

Image and Graphics Best Practices - WWDC 2018 - Videos - Apple Developer