一、丢帧问题概述
应用丢帧通常指的是在应用程序的界面绘制过程中,由于某些原因导致界面绘制的帧率下降,从而造成界面卡顿、动画不流畅等问题。以60Hz刷新率为例子,想要达到每秒60帧(即60fps)的流畅体验,每一帧需要在16.7ms内完成,如果超过16.7ms未完成渲染,就可能会出现丢帧。
二、丢帧问题原理
在HarmonyOS中,图形系统采用了统一渲染的模式,遵循着一个典型的流水线模式,以90Hz刷新率为例,每个Vsync周期是11.1ms,整个过程如下图所示。如果是60Hz,每个Vsync的周期是16.7ms;如果是120Hz,则每个Vsync的周期是8.3ms。
在整个渲染流程中,首先是由应用侧响应用户的屏幕点击等输入事件,由应用侧处理完成后再提交给Render Service,由Render Service协调GPU等资源处理后,再将最终的图像统一送到屏幕上进行显示。
- 应用侧(App)处理用户的屏幕点击等输入事件,生成当前界面描述的数据结构。其中,界面描述数据包括UI元素的位置,大小,资源,UI元素的绘制指令,动效属性等。
- RenderService(渲染服务部件)是图形栈中负责界面内容绘制的模块,其主要职责就是对接ArkUI框架,支撑ArkUI应用的界面显示,包括控件、动效等UI元素。RenderService的RenderThread线程在Vsync下触发UI绘制,绘制过程包含3个阶段:Animation动效,Draw描画和Flush提交。
- Display是显示屏幕的抽象概念,可以是实际的物理屏也可以是虚拟屏。
其中应用侧的渲染流程如下图所示,了解ArkUI的渲染流程有助于我们定位应用侧的卡顿问题出现在哪个环节:
- Animation:动画阶段,在动画过程中会修改相应的FrameNode节点触发脏区标记,在特定场景下会执行用户侧ets实现自定义动画;
- Events:事件处理阶段,比如手势事件处理。在手势处理过程中也会修改FrameNode节点触发脏区标记,在特定场景下会执行用户侧ets 实现自定义事件;
- UpdateUI:自定义组件(@Component)在首次创建挂载或者状态变量变更时会标记为需要rebuild状态,在下一次Vsync过来时会执行rebuild流程,rebuild流程会执行程序UI编码,通过调用View的方法生成相应的组件树结构和属性样式修改任务。
- Measure:布局包装器执行相关的大小测算任务。
- Layout:布局包装器执行相关的布局任务。
- Render:绘制任务包装器执行相关的绘制任务,执行完成后会标记请求刷新RSNode绘制
- SendMessage:请求刷新界面绘制。
在整个处理流程中,应用侧和Render Service侧都有可能出现卡顿导致最终用户观测到丢帧的可能,我们分别将这两种情况命名为AppDeadlineMissed和RenderDeadlineMissed。一般而言,前者可能是应用逻辑处理编码不够高效导致的,后者可能是界面结构过于复杂或者GPU负载过大等原因导致的。这两个故障模型通过Frame模板都可以直观地看到。相应的故障模型如下面两幅图所示。
应用卡顿导致丢帧的故障模型:
Render Service卡顿导致丢帧的故障模型:
三、丢帧问题思路分析
丢帧问题处理流程:
四、处理丢帧问题步骤
1、识别卡顿
首先使用AppAnalyzer检测应用是否存在性能问题,如果检测存在丢帧问题,然后使用Frame Profiler、SmartPerf Host等工具录制Trace,查看应用平均帧率、丢帧率等,同时查看丢帧发生的位置。
2、分析丢帧原因
首先查看CPU调用判断系统是否存在异常,如果判断系统异常开发可以通过在线提单的方式进行反馈;如果系统没有异常,可以继续分析Trace查看卡顿帧的详细信息。最后查看函数调用栈,查看是否存在耗时函数。
3、选择优化方案
根据步骤2分析的丢帧原因,选择适合的优化方案。
4、验证优化效果
优化完成后需要重新测试验证丢帧问题是否得到解决,这里可以再次通过步骤1来确认优化效果。
五、常见丢帧问题
1、自定义动画丢帧问题
在播放动画或者生成动画时,画面产生停滞而导致帧率过低的现象,称为动画丢帧。
播放动画时,系统需要在一个刷新周期内完成动画变化曲线的计算,完成组件布局绘制等操作。建议使用系统提供的动画接口,只需设置曲线类型、终点位置、时长等信息,就能够满足常用的动画功能,减少UI主线程的负载。
2、布局嵌套过深
视图的嵌套层次会影响应用的性能。在屏幕刷新率为120Hz的设备上,每8.3ms刷新一帧,如果视图的嵌套层次多,可能会导致没法在8.3ms内完成一次屏幕刷新,就会造成丢帧卡顿,影响用户体验。因此推荐开发者移除多余的嵌套层次,使用相对布局 (RelativeContainer),缩短组件刷新耗时。
3、UI冗余刷新
自定义组件中的变量被状态装饰器(@State,@Prop等)装饰后成为状态变量,而状态变量的改变会引起使用该变量的UI组件渲染刷新。状态变量的不合理使用可能会带来冗余刷新等性能问题。开发者可以使用状态变量组件定位工具(hidumper)获取状态管理相关信息,例如自定义组件拥有的状态变量、状态变量的同步对象和关联组件等,了解状态变量影响UI的范围,写出高性能应用。
4、主线程中执行冗余和耗时操作
应避免在主线程中执行冗余与易耗时操作,否则可能会阻塞UI渲染,引发界面卡顿或掉帧现象,特别是在高频回调中执行耗时操作。
六、丢帧问题优化建议
- 尽量减少布局的嵌套层数,合理使用布局,使用相对布局来减少层级。
- 使用组件复用减少组件的重复创建与渲染。
- 合理管理状态变量,精准控制组件的更新范围,避免冗余刷新。
- 使用LazyForEach加载长列表,长列表的优化。
- 使用系统提供的动画接口,减少动画丢帧。
- 优化主线程中冗余和耗时操作,