在分析DisplaySync业务时,开发者在抓取trace前可开启debug开关,hdc shell param set persist.ace.debug.enabled 1,并重启手机。当DisplaySync实际造成了组件的属性变化时,才会打印H:ViewPU.viewPropertyHasChanged信息。当DisplaySync注册仅用于帧率监听、回调,且不产生组件属性变化时,开发者可以通过分析该帧的调用栈找出DisplaySync耗时过程中执行的代码逻辑。
对于HarmonyOS而言,不同来源和架构的绘制请求均通过Render Service(渲染服务)进行统一管理。为了确保不同来源的复杂绘制效果能在同一时间节点完成绘制、合成与显示,Render Service依赖Vsync实现全局统一的信号收发机制。如果将Render Service比作一个运行有序的公交系统,那么VsyncGenerator子进程便是始发站,而RSHardware子进程则代表终点站,即屏幕显示。每个需要使用Vsync的“乘客”都可以申请一张“车票”,VsyncGenerator线程会统计每个申请者的信息,确定合理的发车频率,如30、60、120Hz,经过后续的绘制和合成步骤后,最终反映在屏幕的刷新帧率上。
开发者可以在Profiler中抓取trace后,搜索以下关键的Trace点来分析一段时间内的Vsync申请和使用情况:



在理解了Vsync的基础作用后,开发者需认识到Vsync的主要用途是保证绘制信号的垂直统一,对于开发者而言,需尽量避免在静置或单一组件刷新的页面中使用冗余的Vsync信号。从实际开发的角度上来看,DisplaySync和NativeVsync两种接口可能会对场景下的Vsync对象产生实质性的影响。对于使用了React Native、Webview、Flutter的应用,开发者可以通过Vsync的分析方法挖掘潜在功耗问题,不仅可以避免发生Vsync“发空车”导致带来冗余功耗,还可以规避实际没有显示的冗余绘制内容。下面给出了一些Vsync合理使用的低功耗建议。
displaySync方法支持让开发者以特定的帧率请求UI绘制帧率,一般用于开发者自绘制UI,并且对于帧率有特定诉求的场景。通过这种方式创建的对象需由开发者对其做好生命周期管理,确保绘制内容处于屏幕外或不再需要时,应注销displaySync对象,避免应用进程空刷帧。下图中展示了一个错误使用displaySync实现Canvas弹幕功能的场景,当弹幕组件正常显示时以60hz的频次刷新,但当用户关闭弹幕功能时,依然可以发现有displaySync对象持续执行业务。该有问题的UI帧表现的现象有:

进一步展开DisplaySync下方的Trace信息可以发现,其中包含了大量的CanvasRenderingContext2D业务,其中,H:ViewPU.viewPropertyHasChanged位置,会打印出对应的ArkUI组件名称,帮助开发者定位到正在执行业务的组件。锁定可能造成问题的组件后,开发者可找到displayVsync对象的create、start以及stop三个关键状态控制的对应代码。
在分析DisplaySync业务时,开发者在抓取trace前可开启debug开关,hdc shell param set persist.ace.debug.enabled 1,并重启手机。当DisplaySync实际造成了组件的属性变化时,才会打印H:ViewPU.viewPropertyHasChanged信息。当DisplaySync注册仅用于帧率监听、回调,且不产生组件属性变化时,开发者可以通过分析该帧的调用栈找出DisplaySync耗时过程中执行的代码逻辑。
如下图所示,DisplaySync对象在回调函数中有动画相关的业务执行,但实际上并未对组件变量产生变化,也没有形成需要脏区绘制指令,仅带来冗余负载。开发者可以通过调用栈锁定冗余的函数以及对应的执行代码段。

如下两段代码,分别在组件的aboutToDisappear()和onVisibleAreaChange()函数中对displaySync对象进行了优化处理。其中aboutToDisappear()函数是基于自定义组件生命周期的兜底,确保该组件在被销毁时,displaySync对象不出现泄漏。而onVisibleAreaChange()函数则是针对组件可见性的判断来控制displaySync的播放状态,防止组件进入不可见区域时,displaySync对象依然活跃。
- // Stop DisplaySync and set to null when the component is unmounted to prevent idle running and memory leaks
- aboutToDisappear() {
- if (this.backDisplaySyncSlow) {
- this.backDisplaySyncSlow.stop();
- this.backDisplaySyncSlow = undefined;
- }
- if (this.backDisplaySyncFast) {
- this.backDisplaySyncFast.stop();
- this.backDisplaySyncFast = undefined;
- }
- }
- // Pause DisplaySync objects when the component is not visible
- .onVisibleAreaChange([0.0, 1.0], (isExpanding: boolean, currentRatio: number) => {
- if (!isExpanding && currentRatio <= 0.0) {
- console.info('Component is completely invisible.');
- if (this.backDisplaySyncSlow) {
- this.backDisplaySyncSlow.stop();
- }
- if (this.backDisplaySyncFast) {
- this.backDisplaySyncFast.stop();
- }
- }
- })

如上图所示的Trace中,原始页面中,存在一个对Index组件添加的displaySync对象。用户在“1”处所示的位置点击tab进行了页面的切换,并在“2”处所示的位置,解注册了页面中存在的displaySync对象,UI帧率降至0Hz,达成了功耗优化的效果。
NativeVsync可供开发者获取系统vsync回调,可用于实现应用的绘制帧率与系统帧率同步。NativeVsync被广泛用于React Native以及C-API开发的应用中,这些开发框架中有多种回调函数接口,会通过持续请求Vsync来拉起应用自定义的业务内容。如下图是一个静置页面下的Trace,“1”、“2”均是来自应用进程下的OS_VsyncThread线程,H:ReceiveVsync name分别是TaroAnimation和dirty。从“3”中的trace信息可以发现,FlushRenderTask并未打印需要刷新的ArkUI组件,但FlushMessage下有H:MarshRSTransactionData送出,这表明该帧依然有绘制效果通过UI帧将绘制指令递交给RS。

随后Render Service和RSUniRender会继续处理持续性的绘制指令,但由于DisplayNode并无实际需要显示的内容,最终在RSUniRender上打印DisplayNode skip跳过。该场景下,应用存在UI、Render Service冗余负载类型的空跑现象。

在定位这类问题时,由开发者自定义的NativeVsync对象通常都会由VsyncGenerator下发给应用进程下的OS_VsyncThread子线程,开发者可以根据Vsync申请者的名称,找到对应的Vsync申请对象。开发者可利用Profiler工具抓取的Callstack来查看对应的Vsync回调内执行的业务内容,如下图所示,开发者在VsyncListener的监听回调中有一小块rnoh的Animate的调用栈,可能正在执行一块动画业务。对于NativeVsync对象而言,系统仅提供回调,并不会约束开发者在回调函数内的业务实现,故而如果开发者在NativeVsync中有较多绘制相关的业务时,需额外留意该绘制业务的启停时机,并且减少不必要Vsync监听器的使用。
