【HarmonyOS Next】【性能优化】合理处理高负载组件的渲染

HarmonyOS Next应用开发案例(持续更新中……)
HarmonyOS Next性能指导总览

本篇文章链接,请访问:https://gitee.com/harmonyos-cases/cases/blob/master/docs/performance/reasonably-dispose-highly-loaded-component-render.md

简介

在应用开发中,有的页面需要加载大量的数据,就会导致组件数量较多或者嵌套层级较深,从而引起组件负载加重,绘制耗时增长,如果不进行合理的处理,可能引起卡顿掉帧等性能问题。

问题场景

在日历应用的开发中,全年的日期页面需要加载一年中的所有日期,这样就最少需要365个Text组件用于显示日期。一次性绘制这么多的组件,时间会比较久,而且会耗费大量的资源,如果手机配置较差,可能会引起明显的卡顿或者转场动画的掉帧现象。

解决思路

由于一次性加载大量数据、绘制大量组件会导致卡顿,那么减少加载的数据量就是一种解决方法。但是由于业务需求,需要加载的数据总量和绘制的组件数量是不能减少的,那么只能想办法将数据进行拆分,将和数据相关的组件分成多次进行绘制。ArkTS中提供了DisplaySync(可变帧率),支持开发者设置回调监听,可以在回调里做一些数据的处理,在每一帧中绘制少量的数据,减少卡顿或者转场动画的掉帧现象。

优化示例

常规代码

通常情况下,会在进入页面后开始加载数据,即在aboutToAppear()中加载所有数据,并通过LazyForEach绘制所有的组件。

@Entry
@Component
struct Direct {
  ...
  // 初始化日历中一年的数据
  initCalenderData() {
    // 添加自定义trace标签,用于在trace抓取结果中查看相关运行时间信息
    hiTraceMeter.startTrace('push_data_direct', 1);
    for (let i = 1; i <= 12; i++) {
      // 获取每个月的日数据
      const monthDay: number[] = getMonthDate(i, this.currentYear);
      const month: Month1 = {
        month: i + '月',
        num: i,
        days: monthDay
      }
      this.contentData.pushData(month);
    }
    hiTraceMeter.finishTrace('push_data_direct', 1);
  }

  aboutToAppear() {
    ...
    this.initCalenderData();
  }

  build() {
    Column({ space: 12 }) {
      ...
      Grid() {
        LazyForEach(this.contentData, (monthItem: Month1) => {
          // 每个月的日期
          GridItem() {
            Flex({ wrap: FlexWrap.Wrap }) {
              ...
              // 日期信息
              ForEach(monthItem.days, (day: number) => {
                Text(day.toString())
                  ...
              })
            }
         ...
         }
    }
    ...
  }
}

在上面的代码中,要在页面上显示一年中的所有日期,在aboutToAppear()方法中,将每个月的信息放入到一个数组里面,并通过LazyForEach通知Grid进行绘制。编译运行后,通过SmartPerfHost工具,抓取Trace,并查看耗时和掉帧率,如图1所示。其中push_data_direct是自定义添加的Trace标签,可以看到加载数据的开始时间和耗时,Expected Timeline是期望绘制一帧的时间,Actual Timeline是实际绘制一帧的时间。

图1 直接加载所有数据Trace图

通过图中信息可以看到,在aboutToAppear()中直接加载全部数据时,实际上就是在一帧中绘制全部的日期组件。期望的一帧的耗时应该是8ms(120Hz刷新率),绘制全部组件的实际耗时大概是126ms,正常情况下这个时间内应该是绘制15帧左右,而在这段代码中只绘制了1帧,会引起比较严重的卡顿现象。

优化代码

通过DisplaySync中的帧回调方法,将数据拆分到每一帧中进行加载和绘制。

@Entry
@Component
struct EveryFrameMonth {
  ...
  private calenderDisplaySync: displaySync.DisplaySync | undefined = undefined;

  startDisplaySync() {
    // 设置期望帧率120帧
    let range: ExpectedFrameRateRange = {
      expected: 120,
      min: 0,
      max: 120
    };

    // 从1月份开始获取每个月的日期数据
    let current: number = 1;
    // 最多12个月
    const MAX: number = 12;
    let draw60 = (intervalInfo: displaySync.IntervalInfo) => {
      if (current <= MAX) {
        hiTraceMeter.startTrace('push_data_every_frame', 1000);
        // 获取current月的日期数据
        const monthDay: number[] = getMonthDate(current, this.currentYear);
        const month: Month1 = {
          month: current + '月',
          num: current,
          days: monthDay
        }
        this.contentData.pushData(month);
        current = current + 1;
        hiTraceMeter.finishTrace('push_data_every_frame', 1000);
      } else {
        // 加载完数据后停止回调,否则会一直消耗资源
        if (this.calenderDisplaySync) {
          this.calenderDisplaySync.stop();
        }
      }
    };

    this.calenderDisplaySync = displaySync.create();
    this.calenderDisplaySync.setExpectedFrameRateRange(range);
    this.calenderDisplaySync.on("frame", draw60);
    this.calenderDisplaySync.start();
  }

  aboutToAppear() {
    this.date.push(this.currentMonth); // 存入月份信息
    this.date.push(this.currentDay); // 存入日期信息
    this.date.push(this.currentWeekDay); // 存入周信息
    this.startDisplaySync();
  }

  aboutToDisappear(): void {
    // 页面销毁时停止帧回调监听,防止内存泄漏
    if (this.calenderDisplaySync !== undefined) {
      this.calenderDisplaySync.stop();
    }
  }

  build() {
    Column({ space: 12 }) {
      ...
      // 每个月的日期
      Grid() {
        LazyForEach(this.contentData, (monthItem: Month1) => {
          GridItem() {
            Flex({ wrap: FlexWrap.Wrap }) {
              ...
              ForEach(monthItem.days, (day: number) => {
                Text(day.toString())
                ...
              })
            }
            ...  
  }
}

在上面的代码中,aboutToAppear()方法中调用了startDisplaySync()方法,在startDisplaySync()中添加了帧回调的监听,并在每一帧回调中只加载一个月的日期数据。编译运行后,通过SmartPerfHost工具,抓取Trace,并查看耗时和掉帧率,如图2所示。其中push_data_every_frame是自定义添加的Trace标签,可以看到加载数据的开始时间和耗时,Expected Timeline是期望绘制一帧的时间,Actual Timeline是实际绘制一帧的时间。

图2 每帧加载一个月的数据

从图2中可以看到,将每个月的数据拆分到单独的帧中加载时,每一帧的实际耗时变短了——期望耗时是8ms,实际耗时14ms(实际每帧绘制时间不同,此处以第一帧举例)。但是,由于每一帧的实际耗时都比预期长,就会导致预期帧减少的问题,即图中Expected Timeline标签中的空白现象。那么可以按照这个思路做进一步优化,继续拆解每帧加载的数据量。

@Entry
@Component
struct EveryFrameHalfMonth {
  ...
  private calenderDisplaySync: displaySync.DisplaySync | undefined = undefined;

  startDisplaySync() {
    // 设置期望帧率
    let range: ExpectedFrameRateRange = {
      expected: 120,
      min: 0,
      max: 120
    };
    // 从1月份开始获取每个月的日期数据
    let current: number = 1;
    // 最多12个月
    const MAX: number = 12;
    let isAddNew: boolean = true;
    let draw60 = (intervalInfo: displaySync.IntervalInfo) => {
      if (current <= MAX) {
        hiTraceMeter.startTrace('push_data_every_frame_half_month', 1001);
        // 获取current月中所有的日期数据
        const monthDay: number[] = getMonthDate(current, this.currentYear);
        // 找出日期的中间数
        const centerNumber: number = Math.floor(monthDay.length / 2);
        if (isAddNew) {
          const temp: MonthDayDataSource = new MonthDayDataSource();
          // 取前半个月的日期数据
          const firstHalfMonth: number[] = monthDay.slice(0, centerNumber);
          temp.pushData(firstHalfMonth);
          const month: Month = {
            month: current + '月',
            num: current,
            days: temp
          }
          this.contentData.pushData(month);
          isAddNew = false;
        } else {
          // 取后半个月的日期数据
          const secondHalfMonth: number[] = monthDay.slice(centerNumber, monthDay.length - 1);
          // current从1开始计数,数组中的数据索引从0开始,所以这里获取数据时需要使用current-1
          this.contentData.getData(current - 1).days.pushData(secondHalfMonth);
          this.contentData.getData(current - 1).days.notifyDataChange(current - 1);
          isAddNew = true;
          current = current + 1;
        }
        hiTraceMeter.finishTrace('push_data_every_frame_half_month', 1001);
      } else {
        if (this.calenderDisplaySync) {
          this.calenderDisplaySync.stop();
        }
      }
    };

    this.calenderDisplaySync = displaySync.create();
    this.calenderDisplaySync.setExpectedFrameRateRange(range);
    this.calenderDisplaySync.on("frame", draw60);
    setTimeout(() => {
      if (this.calenderDisplaySync) {
        this.calenderDisplaySync.start();
      }
    }, 10)
  }

  aboutToAppear() {
    this.date.push(this.currentMonth); // 存入月份信息
    this.date.push(this.currentDay); // 存入日期信息
    this.date.push(this.currentWeekDay); // 存入周信息
    this.startDisplaySync();
  }

  aboutToDisappear(): void {
    // 页面销毁时停止帧回调监听,防止内存泄漏
    if (this.calenderDisplaySync !== undefined) {
      this.calenderDisplaySync.stop();
    }
  }

  // 自定义日历选取器内容
  build() {
    Column({ space: 12 }) {
      ...
      // 每个月的日期
      Grid() {
        LazyForEach(this.contentData, (monthItem: Month) => {
          // 设置ListItemGroup头部组件,显示年份和月份
          GridItem() {
            Flex({ wrap: FlexWrap.Wrap }) {
              ...
              LazyForEach(monthItem.days, (day: number) => {
                Text(day.toString())
                ...
              })
            }
            ...
  }
}

在上面这段代码中,将每个月的数据再次进行了拆分,每次只加载半个月的数据。编译运行后,通过SmartPerfHost工具,抓取Trace,并查看耗时和掉帧率,如图3所示。其中push_data_every_frame_half_month是自定义添加的Trace标签,可以看到加载数据的开始时间和耗时,Expected Timeline是期望绘制一帧的时间,Actual Timeline是实际绘制一帧的时间。

图3 每帧加载半个月的数据

从图中可以看到,除了第1帧和第2帧有所延迟,其他的帧都没有问题。其中,第1帧实际耗时比期望耗时多158μs左右,时间上的影响很小;通过push_data_every_frame_half_month标签可以看到,第1帧运行到一半时才开始加载数据,导致了第2帧的结束时间比预期要晚一点,实际上第2帧的绘制时间只有不到5ms,对性能的影响也很小。

总结

通过上面的示例代码和优化过程,可以看到在需要加载大量数据的页面,一次性全部加载时会引起比较严重的性能问题,一帧的绘制耗时很长,在性能较差的手机上可能会引起明显的卡顿掉帧现象;而将数据合理拆分后,可以有效减少帧绘制的耗时,从而减少卡顿掉帧现象的发生。

  • 22
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值