图片渲染优化

图片渲染是iOS app中必不可少的一环,当我们在屏幕上展示一幅图片的时候,一个 workflow 应该是这样的:

  • 拿到图片的 Data buffer
  • decode 得到图片的 Image buffer
  • 将 Image buffer 渲染到屏幕上

这个流程看上去没有问题,但是有一点值得我们注意。假设我们要显示一张 2000 1000 的图片,在decode环节我们decode出来的 image buffer 的大小就会是 2000 1000 4 byte = 7.6MB,这里我们会发现 image buffer 的大小和图片的分辨率是正相关的,但有的时候,图片和最终展示在界面上的尺寸是不同的。比如这张 2000 1000 的图片,假如是展示在一个 200 * 100 的 view 上,那显然有很多像素信息是会被浪费掉的。

如何解决这个问题呢?

Downsampling

我们的思路可能是说建立一个新的 workflow :

  • 拿到图片的 Data buffer
  • 根据显示区域的大小建立一个 thumbnail 区域
  • decode 图片的 Data buffer 到这个 thumbnail 区域,得到一个小一些的 Image buffer
  • 将 Image buffer 渲染到屏幕上

WWDC18-session219 详细介绍了如何去进行这样的 downsampling,同事值得注意的是我们可以将 downsampling 的过程放在后台线程进行,再将结果回调到主线程进行展示,这也很大程度上能缓解 CPU 的压力。

Rasterization

别误会,这里的 rasterization 并不是 CALayer 的概念。我们在图片渲染上头疼的点在于图片像素尺寸和界面上展示的尺寸很可能是不同的。这里其实我们还会想到一种方案来解决:使用矢量图。

矢量图和位图不同,矢量图并不存储图片的像素信息,它是通过数学表达的方式通过点和曲线的连接来构成图形。图形层显示矢量图会进行 rasterization 来将矢量图信息转换成位图的信息并直接渲染到 frame buffer 上。

在 iOS 13 以前,我们使用矢量图的方案是通过在 image assets 添加PDF文件来实现。Xcode 会在编译期来进行 rasterization 将图片转换成位图(当然这样做也失去了矢量图的一些特性)。

在 iOS 13之后,我们可以通过私有库 CoreSVG 进行 SVG 的渲染,在未来,我们也有希望使用 SVG Native 来进行规范化的矢量图形渲染。

Rendering Speed

通过上面的做法,我们基本已经避免了在渲染层面上浪费过多的内存的情况,那么我们如何再进一步去优化渲染速度呢?

从 iOS 10 开始, UICollectionView 就支持了通过 UICollectionViewDataSourcePrefetching 来通过预加载来异步获取数据的方案。我们可以通过预加载来提前在后台线程执行图片的获取和解码,在用户视角上提高渲染的速度。

还有一种方案也是通过提前解码来实现加速渲染的,那就是很多第三方库所使用的预解码技术。

在这之前我们先了解一下 Core Animation 的渲染原理。

用人话来说就是对于 UIImageView 来说,渲染流程是这样的:

  • 拿到 UIImageViewlayer.contents
  • 从拿到的 CGimage decode 获取 image buffer
  • 向GPU发送 Draw Calls
  • GPU进行渲染然后将结果展示在硬件上

那么我们的优化方向就是如何让 render server 提前拿到 image buffer,这样就能加快渲染速度了。这里我们就要说说 CGImage 这个东西了。

CGImage

我们先看看 CGImage 的初始化方法:

1
2
3
4
5
6
7
8
9
10
11
init?(width: Int, 
height: Int,
bitsPerComponent: Int,
bitsPerPixel: Int,
bytesPerRow: Int,
space: CGColorSpace,
bitmapInfo: CGBitmapInfo,
provider: CGDataProvider,
decode: UnsafePointer<CGFloat>?,
shouldInterpolate: Bool,
intent: CGColorRenderingIntent)

值得注意的是这个 provider,就是通过它来生成渲染所需要的 image buffer。经过研究,发现这里创建出 CGImage 但并没有进行 decode,而是在需要显示渲染的时候通过调用 CGDataProviderCopyData 来触发decode。所以我们这时列一下整个解码流程:

  • 获取 layer.contents
  • 获取 image.cgImage
  • CGImageGetDataProvider 获取data
  • CGDataProviderRetainBytePtr 触发解码
  • CGDataProviderDirectCallbacks 拿到数据
  • ImageIO进行解码拿到 image buffer

了解了整个流程我们就知道了 iOS 系统的方案在生成 CGImage 的时候并没有创建完整的 image buffer ,而是在渲染的时候进行处理。那我们优化的方向就是用空间换时间,如果能在生成 CGImage 生成的时候直接把 image buffer 创建好,那渲染就会快很多了。

主流图片库的做法也很简单,通过CGContextDrawImage 绘制一遍生成出来的 CGImage 就可以了。

总结

这篇文章主要讲述了三种图片渲染上优化的方向,希望可以和对图片渲染有兴趣的朋友多多交流。