【HarmonyOS】- 长列表优化


知识回顾

前言

针对这类大量数据加载的长列表应用,如何对长列表的性能进行优化是非常重要的。一个正确、高性能的长列表应用能明显降低列表渲染时间、提升页面的滑动帧率、降低应用内存占用,大幅提升用户体验。


源码分析

1. 懒加载

从测试数据可以看出:

在100条数据范围内ForEach和LazyForEach差距不大,总体而言两者各项性能指标都在可接受范围内,而ForEach代码逻辑比LazyForEach简单,此场景下使用ForEach即可。
当数据大于1000条,特别是当数据达到10000条时,ForEach在列表渲染、应用内存占用、丢帧率等各个方面都会有“指数级别”的显著劣化,滑动会出现明显的卡顿,甚至会出现应用crash等现象。
使用LazyForEach除了最后内存稍微增大以外,其列表渲染时间、丢帧率都不会出现明显变化,具有较好的性能。

2. 缓存列表项

在进行列表加载时需要尽量避免一次性加载全部列表数据项,即推荐按需加载列表数据,但实际上这个“需”是有讲究的。例如本例中,页面一次可以显示6条数据,如果不提前缓存部分数据,当下滑到列表最底端时,再快速下滑,可能会引起“滑动白块”的现象。这是因为上一次只请求了屏幕上的6条数据,如果滑动速度过快,则会导致数据来不及加载而出现白块。在追求极致性能的同时,应该避免这样糟糕的用户体验。

LazyForEach懒加载可以通过设置cachedCount来指定缓存数量,在设置cachedCount后,除屏幕内显示的ListItem组件外,还会预先将屏幕可视区外指定数量的列表项数据缓存。这样当一个屏幕数据加载完成后,再次向下滑动时,会先加载上一次请求的数据,加载完成后,再加载本次请求的数据。LazyForEach添加了cachedCount缓存列表项后,其渲染过程如下:

首先,请求n+cachedCount条数据,并在屏幕上显示n条数据。
当列表滑动,缓存列表项需要从屏幕可视区外进入可视区内时,此时只用渲染组件即可,相比不设置cachedCount提升了显示效率。
当列表不断滑动,屏幕可视区外缓存的列表项数量少于cachedCount设置数量时,会触发列表项数据加载事件,继续预加载下一组缓存列表项(cachedCount个)。
当上滑下滑间隔进行时,列表两个方向分别缓存cachedCount条数据。
如果不显式设置cachedCount,cachedCount默认为1

3. 动态预加载

LazyForEach懒加载可以通过设置cachedCount来指定缓存数量,解决当下滑到列表最底端时,再快速下滑,可能会引起“滑动白块”的现象。如若用户使用大量在线数据,实际上在弱网以及快速滑动的场景下依旧会在滑动过程中出现白块,如果将cachedCount设置比较大,滑动过程中可能不会出现白块,但是首屏加载时间过长。在追求极致性能的同时,应该避免这样糟糕的用户体验。

Prefetcher支持应用动态自适应网络状态,通过提前下载一些图片或资源,确保相关资源在需要时能立即显示,以尽可能减少白块出现的概率。

LazyForEach懒加载可以通过使用Prefetcher来预取和预渲染数据,在使用Prefetcher后,除屏幕内显示的ListItem组件外,还会预先将屏幕可视区外的部分列表项数据进行预渲染和预取。这样当列表向下滑动时,会先显示预渲染组件,屏幕可视区外会动态调整预取范围。预取逻辑在Prefetcher的BasicPrefetcher类中实现,BasicPrefetcher支持预取和预渲染(图像解码、添加到组件树等)过程分离、自适应调整预获取范围、优先加载可视区域、以及取消不必要任务(快速滚动列表的场景下,智能取消不必要任务),其渲染过程如下:

首先,请求n条数据,并在屏幕上显示m条数据。
当列表滑动,缓存列表项需要从屏幕可视区外进入可视区内时,此时显示预渲染组件,屏幕可视区外会动态调整预取范围,相比仅设置cachedCount提升了显示效率。
当列表不断滑动,屏幕可视区外实时更新列表项、更新预取数据和预渲染数据。

import { SongInfoItem } from '../model/LearningResource';
import DataSourcePrefetchingRCP from '../model/ArticleListData';
import { ObservedArray } from '../utils/ObservedArray';
import { ReusableArticleCardView } from '../components/ReusableArticleCardView';
import Constants from '../constants/Constants';
import { util } from '@kit.ArkTS';
import PageViewModel from '../components/PageViewModel';
import { BasicPrefetcher } from '@kit.ArkUI';

@Entry
@Component
export struct LazyForEachListPage {
  @State collectedIds: ObservedArray<string> = ['1', '2', '3', '4', '5', '6'];
  @State likedIds: ObservedArray<string> = ['1', '2', '3', '4', '5', '6'];
  @State isListReachEnd: boolean = false;
  // 创建DataSourcePrefetchingRCP对象,具备任务预取、取消能力的数据源
  private readonly dataSource = new DataSourcePrefetchingRCP(PageViewModel.getItems());
  // 创建BasicPrefetcher对象,默认的动态预取算法实现
  private readonly prefetcher = new BasicPrefetcher(this.dataSource);

  build() {
    Column() {
      Header()
      List({ space: Constants.SPACE_16 }) {
        LazyForEach(this.dataSource, (item: SongInfoItem) => {
          ListItem() {
            Column({ space: Constants.SPACE_12 }) {
              ReusableArticleCardView({ articleItem: item })
            }
          }
          .reuseId('article')
        })
      }
      .cachedCount(5)
      .onScrollIndex((start: number, end: number) => {
        // 列表滚动触发visibleAreaChanged,实时更新预取范围,触发调用prefetch、cancel接口
        this.prefetcher.visibleAreaChanged(start, end)
      })
      .width(Constants.FULL_SCREEN)
      .height(Constants.FULL_SCREEN)
      .margin({ left: 10, right: 10 })
      .layoutWeight(1)
    }
    .backgroundColor($r('app.color.text_background'))
  }
}
// ...

在这里插入图片描述
在这里插入图片描述
普通加载在这里插入图片描述
动态预加载在这里插入图片描述
因此当用户使用LazyForEach在线加载含有图片等数据量比较大的资源时,可以考虑使用动态预加载来预防弱网以及快速滑动场景中出现的白块问题。

4. 组件复用

组件复用的原理和机制

组件复用机制如下:

标记为@Reusable的组件从组件树上被移除时,组件和其对应的JSView对象都会被放入复用缓存中。
当列表滑动新的ListItem将要被显示,List组件树上需要新建节点时,将会从复用缓存中查找可复用的组件节点。
找到可复用节点并对其进行更新后添加到组件树中。从而节省了组件节点和JSView对象的创建时间。

组件复用生效的条件

自定义组件被@Reusable装饰器修饰,即表示其具备组件复用的能力;
在一个自定义父组件下创建出来的具备组件复用能力的自定义子组件,在可复用自定义组件从组件树上移除之后,会被加入到其自定义父组件的可复用节点缓存中;

在一个自定义父组件下创建可复用的子组件时,若其父自定义组件的可复用节点缓存中有对应类型的可复用子组件,会通过更新可复用子组件的方式,快速创建可复用子组件;
ForEach循环渲染会一次性加载全量数据,因此不支持组件复用。
在这里插入图片描述

说明
名词介绍:
@Reusable表示组件可以被复用,结合LazyForEach懒加载一起使用,可以进一步解决列表滑动场景的瓶颈问题,提供滑动场景下高性能创建组件的方式来提升滑动帧率。
CustomNode是一种自定义的虚拟节点,它可以用来缓存列表中的某些内容,以提高性能和减少不必要的渲染。通过使用CustomNode,可以实现只渲染当前可见区域内的数据项,将未显示的数据项缓存起来,从而减少渲染的数量,提高性能。
RecycleManager是一种用于优化资源利用的回收管理器。当一个数据项滚出屏幕时,不会立即销毁对应的视图对象,而是将该视图对象放入复用池中。当新的数据项需要在屏幕上展示时,RecycleManager会从复用池中取出一个已经存在的视图对象,并将新的数据绑定到该视图上,从而避免频繁的创建和销毁过程。通过使用RecycleManager,可以大大减少创建和销毁视图的次数,提高列表的滚动流畅度和性能表现。
CachedRecycleNodes是CustomNode的一个集合,常是用于存储被回收的CustomNode对象,以便在需要时进行复用。

使用场景

若业务实现中存在以下场景,并成为UI线程的帧率瓶颈,推荐使用组件复用:

  • 列表滚动(本例中的场景):当应用需要展示大量数据的列表,并且用户进行滚动操作时,频繁创建和销毁列表项的视图可能导致卡顿和性能问题。在这种情况下,使用列表组件的组件复用机制可以重用已经创建的列表项视图,提高滚动的流畅度。
  • 动态布局更新:如果应用中的界面需要频繁地进行布局更新,例如根据用户的操作或数据变化动态改变视图结构和样式,重复创建和销毁视图可能导致频繁的布局计算,影响帧率。在这种情况下,使用组件复用可以避免不必要的视图创建和布局计算,提高性能。
  • 地图渲染:在地图渲染这种场景下,频繁创建和销毁数据项的视图可能导致性能问题。使用组件复用可以重用已创建的视图,只更新数据的内容,减少视图的创建和销毁,能有效提高性能。

以上场景中,为了避免UI线程的帧率瓶颈问题,推荐使用组件复用来提高应用的性能和用户体验。组件复用可以避免不必要的视图创建和销毁,减少布局计算和绘制操作,从而提高界面的流畅度和响应速度。

@Component
@Reusable
export struct ArticleCardView {
  @Prop isCollected: boolean = false;
  @Prop isLiked: boolean = false;
  @Prop articleItem: LearningResource = new LearningResource();
  onCollected?: () => void;
  onLiked?: () => void;

  aboutToReuse(params: Record<string, Object>): void {
    this.onCollected = params.onCollected as () => void;
    this.onLiked = params.onLiked as () => void;
  }

  build() {
    ......
  }
}

无需对@Prop修饰的变量进行赋值,因为这些变量是由父组件传递给子组件的。如果在子组件中重新赋值这些变量,会导致重用的组件的内容重新触发状态刷新,从而降低组件的复用性能。相反,只需要在aboutToReuse方法中对onCollected和onLiked这两个函数进行重新赋值即可。

5. 布局优化

最大嵌套层级控制在5-8层左右即可,过度的优化布局会导致代码开发难度加大,代码不易于阅读理解,增加后续的维护成本,不利于多设备的适配,且也不会带来特别显著的性能提升。

拓展知识

总结

  • ForEach 和 LazyForEach 对比:ForEach 是从列表数据源一次性加载全量数据并全部挂载在组件树上,适用于数据量少且不是性能瓶颈的场景;LazyForEach 按需加载部分数据,构建短小组件树,适用于数据量大易产生性能瓶颈的场景。数据量小于 100 条时两者差距不大,大于 1000 条特别是 10000 条时,ForEach 性能显著劣化,而 LazyForEach 性能较好。
  • 缓存列表项:LazyForEach 懒加载可通过设置 cachedCount 指定缓存数量,避免快速滑动出现 “滑动白块” 现象,适合加载数据请求耗时场景。一般缓存数量为一屏显示列表数的一半时效果较好,但实际开发中需根据场景合理设置,以平衡体验和内存。
  • 动态预加载:LazyForEach 可通过使用 Prefetcher 预取和预渲染数据,提升显示效率,适合加载数据请求耗时场景,如含有大量图片等资源的列表。通过对比不同设置的性能,发现动态预加载在首屏加载时间、白块数量和 CPU 开销方面有较好表现。

针对长列表这一场景,在本地模拟了10、100、1000、10000条数据,分别使用ForEach、LazyForEach,测试关闭和开启懒加载的完全显示所用时间、丢帧率、应用独占内存等各项指标。测试表明,使用LazyForEach懒加载这项技术后,相比ForEach这种加载方式,在列表数据量较小(100条内)且数据一次性全量加载不是性能瓶颈时,两者各项性能指标差异不大。但当列表数据较长特别是达到10000条数据量后,ForEach的上述4项性能指标会有“指数级别”的显著劣化,滑动会出现明显的卡顿,甚至会出现应用crash等现象;而LazyForEach因为采用了懒加载技术能明显减少首屏完全显示所用时间,降低应用的独占内存,提高页面滑动帧率,带来更好的性能。

需要指出的是,ForEach、LazyForEach、缓存列表项、组件复用、布局优化是在本地模拟的10000条数据,是在同等情况下,采用控制变量的方法对ForEach和LazyForEach进行压力测试而得出的数据结论。动态预加载则是模拟在弱网以及快速滑动的状态下加载数据测试而得出的数据结论。当利用网络数据来探讨LazyForEach代码如何进行网络数据的加载和优化时,可以使用动态预加载,使用动态预加载这项技术后,因将预取和预渲染分离且在滑动过程中实时更新列表项、预取数据和预渲染数据,故能在弱网和快速滑动场景中明显减少滑动过程中出现的白块现象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值