概述
在实现如下图所示可滚动布局效果时,可能会通过[columnStart/columnEnd]对GridItem设置其占有行列数,实现不规则的布局效果。
图1 columnStart/columnEnd实现不规则网格布局
如果遇到如下的使用场景,使用columnStart/columnEnd可能会造成性能问题:
- 删除或拖拽等改变GridItem位置
- 使用scrollToIndex滑动到指定GridItem
Grid中存在大量GridItem,当使用columnStart/columnEnd、rowStart/rowEnd设置GridItem大小,该场景下耗时过长时,应该考虑使用GridLayoutOptions提升性能。使用columnStart/columnEnd、rowStart/rowEnd布局,在scrollToIndex滑动指定Index时,Grid会遍历GridItem去查找位置。而使用GridLayoutOptions布局,在scrollToIndex滑动指定Index时,则是通过计算方式去查找位置,查找GridItem位置效率更高。对此,可以通过GridLayoutOptions布局选项,配合rowsTemplate、columnsTemplate仅设置其中一个的Grid使用,替代通过columnStart/columnEnd控制GridItem占用多列的情况
案例说明
场景示例
下面介绍Grid中使用scrollToIndex滑动到指定位置的场景为例。在案例中采有了columnStart/columnEnd设置了不规则的宫格布局的反例,以及才有GridLayoutOption的正例进行对比,示例代码如下:
反例: 使用columnStart,columnEnd设置GridItem大小。
// 导入性能打点模块
import hiTraceMeter from '@ohos.hiTraceMeter';
@Component
struct TextItem {
@State item: string = "";
build() {
Text(this.item)
.fontSize(16)
.backgroundColor(0xF9CF93)
.width('100%')
.height(80)
.textAlign(TextAlign.Center)
}
aboutToAppear() {
// 结束打点任务
hiTraceMeter.finishTrace("useColumnStartColumnEnd", 1);
}
}
class MyDataSource implements IDataSource {
private dataArray: string[] = [];
public pushData(data: string): void {
this.dataArray.push(data);
}
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): string {
return this.dataArray[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
}
unregisterDataChangeListener(listener: DataChangeListener): void {
}
}
@Entry
@Component
struct GridExample {
private datasource: MyDataSource = new MyDataSource();
scroller: Scroller = new Scroller();
aboutToAppear() {
for (let i = 1; i <= 2000; i++) {
this.datasource.pushData(i + '');
}
}
build() {
Column({ space: 5 }) {
Text('使用columnStart,columnEnd设置GridItem大小').fontColor(0xCCCCCC).fontSize(9).width('90%')
Grid(this.scroller) {
LazyForEach(this.datasource, (item: string, index: number) => {
if ((index % 4) === 0) {
GridItem() {
TextItem({ item: item })
}
.columnStart(0).columnEnd(2)
} else {
GridItem() {
TextItem({ item: item })
}
}
}, (item: string) => item)
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.width('90%')
.height('40%')
Button("scrollToIndex:1900").onClick(() => {
// 开始打点任务
hiTraceMeter.startTrace("useColumnStartColumnEnd", 1);
this.scroller.scrollToIndex(1900);
})
}.width('100%')
.margin({ top: 5 })
}
}
正例: 使用GridLayoutOptions设置GridItem大小,布局效果和反例保持一致。
// 导入性能打点模块
import hiTraceMeter from '@ohos.hiTraceMeter';
@Component
struct TextItem {
@State item: string = "";
build() {
Text(this.item)
.fontSize(16)
.backgroundColor(0xF9CF93)
.width('100%')
.height(80)
.textAlign(TextAlign.Center)
}
aboutToAppear() {
// 结束打点任务
hiTraceMeter.finishTrace("useGridLayoutOptions", 1);
}
}
class MyDataSource implements IDataSource {
private dataArray: string[] = [];
public pushData(data: string): void {
this.dataArray.push(data);
}
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): string {
return this.dataArray[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
}
unregisterDataChangeListener(listener: DataChangeListener): void {
}
}
@Component
export struct GridExample2 {
private datasource: MyDataSource = new MyDataSource();
scroller: Scroller = new Scroller();
private irregularData: number[] = [];
layoutOptions: GridLayoutOptions = {
regularSize: [1, 1],
irregularIndexes: this.irregularData,
};
aboutToAppear() {
for (let i = 1; i <= 2000; i++) {
this.datasource.pushData(i + '');
if ((i - 1) % 4 === 0) {
this.irregularData.push(i - 1);
}
}
}
build() {
Column({ space: 5 }) {
Text('使用GridLayoutOptions设置GridItem大小').fontColor(0xCCCCCC).fontSize(9).width('90%')
Grid(this.scroller, this.layoutOptions) {
LazyForEach(this.datasource, (item: string, index: number) => {
GridItem() {
TextItem({ item: item })
}
}, (item: string) => item)
}
.columnsTemplate('1fr 1fr 1fr')
.columnsGap(10)
.rowsGap(10)
.width('90%')
.height('40%')
Button("scrollToIndex:1900").onClick(() => {
// 开始打点任务
hiTraceMeter.startTrace("useGridLayoutOptions", 1);
this.scroller.scrollToIndex(1900);
})
}.width('100%')
.margin({ top: 5 })
}
}
分析步骤
正反例采用相同的操作步骤,收集跳转过程中的性能参数,并进行对比:
-
打开Profiler工具,连接设备,选择对应的app进程
-
选择Frame,点击create Session开始测量收集数据
-
通过点击按钮,在调用scrollToIndex之前使用startTrace开始性能打点跟踪。
-
查看对应应用进程下的自定义打点事件,对应反例代码中定义的“useColumnStartColumnEnd”以及正例代码中的“useGridLayoutOptions”下的trace图。
说明
打点事件说明:当Grid查找到指定GridItem位置,准备渲染GridItem节点前,会进入GridItem自定义组件的生命周期回调aboutToAppear,在aboutToAppear里使用finishTrace停止性能打点跟踪。通过性能打点方式,使用startTrace标记调用scrollToIndex作为开始打点的位置,使用finishTrace标记查找到指定位置后准备渲染首个GridItem节点作为结束打点位置,来对比正反例场景下的耗时数据。
结果对比
如图所示,使用columnStart,columnEnd设置GridItem大小的布局方式,从自定义打点标签“H:useColumnStartColumnEndGrid”上可以看出从调用scrollToIndex到查找到指定Index准备构建GridItem节点耗时447ms。
图2 使用columnStart,columnEnd的打点信息
如图3所示,使用GridLayoutOptions设置GridItem大小的布局方式,从自定义打点标签“H:useGridLayoutOption”上可以看出从调用scrollToIndex到查找到指定Index准备构建GridItem节点耗时12ms。
图3 使用GridLayoutOptions的打点信息
从详细的trace进行分析可以发现,在“H:useColumnStartColumnEndGrid”打点标签时间段中存在大量的“H:Builder:BuildLazyItem”标签,可以发现Grid在查找指定的Index:1900是依次遍历Index来查找的。
图4 使用columnStart,columnEnd的放大trace标签信息
而使用GridLayoutOption的示例中,在“H:useGridLayoutOptions”打点标签时间段中只存在一个“H:Builder:BuildLazyItem”标签,可以发现Grid在查找指定Index:1900是直接一次查找到指定Index的。
图5 使用GridLayoutOptions的放大trace标签信息
通过上述分析可以发现,在相同布局情况下,使用columnStart,columnEnd设置GridItem大小方式,Grid在使用scrollToIndex查找指定Index时,会依次遍历GridItem节点,查找过程耗时较长。而使用GridLayoutOptions设置GridItem大小方式,是直接一次计算找到指定Index,查找过程耗时较短。所以使用GridLayoutOptions设置GridItem大小方式,在使用scrollToIndex滑动指定Index时,能够有效减少Grid加载时间,提升应用性能。