布局流程简介
在ArkUI布局中,应用侧会根据前端UI描述创建后端的页面节点树
@Entry
@Component
struct Index {
build(){
Row(){
InfoView()
Text('这是一段文字描述...')
...
}
}
}
@Component
struct InfoView {
build(){
Stack(){
Image($r('app.media.icon'))
...
Text('1')
...
}.alignConent(Alignment.TopStart)
}
}
其中包含了处理UI组件属性更新、布局测算、事件处理等逻辑。例如代码示例中的Index和InfoView组件会生成CistomNode节点(自定义组件节点,用于处理自定义组件相关业务逻辑。例如执行Build函数
)。Row、Text组件会对应生成FrameNode节点(系统组件节点,在这个过程中,UI线程会对每个元素进行**测算和布局**来确定具体的位置和大小
)
- 测算(Measure):负责确定组件对象的测量宽/高,即该组件元素所占屏幕大小
- 布局(Layout):确定组件最终的宽/高和四个顶点的位置
在确定了页面的具体节点信息后,会根据页面节点树生成当前的界面描述数据结构——渲染树
由图可以知道,渲染树由RenderNode渲染节点组成,描述了具体元素在屏幕上的布局信息,包含大小、位置以及一些其他属性。最后渲染服务的渲染线程会根据渲染树的信息执行相应的绘制工作
注意:在布局阶段中,如果视图嵌套层级深,且节点数过多,会导致在测算和布局阶段中,通过遍历测量组件大小和边界的时间过长,造成额外的计算,所以优化布局性能可以从减少节点数或减少测算布局耗时方面来考虑
精简节点数
在了解了布局过程的大概流程之后,通过数据对比来看一下节点数量对布局时间的影响
可以看出随着组件数量增加,性能呈现线性增长的劣化趋势
对比嵌套和平铺的情况下Row内组件个数在10、100、、500和1000条件下,平铺和嵌套在相同组件个数时性能差异不大且整体趋势保持一致
结论
真正影响布局性能的因素是参与布局的节点数量,所以在进行布局时,应该尽量减少参与布局的节点数,来减少布局的性能消耗。而针对减少布局的总节点数,主要有两个优化方向:
- 移除冗余的节点
- 使用扁平化布局减少节点数
合理控制元素的显示与隐藏
在ArkUI中,可以通过if条件渲染或者设置不同的visibility属性值来达到控制元素的显示和隐藏的效果
//if条件渲染
Row() {
Text('Hello World!')
if(this.visible) {
Column() {
...//100张Image组件
}
}
}
//visibility属性
Row() {
Text('Hello World!')
Column() {
...//100张Image组件
}.visibility(this.visibility ? Visibility.Visible : Visibility.None)
}
-
对比初次加载时的结果:
- if条件判断控制显示与隐藏的方式加载时,会根据条件值为true或false来判断是否创建对应组件内容。当条件为false时,对应的组件将不参与Measure和Layout阶段。
- 对于Visibility属性控制显示与隐藏的方式加载时,无论Visibility的值为Visibility.Visible还是Visibility.None,都会创建对应组件内容,当Visibility属性为Visibility.None时,对应的组件不参与Layout阶段
-
对比切换显示状态时的结果:
- 在使用if条件判断切换显示时,组件会因为条件改变而判断是否创建、布局过程。切换过程会造成较大的Measure的性能的消耗。
原因是创建了新的组件,创建了新的Measure和Layout过程
。 - 在使用Visibility的情况下,无论是否隐藏,组件都参与了创建,并一直都储存在组件树上,不会出现组件重新创建的过程。并且Measure和Layout的性能消耗,比使用if条件渲染的方式性能小很多。
原因是组件的计算在初次加载时已经计算过,不需要重复计算
。
- 在使用if条件判断切换显示时,组件会因为条件改变而判断是否创建、布局过程。切换过程会造成较大的Measure的性能的消耗。
总结
- 对性能要求过高,并且会频繁的切换元素显示与隐藏的情况下,应该避免使用if条件判断,而改为通过Visibility的属性控制。
- 如果组件的创建非常消耗资源,且不会立即使用,也并非频繁交互的情况下,只在特定的条件下才会出现时,可以通过if条件渲染来进行内容的显示与与隐藏控制,达到懒加载的效果
给定组件宽高
在拖拽、缩放时,如果部分内容的大小不需要自适应,可以通过给对应组件设置固定的宽和高来减少Measure过程的消耗。
Column(){
Button('修改宽度')
.onClick(() => {
this.testWidth = '90%'
}).height('20%')
Row(){
//400条文本数据
...
}
}.width(this.testWidth)
我们通过这段代码点击修改外层Column宽度来模拟缩放场景,对比给内层Row设置宽高的三种情况:
- 设置宽高分别为对应值300和400
- 设置宽高分别为“100%”和“70%”
- 不设置宽高属性
对比在初次加载和点击修改宽度下的Measure和Layout耗时
- 从初次加载的数据图可以看出,在初次加载的情况下,三种情况的加载耗时差距不大,这是由于在首次加载的情况下,所有组件都会参与布局的过程
- 在触发重新绘制的情况下,宽高为固定值比较设置为百分比和不设置宽高,Measure和Layout耗时明显更低。这是由于在未设置宽高和百分比宽高的情况下,外层容器发生变化时,组件本身也会触发重新进行Measure过程,对组件的宽高进行重新测算,导致其布局时间很长,而设置了固定宽高的组件则不会经过这一过程,而是直接使用初次绘制时保留的的节点大小数据,减少了Measure的时间
总结
对于能够在初期给定宽高的组件,在进行UI组件描述时尽量给定宽高的大小,能够减少由于容器尺寸的变化而重新测算过程的性能的消耗
使用推荐的布局组件
不同的布局容器使用的布局算法对性能带来的影响不同,我们应该根据场景使用合适的布局,尽量优先使用推荐的布局组件。
高级布局提供了场景化的能力,解决一种或多种布局场景,但是在一些场景下,不恰当的高级组件会额外增加性能的消耗。通过对不同的布局方式,设置对应容器相同的嵌套深度为5,总节点数为20个text的情况下,来对比其性能的消耗
不难发现,在嵌套深度和总结点数相同的情况下,使用基础组件如Column和Row容器的性能明显高于其他布局,Flex的性能明显低于Column和Row容器。Grid、RelativeContanier的性能消耗高于Column、Row和Stack等容器
以上数据只是基于相同节点数和布局层数的情况下的对比结果,反映了布局本身的相对性能消耗,但并不意味着使用了该组件性能就一定差,也不是任何情况下使用该组件都能保持良好的性能。因为在一些情况下,使用高级组件能够大大减少嵌套节点层数,和节点数,其带来的性能提升反而高于组件本身的性能消耗。
总结
- 在相同嵌套层级的情况下,如果多种布局方式可以实现相同的布局效果,优选低耗时的布局,如使用Row、Column和Flex来实现相同的单行布局
- 在能够通过其他布局大幅优化节点数的情况下可以使用高级组件替代,例如使用RelativeContanier替代Row和Column实现扁平化布局,此时布局收益大于布局本身的性能差距
- 仅在必要的场景下使用高耗时的布局组件,如使用Flex实现折行布局、使用Grid实现二维网格布局等