在iOS开发中,卡顿的问题是一个绕不开的问题,在这里我们从iOS的渲染循环(Render Loop)
的角度来分析在渲染过程中可能会出现卡顿的原因。
渲染循环
VSYNC
渲染循环
是一个连续性
的过程。通过触碰事件传送给app,然后转化到用户界面,向操作系统传送。最终呈现给用户,这就是循环,随着设备的刷新率发生。
在
iphone和ipad
中,VSYNC
信号的频率为60HZ
,在 ipad pro
中为 120HZ
。我们以iphone为例,这意味着每 16.67
毫秒,就可以显示一个新帧
。
整个渲染循环
由五个阶段
组成 :事件阶段(Event)
、提交阶段
、渲染准备
、渲染执行
、展示阶段
。
在提交或渲染或展示阶段,如果花费的时间超过一帧,就会造成卡顿
。
事件阶段(Event)
在这个阶段,App处理
触碰事件
或者Timer
等其他事件,决定用户界面是否需要变化。
提交阶段(Commit)
在提交阶段,app向渲染服务器提交渲染命令。
渲染准备
在下一个VSYNC
中,渲染服务器处理命令,在渲染准备阶段,为在GPU上绘制做好准备。
渲染执行
在渲染执行阶段,GPU将用户界面的最后图像绘制出来。
展示阶段
在下一个
VSYNC
,这一帧将会呈现给用户。
要想有丝滑的用户体验,每一个阶段都至关重要。如果某一阶段的时间超过了VSYNC的时间
,则会造成卡顿
:
提交阶段的卡顿
提交事务
渲染循环
在Event
阶段,处理触摸事件及其他事件,收到事件后,需要改变View
的backgroundColor
、frame
等属性
下一次事务提交时,系统记录这些子视图将需要某个布局或显示
在提交事务时,这些需要某个显示或布局的视图,会通过调用
drawRect
或 layoutSubviews
来进行相应的更新。
提交事务的4个步骤:
提交事务有 Layout(布局)
、Display(展示)
、Prepare(筹备)
、Commit(提交)
4个步骤。
Layout(布局阶段)
layoutSubviews
在view
需要布局的时候会调用,以下情况需要重新布局:
- 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,在需要更新布局是,尽量只使用
setNeedsLayout
,layoutIfNeeded
会消耗当前事务的生命周期,会造成卡顿。大多数时候,可以等到下一次循环执行时,在更新布局。 -
3,加载图片时,对图片进行解码。
渲染阶段的卡顿
渲染阶段里面包含两个阶段:渲染准备阶段(Render prepare)
和渲染执行阶段(Render execute)
。
渲染准备阶段
将图形树分解为一系列简单的操作,供GPU执行
渲染执行阶段
GPU
将App图层
绘制成最终图像。
这两个阶段都可能造成帧延迟。
渲染过程
通过绘制一个有阴影的示例,来讲解下绘制过程
在渲染准备阶段
,渲染服务器会逐层编译一系列绘图命令,使GPU能从后向前
绘制用户界面。
从
根节点
开始,渲染服务器从同级到同级,从父级到子级,直到涵盖层级中的每个图层,这样就得到了渲染的整个管道
。
在渲染执行阶段,按照这个管道的顺序进行绘制。
1,先绘制蓝色 2,绘制深蓝色
3,绘制阴影
阴影的形状由它下面的两个层定义,因此GPU不知道用什么形状来绘制阴影
,但如果先绘制圆形和长条,那么阴影会用黑色遮挡它们,看起来会不正确,此时,GPU必须切换到不同的纹理,以确定阴影的形状,这种情况,我们称之为 离屏渲染
,在新开辟的纹理中,加下面的层复制过来,确定阴影的形状,阴影渲染完成后,将那个离屏纹理复制到最终纹理中
4,绘制圆形和长方形和文字
在这个过程中,我们绘制阴影
的时候,使用了一个特殊的技巧来绘制:GPU先在其他地方渲染一个图层,然后再将其复制过来
,我们称之为 离屏通道(Offscreen Pass)
。就阴影而言,它必须绘制图层,以确定最终形状,离屏渲染会积少成多,对性能造成影响。
对于阴影,我们可以通过UIBerthPath
给阴影指定形状,来避免离屏渲染。
为了方便离屏渲染的检测和给出相对应的解决方法
,在Xcode 12
中添加了一个新的运行时问题类型,称之为优化机会
。在LLDB
状态下,在Editor
选项下
本文参考视频:
Explore UI animation hitches and the render loop