鸿蒙(HarmonyOS)性能优化实战-高负载组件的渲染
在应用开发中,有的页面需要加载大量的数据,就会导致组件数量较多或者嵌套层级较深,从而引起组件负载加重,绘制耗时增长,如果不进行合理的处理,可能引起卡顿掉帧等性能问题。通过上面的示例代码和优化过程,可以看到在需要加载大量数据的页面,一次性全部加载时会引起比较严重的性能问题,一帧的绘制耗时很长,在性能较差的手机上可能会引起明显的卡顿掉帧现象;而将数据合理拆分后,可以有效减少帧绘制的耗时,从而减少卡顿掉
简介
在应用开发中,有的页面需要加载大量的数据,就会导致组件数量较多或者嵌套层级较深,从而引起组件负载加重,绘制耗时增长,如果不进行合理的处理,可能引起卡顿掉帧等性能问题。
问题场景
在日历应用的开发中,全年的日期页面需要加载一年中的所有日期,这样就最少需要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,对性能的影响也很小。
总结
通过上面的示例代码和优化过程,可以看到在需要加载大量数据的页面,一次性全部加载时会引起比较严重的性能问题,一帧的绘制耗时很长,在性能较差的手机上可能会引起明显的卡顿掉帧现象;而将数据合理拆分后,可以有效减少帧绘制的耗时,从而减少卡顿掉帧现象的发生。
码牛课堂也为了积极培养鸿蒙生态人才,让大家都能学习到鸿蒙开发最新的技术,针对一些在职人员、0基础小白、应届生/计算机专业、鸿蒙爱好者等人群,整理了一套纯血版鸿蒙(HarmonyOS Next)全栈开发技术的学习路线。大家可以进行参考学习:https://qr21.cn/FV7h05
①全方位,更合理的学习路径:
路线图包括ArkTS基础语法、鸿蒙应用APP开发、鸿蒙能力集APP开发、次开发多端部署开发、物联网开发等九大模块,六大实战项目贯穿始终,由浅入深,层层递进,深入理解鸿蒙开发原理!②多层次,更多的鸿蒙原生应用:
路线图将包含完全基于鸿蒙内核开发的应用,比如一次开发多端部署、自由流转、元服务、端云一体化等,多方位的学习内容让学生能够高效掌握鸿蒙开发,少走弯路,真正理解并应用鸿蒙的核心技术和理念。③实战化,更贴合企业需求的技术点:
学习路线图中的每一个技术点都能够紧贴企业需求,经过多次真实实践,每一个知识点、每一个项目,都是码牛课堂鸿蒙研发团队精心打磨和深度解析的成果,注重对学生的细致教学,每一步都确保学生能够真正理解和掌握。
为了能让大家更好的学习鸿蒙(HarmonyOS NEXT)开发技术,这边特意整理了《鸿蒙开发学习手册》(共计890页),希望对大家有所帮助:https://qr21.cn/FV7h05
《鸿蒙开发学习手册》:https://qr21.cn/FV7h05
如何快速入门:
- 基本概念
- 构建第一个ArkTS应用
- ……
开发基础知识:https://qr21.cn/FV7h05
- 应用基础知识
- 配置文件
- 应用数据管理
- 应用安全管理
- 应用隐私保护
- 三方应用调用管控机制
- 资源分类与访问
- 学习ArkTS语言
- ……
基于ArkTS 开发:https://qr21.cn/FV7h05
- Ability开发
- UI开发
- 公共事件与通知
- 窗口管理
- 媒体
- 安全
- 网络与链接
- 电话服务
- 数据管理
- 后台任务(Background Task)管理
- 设备管理
- 设备使用信息统计
- DFX
- 国际化开发
- 折叠屏系列
- ……
鸿蒙开发面试真题(含参考答案):https://qr21.cn/FV7h05
大厂鸿蒙面试题::https://qr18.cn/F781PH
鸿蒙开发面试大盘集篇(共计319页):https://qr18.cn/F781PH
1.项目开发必备面试题
2.性能优化方向
3.架构方向
4.鸿蒙开发系统底层方向
5.鸿蒙音视频开发方向
6.鸿蒙车载开发方向
7.鸿蒙南向开发方向
更多推荐
所有评论(0)