鸿蒙NEXT开发【点击完成时延分析】性能分析

在移动终端应用开发中,完成时延是指用户操作移动终端时,从输入触控指令到界面完全刷新结束并达到可以阅读的稳定状态所用时间,点击完成时延依据页面转场类型可以分为页面内跳转和页面间跳转两种。完成时延在用户体验设计中扮演着关键的角色,直接影响用户对产品的满意度和使用体验。完成时延反映了用户对响应速度的整体感受,主要影响用户对触控交互及时性和愉悦性的体验评价。如图一所示,点击完成时延包含点击响应时延

在一定时延水平以上,完成时延越短越好,当完成时延小于一定水平后,用户的流畅体验不再继续提升,建议应用或元服务内点击操作完成时延≤900ms,本文将介绍相关分析工具,点击完成时延问题定位流程以及常见问题根因分析。

图1 点击完成起止点示意图
1

图2 页面转场过程解析
2

工具介绍

性能问题检测工具 AppAnalyzer

AppAnalyzer是DevEco Studio中提供的检测评分工具,用于测试并评价HarmonyOS应用或元服务的质量,能快速提供评估结果和改进建议,当前支持的测试类型包括兼容性、性能、UX测试和最佳实践等,其中点击完成时延是性能类型中的一项检测规则,开发者可以使用该工具检测响应性能。

性能问题分析工具 DevEco Profiler

性能调优深入分析工具,支持冷启动、卡顿丢帧、状态变量、并行化、网络耗时、ArkWeb、内存优化等场景化调优能力。其中Frame分析可以帮助开发者深度分析性能问题,通过录制应用运行过程中的关键数据,从而识别卡顿丢帧、耗时长等问题的原因所在。

性能问题分析工具 ArkUI Inspector

ArkUI Inspector是DevEco Studio中提供用于检查UI的工具,开发者可以借助它预览真机或模拟器中的UI效果,快速定位布局层级问题,也可以观察组件属性、不同组件之间的关系等。

问题定位流程

下图展示了定位点击完成时延高耗时的简易流程

图3 问题定位流程图
3

如上图所示,分析点击完成时延问题一般需要以下几个步骤:

  1. 性能体检:使用性能检测工具AppAnalyzer检测应用是否存在性能问题。
  2. 确定完成时延耗时:使用录屏工具来确定点击完成时延的起点与终点,然后计算出整个完成时延的耗时时间,判断是否符合[《时延体检建议》]中的规范。
  3. 抓取Trace信息:使用性能分析工具DevEco Profiler抓取Trace,并确定Trace图中的起止点。
  4. 分析问题:结合关键泳道Trace信息以及ArkUI Inspector布局分析工具来定位具体问题。

关键泳道简介

上述五个关键泳道可通过函数调用耗时、转场页面绘制耗时、转场动画时延三个角度进行分析,现依据这三个角度,对关键泳道展开介绍

  • 函数调用耗时分析:

    ArkTS Callstack:提供了ArkTS侧的方法调用栈信息,对于分析ArkTS代码的执行实践和性能瓶颈非常关键;

    Callstack:提供了Native侧的方法调用栈信息,对于分析Native层面的性能问题非常关键;

  • 转场页面绘制耗时分析:

    Frame:提供了应用主线程的帧渲染信息,它可以帮助识别点击完成过程中哪些帧没有按时渲染,以及可能的原因;

    ArkUI Component:提供了ArkUI组件的创建、布局、渲染等过程的详细信息。可以帮助识别出哪些组件的创建或渲染过程耗时较长;

  • 转场动画时延分析:

    H:Animator:提供了动画执行过程中的详细信息,可以帮助识别点击完成过程中转场动画是否耗时较长

    关键Trace说明如下

    序号泳道Trace点描述
    1应用线程ReceiveVsync接受Vsync信号
    2应用线程OnvsyncEvent收到Vsync信号,渲染流程开始
    3应用线程FlushVsync刷新视图同步事件,包括记录帧信息、刷新任务、绘制上下文、处理用户输入
    4应用线程FlushDirtyNodeUpdate标脏组件刷新。页面刷新渲染的时候要尽量减少刷新的组件数量。当状态变量改变后,会先对状态变量相关的组件进行标脏,然后对这些组件重新测量和布局,最后再进行渲染
    5应用线程JSAnimation显示动画,动画会影响组件加载完成时延
    6应用线程FlushLayoutTask执行布局任务。在此阶段会对组件做布局测算,如果层级较深或者组件较多会影响性能
    7应用线程FlushMessages发送消息通知图行侧进行渲染
    8应用线程aboutToBeDeleted自定义组件生命周期函数,组件析构时出现,在未使用复用机制时,FlushDirtyNodeUpdate和LazyForEach predict下会析构组件,导致刷新时组件重复创建
    9应用进程SendCommands应用UI提交到Render Service
    10ArkTS CallstackcreatHttp创建网络请求
    11ArkTS Callstackrequest发送网络请求
    12ArkTS Callstackparse解析数据
    13ArkTS Callstackoff取消订阅

关键Trace分析

确定起止点

开发者可以通过录屏辅助测试,通过录屏分析工具来确定点击完成时延的起止点,进而判断是否存在需要优化的时延类问题。

下面介绍如何使用DevEco Profiler工具确定点击完成时延Trace的起止点。

  1. 搜索"H:DispatchTouchEvent"标签,找到type=1的那个DispatchTouchEvent,就是点击离手起点,将该时间戳设为起点。

    图4 确认Trace起点
    4

  2. 点击操作完成时延的终点位置在泳道图中没有明确的Trace点,需要通过录屏工具计算出点击加载完成耗时。从起点往后拉相同的时间找到终点位置。

    图5 确认Trace终点
    5

  3. 使用Profiler工具标记Trace起点与终点。

ArkTS Callstack泳道分析ArkTS侧耗时函数

ArkTS Callstack子泳道ArkVM是需要优先查看耗时情况的泳道,可以看到ArkTS侧一些方法的耗时,优先分析耗时最长的调用栈(program除外,program代表程序执行进入纯Native代码阶段,该阶段无Ark TS代码执行,也无Ark TS调用Native或者Native调用Ark TS情况,需要切换到Callstack泳道看具体的调用栈信息,一般很难通过这里分析出有效的信息),逐级展开,可以看到具体耗时的文件。基于 [“HMOS世界”]切换tab页场景,抓取Trace信息。

图6 ArkTS Callstack泳道图
6

观察发现MainPage文件中匿名函数耗时350ms,展开该节点。

图7 ArkTS Callstack泳道耗时函数详情
7

展开节点后发现函数调用链中AudioPlayerService中getInstance函数调用耗时327ms,接下来定位源代码。

// products\phone\src\main\ets\pages\MainPage.ets

Tabs({ index: this.currentIndex }) {
  // ...
}
.layoutWeight(1)
.barHeight(0)
.scrollable(false)
.onChange((index) => {
  this.currentIndex = index;
  ContinueModel.getInstance().data.mainTabIndex = index;
  if (AppStorage.get('audioPlayerStatus') !== AudioPlayerStatus.IDLE) {
    AudioPlayerService.getInstance().stop().then(() => {
      AudioPlayerService.destroy();
    });
  }
})

AudioPlayerService.ets相关代码如下

// commons\audioplayer\src\main\ets\service\AudioPlayerService.ets

export class AudioPlayerService {
  private static instance: AudioPlayerService | null = null;
  // ...

  public static getInstance(): AudioPlayerService {
    if (!AudioPlayerService.instance) {
      AudioPlayerService.instance = new AudioPlayerService();
    }
    return AudioPlayerService.instance;
  }

  public static destroy() {
    AudioPlayerService.getInstance().releaseAudioPlayer();
    AudioPlayerService.instance = null;
  }
  // ...
}

观察源代码发现AudioPlayerService调用getInstance创建单例对象耗费大量时间,随即又调用destroy方法销毁对象。优化方式如下:获取单例对象前,先判断单例对象是否被实例化,若没有实例化则直接跳过获取与销毁,避免实例对象的无效创建与销毁,参考如下代码。

// products\phone\src\main\ets\pages\MainPage.ets

Tabs({ index: this.currentIndex }) {
  // ...
}
.layoutWeight(1)
.barHeight(0)
.scrollable(false)
.onChange((index) => {
  this.currentIndex = index;
  ContinueModel.getInstance().data.mainTabIndex = index;
  if (AppStorage.get('audioPlayerStatus') !== AudioPlayerStatus.IDLE &&
  AudioPlayerService.instanceIsNotNull()) {
    AudioPlayerService.getInstance().stop().then(() => {
      AudioPlayerService.destroy();
    });
  }
})

优化后AudioPlayerService.ets代码如下:

// commons\audioplayer\src\main\ets\service\AudioPlayerService.ets

export class AudioPlayerService {
  private static instance: AudioPlayerService | null = null;
  // ...

  public static getInstance(): AudioPlayerService {
    if (!AudioPlayerService.instance) {
      AudioPlayerService.instance = new AudioPlayerService();
    }
    return AudioPlayerService.instance;
  }

  public static destroy() {
    AudioPlayerService.getInstance().releaseAudioPlayer();
    AudioPlayerService.instance = null;
  }

  public static instanceIsNotNull(): boolean {
    return AudioPlayerService.instance !== null;
  }
  // ...
}

Frame主线程泳道分析异常帧

查看 Frame 泳道里面的应用主线程子泳道,观察 app 侧帧数据。在这个泳道中,如果出现红色的帧通常表示该帧的渲染时间超过了预期,这可能是一个性能异常的指示。

如下图所示的第145帧

图8 超长帧Trace信息
8

每帧的预期耗时(ms) = 1000ms / 帧率 。如上图,鼠标点击选中超长帧,可以看到该帧的预期耗时Expected Duration 为8ms 330μs,说明帧率是120,而实际耗时为92ms571μs,这远远超过了预期耗时,因此被识别为超长帧。超长帧的长时间渲染会直接影响用户体验,并可能导致整个点击完成时延不达标。

通过上图发现卡顿期间有长段的ExecuteJS,需要查看具体的调用栈,观察ArkTS Callstack泳道无异常,接下来查看Callstack泳道的函数栈。

关于首帧渲染的特别说明:页面跳转后,由于需要重新加载和渲染新的UI元素,首帧的渲染时间往往会较长,不一定能够达到应用目标帧率下的预期耗时。因此,在性能分析中,页面跳转后的首帧出现红色(即超过该场景下的合理预期时间)是比较常见的现象,不一定意味着存在严重的性能问题,但也需要关注其是否过长,以便进行必要的优化。

Callstack泳道分析Native侧耗时函数

Callstack泳道,该泳道显示Native函数调用泳道,也可以看到Native函数调用栈以及各函数的耗时情况,主要查看主线程子泳道以及有内容的WorkerThread子泳道。

下图为超长帧案例中的Callstack的主线程子泳道图。

图9 Callstack主线程泳道图
9

滑动观察右侧权重占比最大的函数调用栈,定位到主要耗时是由于MainPage.ets文件下第203行代码引起。

ArkUI Component泳道分析组件绘制耗时

ArkUI Component泳道记录了自定义组件以及系统组件的绘制次数、耗时等信息,重点关注相对于其他组件耗时比较久的组件。

图10 ArkUI Component泳道泳道图
10

然后可以在详情Details中使用下图中被框选的按钮过滤目标组件,查看组件在刷新过程中不同阶段的耗时情况。结合函数调用栈与ArkUI Inspector工具定位目标组件绘制耗时过长的具体原因。

图11 ArkUI Component泳道图Details信息
11

H:Animator泳道分析动画时长

如果点击页面切换的过程中有加载的loading动画,出于用户体验考虑,故意将动画的停止与网络请求的完成相关联。例如,为了向用户展示“加载中”的状态,直到数据加载完成。可以通过H:Animator 泳道,看出动画耗时。

图12 H:Animator 泳道图
12

常见问题根因分析

网络请求耗时

在附带网络请求的页面跳转场景中,完成时延耗时长的绝大多数原因都是因为网络数据Http请求时间长。由于网络是从操作系统侧发起和控制的,且网络环境存在不可控性,所以我们很难在业务逻辑的代码中优化请求速度。因此尽可能的提前发起请求就尤为重要。通常可以从以下两个方面进行优化:

避免在异步函数中发起网络请求

由于ArkTS单线程EventLoop特性,其异步调用的执行时机会被延迟到同步逻辑后。那么如果我们将Http请求接口放到异步函数时,则可能会出现如下图所示情况网络请求被UI绘制阻塞,网络请求等待第一帧UI绘制结束才开始,如果页面首帧较复杂,则会导致该时长严重增加。

13

避免在页面子组件中发起网络请求

由于ArkUI组件的创建基于组件树结构,存在先后顺序。那么如果我们在页面的某一子组件中发起网络请求,则该请求需要等待其前面的组件创建完成才会发起,如果前面组件创建耗时较长,也会导致该请求被严重阻塞。

如下图情况,应用页面结构分为Header和Tabs两部分,如果将Tabs内容数据的Http请求放在Tabs组件中发起,由于Tabs组件在UI结构上依赖Header部分,则需要先创建Header,同时又因为Header内容的渲染也依赖网络请求,所以最终导致Tabs的数据请求严重延后。

14

动画时延耗时

页面的转场动画是提升用户体验的重要环节。然而,当动画时延耗时较长时,它会对用户的点击完成时延产生显著影响。动画的完成时间直接关系到用户何时能够开始与应用进行交互。动画时延影响点击完成时延的根因主要为动画时长设置过长。

常见的页面转场动画时长参数有:

  1. [Tabs]组件设置TabContent切换动画时长,即[animationDauration]属性。
  2. [Swiper]组件设置子组件切换动画时长,即[duration]属性。
  3. 页面间转场([pageTransition])设置转场动画时长,即[PageTransitionOptions]对象中的duration字段。

使用Tabs组件进行页面切换时,当不设置BottomTabBarStyle时默认[animationDuration]属性有300ms的动画时长,当该属性值设置过长时会导致完成时延变大。接下来将该属性值分别设置为100ms与1000ms来探究animationDuration属性对完成时延的影响。

实验一:设置animationDuration为100ms

@Entry
@Component
struct TabsPositiveExample {
  @State currentIndex: number = 0;
  private controller: TabsController = new TabsController();
  private list: string[] = ['green', 'blue', 'yellow', 'pink'];

  @Builder
  customContent(color: Color) {
    Column()
      .width('100%')
      .height('100%')
      .backgroundColor(color)
  }

  build() {
    Column() {
      Row({ space: 10 }) {
        ForEach(this.list, (item: string, index: number) => {
          Text(item)
            .textAlign(TextAlign.Center)
            .fontSize(16)
            .height(32)
            .layoutWeight(1)
            .fontColor(this.currentIndex === index ? Color.White : Color.Black)
            .backgroundColor(this.currentIndex === index ? Color.Blue : '#f2f2f2')
            .borderRadius(16)
            .onClick(() => {
              this.currentIndex = index;
              this.controller.changeIndex(index);
            })
        }, (item: string, index: number) => JSON.stringify(item) + index)
      }
      .margin(10)

      Tabs({ barPosition: BarPosition.Start, controller: this.controller }) {
        TabContent() {
          this.customContent(Color.Green)
        }

        TabContent() {
          this.customContent(Color.Blue)
        }

        TabContent() {
          this.customContent(Color.Yellow)
        }

        TabContent() {
          this.customContent(Color.Pink)
        }
      }
      .animationDuration(100)
      .layoutWeight(1)
      .barHeight(0)
      .scrollable(false)
    }
    .width('100%')
  }
}

15

实验二:设置animationDuration为1000ms

@Entry
@Component
struct TabsNegativeExample {
  // ...
  private controller: TabsController = new TabsController();

  // ...

  build() {
    Column() {
      // ...

      Tabs({ barPosition: BarPosition.Start, controller: this.controller }) {
        // ...

      }
      .barHeight(0)
      .layoutWeight(1)
      .animationDuration(1000)
      .scrollable(false)
    }
    .width('100%')
  }
}

16

17
|

animationDuration属性值完成时延
100ms99ms39μs
1000ms1s7ms693μs

上述示例通过减少animationDuration属性的数值,减小了Tabs组件切换动画的完成时延。当不设置BottomTabBarStyle样式时,动画时长默认为300ms,开发者可根据实际业务场景需要适当降低该动画时长,提高应用性能。

UI组件优化

转场新页面的组件过于复杂、布局不合理以及资源全量加载等会影响页面首次加载时延,可以采取如下方法进行性能优化:

  • UI优化:可以通过减少嵌套层级、减少渲染时间、使用缓存动效等方式进行优化。相关原理介绍以及场景案例
  • 按需加载优化:使用LazyForEach懒加载、动态import可以避免一次性加载所有资源。相关原理介绍以及场景案例
  • 全局自定义组件复用:使用自定义组件复用池,实现跨页面的组件复用,实现思路以及场景案例
  • 预创建组件:使用组件预创建,可以利用动画执行过程空闲时间进行组件预创建和属性设置。相关原理介绍以及场景案例
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值