iOS的渲染循环、离屏渲染原理、卡顿原理

3,040 阅读5分钟

在iOS开发中,卡顿的问题是一个绕不开的问题,在这里我们从iOS的渲染循环(Render Loop)的角度来分析在渲染过程中可能会出现卡顿的原因。

渲染循环

VSYNC

截屏2021-10-10 下午9.30.43.png 渲染循环是一个连续性的过程。通过触碰事件传送给app,然后转化到用户界面,向操作系统传送。最终呈现给用户,这就是循环,随着设备的刷新率发生。

截屏2021-10-10 下午9.35.38.pngiphone和ipad中,VSYNC信号的频率为60HZ,在 ipad pro中为 120HZ。我们以iphone为例,这意味着每 16.67毫秒,就可以显示一个新帧

整个渲染循环五个阶段组成 :事件阶段(Event)提交阶段渲染准备渲染执行展示阶段

在提交或渲染或展示阶段,如果花费的时间超过一帧,就会造成卡顿

事件阶段(Event)

截屏2021-10-10 下午9.43.10.png 在这个阶段,App处理触碰事件或者Timer等其他事件,决定用户界面是否需要变化。

提交阶段(Commit)

截屏2021-10-10 下午9.45.44.png 在提交阶段,app向渲染服务器提交渲染命令。

渲染准备

在下一个VSYNC中,渲染服务器处理命令,在渲染准备阶段,为在GPU上绘制做好准备。

渲染执行

截屏2021-10-10 下午9.49.38.png 在渲染执行阶段,GPU将用户界面的最后图像绘制出来。

展示阶段

截屏2021-10-10 下午10.04.56.png 在下一个VSYNC,这一帧将会呈现给用户。

要想有丝滑的用户体验,每一个阶段都至关重要。如果某一阶段的时间超过了VSYNC的时间,则会造成卡顿

截屏2021-10-10 下午8.56.57.png

提交阶段的卡顿

提交事务

渲染循环Event阶段,处理触摸事件及其他事件,收到事件后,需要改变ViewbackgroundColorframe等属性

截屏2021-10-10 下午10.22.40.png 下一次事务提交时,系统记录这些子视图将需要某个布局或显示

截屏2021-10-10 下午10.24.19.png 在提交事务时,这些需要某个显示或布局的视图,会通过调用 drawRectlayoutSubviews 来进行相应的更新。

提交事务的4个步骤:

提交事务有 Layout(布局)Display(展示)Prepare(筹备)Commit(提交)4个步骤。

Layout(布局阶段)

layoutSubviewsview需要布局的时候会调用,以下情况需要重新布局:

  • Positioning views(位置改变),例如,frame,bounds,transform属性改变,会重新布局。
  • 添加或者删除view。
  • 显示调用 setNeedsLayout()

Display(展示阶段)

需要更新内容的视图,都会调用draw(rect:)方法。以下情况会调用draw(rect:)方法:

  • 添加了重写draw(rect:)方法的视图。
  • 直接调用了 setNeedsDisplay()方法,以表明需要展示。

Prepare(筹备阶段)

  • 对未解码的图片,进行解码。
  • 若某个图像的颜色格式图形处理器无法直接使用,将会进行转换,这样会消耗很多的内存。

Commit (提交阶段)

视图层次结构将被递归打包,并发送到渲染服务器。

避免提交卡顿的建议

  • 1,保持视图的轻量:

      1.1:尽量使用`CALayer`的可用属性。
      1.2: 避免出现空的 drawRect 实现。
      1.3: 复用视图,避免使用代价过高的视图层级结构操作,比如添加和移除。如果一定要移除,可以考虑使用 hidden 属性。
    
  • 2,在需要更新布局是,尽量只使用setNeedsLayoutlayoutIfNeeded会消耗当前事务的生命周期,会造成卡顿。大多数时候,可以等到下一次循环执行时,在更新布局。

  • 3,加载图片时,对图片进行解码。

渲染阶段的卡顿

渲染阶段里面包含两个阶段:渲染准备阶段(Render prepare)渲染执行阶段(Render execute)

渲染准备阶段

将图形树分解为一系列简单的操作,供GPU执行

渲染执行阶段

GPUApp图层绘制成最终图像。

这两个阶段都可能造成帧延迟。

渲染过程

通过绘制一个有阴影的示例,来讲解下绘制过程

截屏2021-10-10 下午11.24.38.png

渲染准备阶段,渲染服务器会逐层编译一系列绘图命令,使GPU能从后向前绘制用户界面。

截屏2021-10-10 下午4.12.22.png根节点开始,渲染服务器从同级到同级,从父级到子级,直到涵盖层级中的每个图层,这样就得到了渲染的整个管道

截屏2021-10-10 下午11.30.46.png 在渲染执行阶段,按照这个管道的顺序进行绘制。

1,先绘制蓝色 2,绘制深蓝色

截屏2021-10-10 下午11.33.11.png 3,绘制阴影

阴影的形状由它下面的两个层定义,因此GPU不知道用什么形状来绘制阴影,但如果先绘制圆形和长条,那么阴影会用黑色遮挡它们,看起来会不正确,此时,GPU必须切换到不同的纹理,以确定阴影的形状,这种情况,我们称之为 离屏渲染,在新开辟的纹理中,加下面的层复制过来,确定阴影的形状,阴影渲染完成后,将那个离屏纹理复制到最终纹理中

截屏2021-10-10 下午11.35.33.png

截屏2021-10-10 下午11.36.43.png

4,绘制圆形和长方形和文字

截屏2021-10-10 下午11.39.05.png

在这个过程中,我们绘制阴影的时候,使用了一个特殊的技巧来绘制:GPU先在其他地方渲染一个图层,然后再将其复制过来,我们称之为 离屏通道(Offscreen Pass)。就阴影而言,它必须绘制图层,以确定最终形状,离屏渲染会积少成多,对性能造成影响。

对于阴影,我们可以通过UIBerthPath给阴影指定形状,来避免离屏渲染。

为了方便离屏渲染的检测和给出相对应的解决方法,在Xcode 12中添加了一个新的运行时问题类型,称之为优化机会。在LLDB状态下,在Editor选项下

截屏2021-10-11 上午12.01.24.png

如何消除卡顿

关于如何消除卡顿,有兴趣的可以查看这篇文章iOS 列表滑动的卡顿检测和优化

参考

Explore UI animation hitches and the render loop

Find and fix hitches in the commit phase

Demystify and eliminate hitches in the render phase