应用启动到UI页面展示过程包含框架初始化、页面加载和布局渲染三个步骤。其中页面加载和布局渲染的主要流程如下:
图1 页面首次加载过程流程图
- 在执行页面文件时,前端UI描述会在后端创建相应的FrameNode节点树。该树主要用于处理UI组件属性更新、布局测算、事件处理。每个树节点和前端UI组件是一一对应的关系。
- FrameNode节点树生成之后,根节点开始创建布局任务。该任务遍历所有子节点并创建子节点的布局包装任务。布局包装任务包括执行相关测算和布局任务。
- 布局包装任务完成后,每个FrameNode将创建相应的渲染包装任务并进行内容绘制。
可以看到,应用启动后页面加载和渲染的性能与FrameNode树上的节点数量以及每个节点上的属性相关。因此,为缩短页面加载和布局渲染时长,在前端使用UI组件时可以考虑以下优化方案:
- 避免在自定义组件的生命周期内执行高耗时操作:自定义组件创建后在渲染前会调用其生命周期回调函数,若函数中包含高耗时操作将阻塞UI渲染,将增加主线程负担。
- 按需注册组件属性:后端在创建FrameNode节点树时,对于组件上注册的每个属性也会保存在FrameNode节点上,包括渲染类属性集合(如颜色)和布局类属性集合(如长宽,对齐方式)。在FrameNode执行布局包装任务和渲染包装任务时,属性集合将作为输入参与。因此,在应用开发中应按需注册组件属性,避免设置冗余属性。
- 使用@builder函数代替自定义组件:前端定义的每一个自定义组件都会在后端FrameNode节点树上创建一对一的CustomNode类型的节点。CustomNode类作为FrameNode的子类,用于处理自定义组件相关业务逻辑。当在页面上大量使用自定义组件时,会成倍增加FrameNode节点树上CustomNode类型的节点数量,增加页面创建和渲染时长。因此,在满足业务需求的前提下,可以优先使用@builder函数代替自定义组件。
- 合理使用布局容器组件:ArkUI提供了一系列[布局容器组件]用于开发者快速搭建页面。不同的业务场景应选择合适的布局容器组件,并合理使用该组件的特性功能可以有效缩短页面布局时长。
避免在自定义组件的生命周期内执行高耗时操作
图2 自定义组件生命周期流程图
如上图所示,自定义组件创建完成之后,在build函数执行之前,将先执行aboutToAppear()生命周期回调函数。此时若在该函数中执行耗时操作,将阻塞UI渲染,增加UI主线程负担。因此,应尽量避免在自定义组件的生命周期内执行高耗时操作。对于复杂计算的耗时场景,可以将计算结果进行缓存处理。对于不需要等待结果的高耗时任务,可以采用多线程处理该任务,通过并发的方式避免主线程阻塞。在aboutToAppear()生命周期函数内建议只做当前组件的初始化逻辑,其他业务逻辑可以按需提前或延后处理。假设在首页视频列表中的子组件内需要初始化创建一个复杂播放器对象,该对象的创建非常耗时。若在该组件的aboutToAppear()函数中对创建该对象,当首页加载渲染时,列表内每个子组件的渲染都将等待相应的播放器对象初始化创建完成,此时页面加载将非常耗时甚至可能出现白屏。伪代码如下:
@Component
export struct VideoCard{
// ...
aboutToAppear(): void {
// 创建复杂对象任务,若该任务执行耗时1s,则组件将在1s后再渲染
this.createComplexVideoPlayer();
}
// ...
}
@Component
export struct CardList {
@State videoList: VideoItem[] = getVideoList();
build() {
List() {
ForEach(this.videoList, (item: VideoItem) => {
ListItem() {
VideoCard({ item })
}
}, (item: VideoItem) => item.id)
}
}
}
对于该场景,可以考虑将创建播放器对象任务的时机延后。如,计算当前组件出现在页面中的位置,当子组件滑动到页面的三分之一处时再创建播放器对象并播放视频。此时,页面首次渲染时,不会出现主线程阻塞。
例如在生命周期aboutToAppear中应该避免使用[ResourceManager]的getXXXSync接口入参中直接使用资源信息,推荐使用资源id作为入参,推荐用法为:resourceManager.getStringSync($r(‘app.string.test’).id)。 下面以[getStringSync]为例,测试一下这两种参数在方法中的使用是否会有耗时区别。
反例
import { hiTraceMeter } from '@kit.PerformanceAnalysisKit';
@Entry
@Component
struct Index {
@State message: string = 'getStringSync';
aboutToAppear(): void {
hiTraceMeter.startTrace('getStringSync', 1);
// getStringSync接口的入参直接使用资源,未使用资源ID
getContext().resourceManager.getStringSync($r('app.string.app_name'));
hiTraceMeter.finishTrace('getStringSync', 1);
}
build() {
RelativeContainer() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
}
.height('100%')
.width('100%')
}
}
可以通过[冷启动分析:Launch分析]工具抓取Trace,根据hiTraceMeter性能打点,查看耗时为1.942ms。
正例
import { hiTraceMeter } from '@