理解离屏渲染

本文关注如下几个问题

  • iOS 平台上离屏渲染的概念?
  • 为什么需要离屏渲染?
  • 离屏渲染到底好还是不好?能避免离屏渲染吗?如何优化?

之前聊聊iOS渲染 (opens new window)这篇文章里介绍了正常情况下渲染视图的架构方式,如图,那什么是离屏渲染?

Untitled

# 离屏渲染

离屏渲染这个概念不光是 iOS 独有的,Windows 上也有类似的概念 (opens new window)

离屏渲染的过程是**「GPU 因为一些原因,没办法把渲染出来的结果直接输出给帧缓冲器(frame buffer),而是先暂存在别的内存区域,之后再写入帧缓冲区」。**

即正常流程是这样子的

Untitled

离屏渲染后

Untitled

一个误区:我们通过在 drawRect 里面进行的绘制,并不是离屏渲染,那儿的绘制主要是通过 Quartz2D 图形库在 CGContext 维护的内存(backing store)里进行的绘制。距离 UIKit 工程师说的真正的 GPU 离屏渲染还差的远呢。参考 即刻技术-关于iOS离屏渲染的深入研究 (opens new window)

# 为什么需要离屏渲染

先说正常的渲染方式,在上篇文章 (opens new window)里我们提到一个重要的渲染过程的参与者「渲染服务器」,主要的渲染操作就是渲染服务器拿到 CoreAnimation 提交的图层树(layer tree)并解码后,通过 OpenGL 或者 Metal 提交命令去让 GPU 渲染对应的图层。

具体的渲染方法是参考「画家算法」,GPU 一层层的渲染,并且将渲染结果放到帧缓冲器里,后面绘制结果会覆盖掉前面的结果,而每一层绘制完之后这一层的内容就会被丢弃。这意味着整个渲染的过程是不可逆的。

PS:没找到 iOS 官方说明的渲染方法,可能画家算法是业内通用的 GPU 绘制方式?

需要离屏渲染的原因就是普通的画家算法不能满足一些绘图的场景,比如下图这种互相遮挡的绘图场景,我应该先绘制谁呢?好像先绘谁也不好使。

Untitled

# 离屏渲染到底好还是不好?能避免离屏渲染吗?

离屏渲染相对于普通的渲染流程来说肯定是更加消耗资源的,需要单独分配和管理内存,而且打乱了原始的 GPU 设计好的渲染管线流程。最后将内存中的 OffscreenFrameBuffer 拷贝到待显示的 FrameBuffer 这一步也是消耗 CPU 资源的。

但是有的时候离屏渲染是不可避免的,有一些效果就是需要离屏渲染才能做到。这时候我们能做的就是尽量降低离屏渲染带来的性能损耗。

CALayer 提供了一种优化方案就是「光栅化」。CALayer 提供了属性 shouldRasterize 来让开发者设置光栅化开关。光栅化的效果是让渲染服务器(render server)将layer的渲染结果缓存起来,这样被缓存起来的内容可以再进行复用,从而减少离屏渲染次数,提升性能。

光栅化是指将基于矢量的图像或对象转换为位图格式的过程。它涉及将形状和线条的数学描述转换为像素网格。

当然如果 CALayer 的其他属性修改影响了最终的渲染效果,之前缓存好的内容会被丢弃。所以优化离屏渲染的最佳场景是 CALayer 为静态内容的情况。

iOS 模拟器提供了识别离屏渲染机制的方式,通过打开模拟器的 Debug → Color Off-screen Rendered 的选项,就能查看当前视图是否被离屏渲染了。

Color Off-screen Rendered-Simulator overlays content in yellow that is rendered offscreen. Only use off-screen rendering when you have confirmed with tests that the memory and performance tradeoffs work for your app.

我们知道 CALayer 的一些常见属性是能触发离屏渲染的

  1. 阴影(shadow)相关的属性。

    btn.layer.shadowOffset = CGSize.init(width: 20, height: 20)
    btn.layer.shadowColor = UIColor.black.cgColor
    btn.layer.shadowRadius = 5
    btn.layer.shadowOpacity = 1
    

    在模拟器上展示效果如下,可以看到整个视图都被黄色覆盖,说明视图被离屏渲染了。

    Untitled

    但是通过配置 shadowPath 属性可以避免离屏渲染

    var scale = CGAffineTransform.identity
    btn.layer.shadowPath = CGPath.init(rect: .init(x: 20, y: 20, width: 200, height: 60), transform: &scale)
    btn.layer.shadowColor = UIColor.black.cgColor
    btn.layer.shadowOpacity = 0.5
    btn.layer.shadowRadius = 5
    

    Untitled

  2. **透明度(opacity)相关属性。**多说一句,UIViewalpha 属性设置底层就是通过 Layeropacity 实现的,所以这俩属性我理解除了语义上的区别之外,没啥差别。设置 UIView alpha 属性同样能触发离屏渲染的。这种透明度的设置是对整个子图层树都生效的,所以我理解这也是它需要离屏渲染的原因。

  3. 有争议的圆角属性 cornerRadius 属性。最近的 iOS 系统中使用 cornerRadius 并不会触发离屏渲染。但一些使用场景需要注意

    1. cornerRadius 本身并不会触发离屏渲染,但是如果结合 masksToBounds 属性的化就会触发离屏渲染,如图四个角会被标记为黄色。

      Untitled

    2. 有背景色也可以使用 cornerRadius。因为之前看即刻的技术文章 (opens new window)说,没有backgroundColor 可以使用 cornerRadius。我自己理解就算有 backgoundColor 也能用,但得知道是设置谁的 backgroundColor

      看下面代码,当 ① ③ 组合的时候,cornerRadius 并不对背景色生效;当 ② ③ 组合的时候 cornerRadius 是对背景色生效。

      ① label.backgroundColor = UIColor.gray 
      ② label.layer.backgroundColor = UIColor.gray.cgColor
      ③ label.layer.cornerRadius = 10
      

      因为 UIViewbackgoundColor 本质上是给 CALayercontents 属性填充内容。CALayerbackgoundColorcontents 属性不一样的。

      cornerRadius 作用的是 CALayerbackgoundColorborder,而不对 contents 属性生效。上面两种都不会触发离屏渲染。

    3. 有一些文章推荐使用 CoreGraphics 的方式来进行圆角绘制,如下

      override func draw(_ rect: CGRect) {
          let context = UIGraphicsGetCurrentContext()
          let path = UIBezierPath(roundedRect: rect, cornerRadius: rect.height / 2)
          context?.setFillColor(UIColor.gray.cgColor)
          path.fill()
      }
      

      不过我自己认为既然 cornerRadius 已经不会触发离屏渲染了,是否还有必要提前绘制圆角呢?这种场景我理解就是将绘制的过程前置让 CPU 提前对视图做了预处理,这样 GPU 就不用再做了。

最后关于调试离屏渲染,离屏渲染消耗的时间并不会出现在 Instrument 的 TimeProfiler 工具里,因为这一层的渲染是 CoreAnimation 的 RenderServer 和 GPU 配合做的,和我们的 App并没有直接的关系。

参考地址:

  1. 知乎-离屏渲染 (opens new window)
  2. objcio-绘制像素到屏幕上 (opens new window)
  3. Drawing Offscreen-OpenGL Programming Guide for Mac (opens new window)
  4. 即刻技术-关于iOS离屏渲染的深入研究 (opens new window)
  5. Core Animation Programming Guide (opens new window)
  6. Quartz 2D Programming Guide (opens new window)
  7. WWDC14:Advanced Graphics and Animations for iOS Apps (opens new window)
  8. WWDC14: Advanced Graphics and Animations for iOS Apps 字幕版 (opens new window)
  9. [WWDC 2012 Polishing Your Interface Rotations](https://archive.org/details/wwdc-2012-sessions/[2012]+[Session+240]+Polishing+Your+Interface+Rotations.mov](https://archive.org/details/wwdc-2012-sessions/%5B2012%5D+%5BSession+240%5D+Polishing+Your+Interface+Rotations.mov)