鸿蒙NEXT开发【瀑布流性能优化】常见性能场景

概述

瀑布流是应用开发中相当常见的开发场景。它通过容器自身的布局规则,将元素项目自上而下排列,在整体界面的呈现上,多列参差不齐、不停加载的形式使其内容看着像瀑布一样从上而下倾泻。借助其特点,瀑布流通常被用于展示图片资讯、购物商品、直播视频等多种形式的数据。当瀑布流上下滑动时,由于无限加载的特性,其能展示的数目非常多;大小不一的子元素,也带来了测量绘制的性能消耗。

针对瀑布流这种场景进行性能优化,可以在加快渲染速度、提升滑动帧率、降低内存占用等方面,增强应用的运行效率,进而提升用户的操作体验。

下面分别介绍优化瀑布流性能的主要方法。

懒加载

先看一下组件示例代码中瀑布流的基本用法:

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)通知,瀑布流就知道有新增数据可以继续加载,同时又不会重复处理已有数据。

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中判断距离数据终点的数量,提前增加数据的方式实现了无停顿的无限滚动。

2

固定宽高比

当新数据获取后,建议根据内容提前固定元素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等容器组件,实现了一个商城场景的瀑布流实例。

3

分别使用ForEach、LazyForEach,测试应用的完全显示所用时间、丢帧率、应用独占内存等各项指标,得到数据如下:

性能指标ForEachLazyForEach缓存数据项组件复用后固定宽高比后
完全显示所用时间2s 127ms262ms270ms266ms257ms
丢帧率28.2%4.6%2.9%0.5%0.0%
独占内存260.2MB81.4MB82.6MB80.2MB79.8MB

从测试结果可以看出,使用LazyForEach懒加载后,相关指标已经得到大幅度提高。使用其他优化方法,主要可以更进一步地减少滑动过程中的丢帧率,优化交互体验。此外,性能优化能够解决一些问题,比如缓存数据项和组件复用,可以减少“滑动白块“的现象,而固定宽高比,可以避免新加载元素瞬间刷新带来的“闪烁“问题。

最后,在瀑布流的性能优化上,我们可以总结如下:

  • 使用LazyForEach懒加载,针对较多数据可以快速渲染显示,同时减少内存占用。
  • 使用cachedCount缓存数据项,预先加载屏幕外的数据缓存,避免快速滑动时的加载“白块“。
  • 使用@Reusable复用组件,借助RecycleManager从复用池中取出的视图对象,快速绑定新的数据,减少创建、销毁视图带来的性能损耗。
  • 使用onReachEnd或onAppear实现瀑布流的新增数据加载,提供无限滚动的交互效果。
  • 预先固定FlowItem的宽高比,确定组件及子组件的宽高,减少布局阶段的measure过程,从而提升滑动过程中的流畅体验。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值