HarmonyOS—UI 开发性能提升的推荐方法
注:本文转载自 HarmonyOS 官网文档
开发者若使用低性能的代码实现功能场景可能不会影响应用的正常运行,但却会对应用的性能造成负面影响。本章节列举出了一些可提升性能的场景供开发者参考,以避免应用实现上带来的性能劣化。
使用数据懒加载
开发者在使用长列表时,如果直接采用循环渲染方式,如下所示,会一次性加载所有的列表元素,一方面会导致页面启动时间过长,影响用户体验,另一方面也会增加服务器的压力和流量,加重系统负
上述代码会在页面加载时将 100 个列表元素全部加载,这并非我们需要的,我们希望从数据源中按需迭代加载数据并创建相应组件,因此需要使用数据懒加载,如下所示:
class BasicDataSource implements IDataSource { private listeners: DataChangeListener[] = [] public totalCount(): number { return 0 } public getData(index: number): any { return undefined } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { console.info('add listener') this.listeners.push(listener) } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { console.info('remove listener') this.listeners.splice(pos, 1) } } notifyDataReload(): void { this.listeners.forEach(listener => { listener.onDataReloaded() }) } notifyDataAdd(index: number): void { this.listeners.forEach(listener => { listener.onDataAdd(index) }) } notifyDataChange(index: number): void { this.listeners.forEach(listener => { listener.onDataChange(index) }) } notifyDataDelete(index: number): void { this.listeners.forEach(listener => { listener.onDataDelete(index) }) } notifyDataMove(from: number, to: number): void { this.listeners.forEach(listener => { listener.onDataMove(from, to) }) } } class MyDataSource extends BasicDataSource { private dataArray: string[] = ['item value: 0', 'item value: 1', 'item value: 2'] public totalCount(): number { return this.dataArray.length } public getData(index: number): any { return this.dataArray[index] } public addData(index: number, data: string): void { this.dataArray.splice(index, 0, data) this.notifyDataAdd(index) } public pushData(data: string): void { this.dataArray.push(data) this.notifyDataAdd(this.dataArray.length - 1) } } @Entry @Component struct MyComponent { private data: MyDataSource = new MyDataSource() build() { List() { LazyForEach(this.data, (item: string) => { ListItem() { Row() { Text(item).fontSize(20).margin({ left: 10 }) } } .onClick(() => { this.data.pushData('item value: ' + this.data.totalCount()) }) }, item => item) } } }
上述代码在页面加载时仅初始化加载三个列表元素,之后每点击一次列表元素,将增加一个列表元素。
设置 List 组件的宽高
在使用 Scroll 容器组件嵌套 List 组件加载长列表时,若不指定 List 的宽高尺寸,则默认全部加载。
说明
Scroll 嵌套 List 时:
● List 没有设置宽高,会布局 List 的所有子组件。
● List 设置宽高,会布局 List 显示区域内的子组件。
● List 使用ForEach加载子组件时,无论是否设置 List 的宽高,都会加载所有子组件。
● List 使用LazyForEach加载子组件时,没有设置 List 的宽高,会加载所有子组件,设置了 List 的宽高,会加载 List 显示区域内的子组件。
因此,此场景下建议设置 List 子组件的宽高。
class BasicDataSource implements IDataSource { private listeners: DataChangeListener[] = [] public totalCount(): number { return 0 } public getData(index: number): any { return undefined } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { console.info('add listener') this.listeners.push(listener) } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { console.info('remove listener') this.listeners.splice(pos, 1) } } notifyDataReload(): void { this.listeners.forEach(listener => { listener.onDataReloaded() }) } notifyDataAdd(index: number): void { this.listeners.forEach(listener => { listener.onDataAdd(index) }) } notifyDataChange(index: number): void { this.listeners.forEach(listener => { listener.onDataChange(index) }) } notifyDataDelete(index: number): void { this.listeners.forEach(listener => { listener.onDataDelete(index) }) } notifyDataMove(from: number, to: number): void { this.listeners.forEach(listener => { listener.onDataMove(from, to) }) } } class MyDataSource extends BasicDataSource { private dataArray: Array<string> = new Array(100).fill('test') public totalCount(): number { return this.dataArray.length } public getData(index: number): any { return this.dataArray[index] } public addData(index: number, data: string): void { this.dataArray.splice(index, 0, data) this.notifyDataAdd(index) } public pushData(data: string): void { this.dataArray.push(data) this.notifyDataAdd(this.dataArray.length - 1) } } @Entry @Component struct MyComponent { private data: MyDataSource = new MyDataSource() build() { Scroll() { List() { LazyForEach(this.data, (item: string, index: number) => { ListItem() { Text('item value: ' + item + (index + 1)).fontSize(20).margin(10) }.width('100%') }) }.width('100%').height(500) }.backgroundColor(Color.Pink) } }
使用条件渲染替代显隐控制
如下所示,开发者在使用 visibility 通用属性控制组件的显隐状态时,仍存在组件的重新创建过程,造成性能上的损耗。
@Entry @Component struct MyComponent { @State isVisible: Visibility = Visibility.Visible; build() { Column() { Button("显隐切换") .onClick(() => { if (this.isVisible == Visibility.Visible) { this.isVisible = Visibility.None } else { this.isVisible = Visibility.Visible } }) Row().visibility(this.isVisible) .width(300).height(300).backgroundColor(Color.Pink) }.width('100%') } }
要避免这一问题,可使用 if 条件渲染代替 visibility 属性变换,如下所示:
@Entry @Component struct MyComponent { @State isVisible: boolean = true; build() { Column() { Button("显隐切换") .onClick(() => { this.isVisible = !this.isVisible }) if (this.isVisible) { Row() .width(300).height(300).backgroundColor(Color.Pink) } }.width('100%') } }
![](https://static001.geekbang.org/infoq/bd/bd1fe9df76793dca1031f378b8db1650.gif)
使用 Column/Row 替代 Flex
由于 Flex 容器组件默认情况下存在 shrink 导致二次布局,这会在一定程度上造成页面渲染上的性能劣化。
@Entry @Component struct MyComponent { build() { Flex({ direction: FlexDirection.Column }) { Flex().width(300).height(200).backgroundColor(Color.Pink) Flex().width(300).height(200).backgroundColor(Color.Yellow) Flex().width(300).height(200).backgroundColor(Color.Grey) } } }
上述代码可将 Flex 替换为 Column、Row,在保证实现的页面布局效果相同的前提下避免 Flex 二次布局带来的负面影响。
@Entry @Component struct MyComponent { build() { Column() { Row().width(300).height(200).backgroundColor(Color.Pink) Row().width(300).height(200).backgroundColor(Color.Yellow) Row().width(300).height(200).backgroundColor(Color.Grey) } } }
![](https://static001.geekbang.org/infoq/f8/f8cb5d94e6db4b415a836eb1470c0b69.png)
减少应用滑动白块
应用通过增大 List/Grid 控件的 cachedCount 参数,调整 UI 的加载范围。cachedCount 表示屏幕外 List/Grid 预加载 item 的个数。
如果需要请求网络图片,可以在 item 滑动到屏幕显示之前,提前下载好内容,从而减少滑动白块。
如下是使用 cachedCount 参数的例子:
@Entry @Component struct MyComponent { private source: MyDataSource = new MyDataSource(); build() { List() { LazyForEach(this.source, item => { ListItem() { Text("Hello" + item) .fontSize(50) .onAppear(() => { console.log("appear:" + item) }) } }) }.cachedCount(3) // 扩大数值appear日志范围会变大 } } class MyDataSource implements IDataSource { data: number[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; public totalCount(): number { return this.data.length } public getData(index: number): any { return this.data[index] } registerDataChangeListener(listener: DataChangeListener): void { } unregisterDataChangeListener(listener: DataChangeListener): void { } }
![](https://static001.geekbang.org/infoq/00/008d9d4acbaa868c3dd8e14019f6007a.gif)
使用说明:
cachedCount 的增加会增大 UI 的 cpu、内存开销。使用时需要根据实际情况,综合性能和用户体验进行调整。