概述
瀑布流是应用开发中相当常见的开发场景。它通过容器自身的布局规则,将元素项目自上而下排列,在整体界面的呈现上,多列参差不齐、不停加载的形式使其内容看着像瀑布一样从上而下倾泻。借助其特点,瀑布流通常被用于展示图片资讯、购物商品、直播视频等多种形式的数据。当瀑布流上下滑动时,由于无限加载的特性,其能展示的数目非常多;大小不一的子元素,也带来了测量绘制的性能消耗。
针对瀑布流这种场景进行性能优化,可以在加快渲染速度、提升滑动帧率、降低内存占用等方面,增强应用的运行效率,进而提升用户的操作体验。
下面分别介绍优化瀑布流性能的主要方法。
懒加载
先看一下组件示例代码中瀑布流的基本用法:
build() {
Column({ space: 2 }) {
WaterFlow() {
LazyForEach(this.datasource, (item: number) => {
FlowItem() {
Column() {
Text("N" + item).fontSize(12).height('16')
Image('res/waterFlowTest (' + item % 5 + ').jpg')
.objectFit(ImageFit.Fill)
.width('100%')
.layoutWeight(1)
}
}
.width('100%')
.height(this.itemHeightArray[item])
.backgroundColor(this.colors[item % 5])
}, (item: string) => item)
}
.columnsTemplate("1fr 1fr")
.columnsGap(10)
.rowsGap(5)
.backgroundColor(0xFAEEE0)
.width('100%')
.height('80%')
}
}
示例代码已经使用了LazyForEach进行数据懒加载,WaterFlow布局时会根据可视区域按需创建FlowItem组件,并在FlowItem滑出可视区域外时销毁以降低内存占用。
瀑布流的开发,也属于长列表加载的一种场景
缓存数据项
LazyForEach懒加载可以通过设置cachedCount来指定缓存数量,在设置cachedCount后,除屏幕内显示的Item组件外,还会预先将屏幕可视区外指定数量的数据缓存。这样当一个屏幕数据加载完成后,再次向下滑动时,会先加载上一次请求的数据,加载完成后,再加载本次请求的数据。
组件复用
考虑到滑动场景存在FlowItem及其子组件的频繁创建和销毁,可以将FlowItem中的组件封装成自定义组件,并使用@Reusable装饰器修饰,使其具备组件复用能力,减少ArkUI框架内部反复创建销毁节点的开销。
无限滚动
前文示例代码中FlowItem数量是固定的,不能满足无限滚动的场景。
基于WaterFlow本身提供的能力,可以在onReachEnd时给LazyForEach数据源增加新数据,并将footer做成正在加载新数据的样式(使用LoadingProgress组件)。
build() {
Column({ space: 2 }) {
WaterFlow({ footer: this.itemFoot.bind(this) }) {
LazyForEach(this.datasource, (item: number) => {
FlowItem() {
Column() {
Text("N" + item).fontSize(12).height('16')
Image('res/waterFlowTest (' + item % 5 + ').jpg')
.objectFit(ImageFit.Fill)
.width('100%')
.layoutWeight(1)
}
}
.width('100%')
.height(this.itemHeightArray[item % 100])
.backgroundColor(this.colors[item % 5])
}, (item: string) => item)
}
// 触底加载数据
.onReachEnd(() => {
console.info("onReachEnd")
setTimeout(() => {
for (let i = 0; i < 100; i++) {
this.datasource.addLastItem()
}
}, 1000)
})
.columnsTemplate("1fr 1fr")
.columnsGap(10)
.rowsGap(5)
.backgroundColor(0xFAEEE0)
.width('100%')
.height('80%')
}
}
// 在数据尾部增加一个元素
public addLastItem(): void {
this.dataArray.splice(this.dataArray.length, 0, this.dataArray.length);
this.notifyDataAdd(this.dataArray.length - 1);
}
此处需要通过在尾部增加元素的方式新增数据,不能使用直接修改dataArray后通过LazyForEach的onDataReloaded()通知瀑布流重新加载数据。
由于瀑布流布局子组件高度不相等的特点,下面节点的位置依赖上面的节点,重新加载所有数据会触发整个瀑布流重新计算布局导致卡顿。而在数据末尾增加数据后使用notifyDataAdd(this.dataArray.length - 1)通知,瀑布流就知道有新增数据可以继续加载,同时又不会重复处理已有数据。
提前新增数据
虽然在onReachEnd()触发时新增数据可以实现无限加载,但在滑动到底部时,会有明显的停顿加载新数据的过程。
想要流畅的进行无限滑动,还需要调整下增加新数据的时机。比如可以在LazyForEach还剩若干个数据就迭代到结束的情况下提前增加一些新数据。
build() {
Column({ space: 2 }) {
WaterFlow() {
LazyForEach(this.datasource, (item: number) => {
FlowItem() {
Column() {
Text("N" + item).fontSize(12).height('16')
Image('res/waterFlowTest (' + item % 5 + ').jpg')
.objectFit(ImageFit.Fill)
.width('100%')
.layoutWeight(1)
}
}
.onAppear(() => {
// 即将触底时提前增加数据
if (item + 20 == this.datasource.totalCount()) {
for (let i = 0; i < 100; i++) {
this.datasource.addLastItem()
}
}
})
.width('100%')
.height(this.itemHeightArray[item % 100])
.backgroundColor(this.colors[item % 5])
}, (item: string) => item)
}
.columnsTemplate("1fr 1fr")
.columnsGap(10)
.rowsGap(5)
.backgroundColor(0xFAEEE0)
.width('100%')
.height('80%')
}
}
此处通过在FlowItem的onAppear中判断距离数据终点的数量,提前增加数据的方式实现了无停顿的无限滚动。
固定宽高比
当新数据获取后,建议根据内容提前固定元素FlowItem的宽高比,从而计算FlowItem高度(因宽度在2列情况下是确定的)。固定宽高比后,就可以在UI描述时指定组件的宽高数值。如果组件本身的宽高是固定的,理论上来讲,该组件在布局阶段不会参与measure阶段,其节点中保存了对应的大小信息,当瀑布流内容较多时,由于避免了组件整体的测算过程,性能会带来一定的提升。
另外,由于Image组件默认异步加载,提前设定FlowItem的高度,可以避免图片加载成功后高度变化触发的瀑布流布局刷新。
// 计算FlowItem高度
getSize() {
let ret = Math.floor(Math.random() * this.maxSize)
return (ret > this.minSize ? ret : this.minSize)
}
// 设置FlowItem的高度数组
setItemSizeArray() {
for (let i = 0; i < 100; i++) {
this.itemHeightArray.push(this.getSize())
}
}
build() {
Column({ space: 2 }) {
WaterFlow() {
LazyForEach(this.datasource, (item: number) => {
FlowItem() {
...
}
.width('100%')
// 从数组中取得数值,设置FlowItem的高度
.height(this.itemHeightArray[item % 100])
}, (item: string) => item)
}
...
}
}
实践总结
这里基于WaterFlow,结合Tabs、Refresh、Scroll、Swiper、Grid等容器组件,实现了一个商城场景的瀑布流实例。
分别使用ForEach、LazyForEach,测试应用的完全显示所用时间、丢帧率、应用独占内存等各项指标,得到数据如下:
性能指标 | ForEach | LazyForEach | 缓存数据项 | 组件复用后 | 固定宽高比后 |
---|---|---|---|---|---|
完全显示所用时间 | 2s 127ms | 262ms | 270ms | 266ms | 257ms |
丢帧率 | 28.2% | 4.6% | 2.9% | 0.5% | 0.0% |
独占内存 | 260.2MB | 81.4MB | 82.6MB | 80.2MB | 79.8MB |
从测试结果可以看出,使用LazyForEach懒加载后,相关指标已经得到大幅度提高。使用其他优化方法,主要可以更进一步地减少滑动过程中的丢帧率,优化交互体验。此外,性能优化能够解决一些问题,比如缓存数据项和组件复用,可以减少“滑动白块“的现象,而固定宽高比,可以避免新加载元素瞬间刷新带来的“闪烁“问题。
最后,在瀑布流的性能优化上,我们可以总结如下:
- 使用LazyForEach懒加载,针对较多数据可以快速渲染显示,同时减少内存占用。
- 使用cachedCount缓存数据项,预先加载屏幕外的数据缓存,避免快速滑动时的加载“白块“。
- 使用@Reusable复用组件,借助RecycleManager从复用池中取出的视图对象,快速绑定新的数据,减少创建、销毁视图带来的性能损耗。
- 使用onReachEnd或onAppear实现瀑布流的新增数据加载,提供无限滚动的交互效果。
- 预先固定FlowItem的宽高比,确定组件及子组件的宽高,减少布局阶段的measure过程,从而提升滑动过程中的流畅体验。