Skip to content

长列表优化

所有的前端应用,都会涉及到长列表渲染,长列表优化几乎是所有优化课程的课题。Flutter列表中,如果你的使用不当,即便是很短的列表,在滚动的过程中也会导致掉帧。这是因为列表中的组件,渲染的过程中一直被重构。今天我们就来说一下如何优化长列表。
Flutter中ListView采用懒加载机制。对于ListView里面的每一个item,并不会在build阶段全部进行构建。而是在layout阶段,根据屏幕当前的尺寸以及缓存区的范围,动态的构建每一个组件。

固定列表和长列表的区别

如果你的组件。使用Listview进行封装来滚动,但组件数量较少,且不会随着滚动而改变,那么你可以使用下面的代码实现:

dart
ListView(
  shrikWrap: true,
  children: <Widget>[
    const Text('1'),
    const Text('2'),
    const Text('3'),
    const Text('4'),
    const Text('5'),
  ]
)

但上面的代码中,有一个很严重的问题,就是列表中的组件,每次都会被重新构建。短列表中,防止这样的情况出现也十分必要,下面的章节中,我们会说到如何防止这样的情况发生。 而长列表的显著特征就是较多的列表项,或者无限加载的列表,像这样的列表,使用上面的方法来书写,肯定是不行的。只会随着列表长度的增加变得更卡,更慢,甚至内存超出导致闪退。长列表需要根据item长度,使用builder来进行渲染:

dart
ListView.builder(
  itemBuilder: (context, index) => ListItem(),
  itemCount: itemCount,
)

因为ListView.builder会按需构建列表元素,也就是只有那些可见得元素才会调用itemBuilder 构建元素,这样对于大列表而言性能开销自然会小很多。下面我们就进入正题,如何对长列表进行优化。

减少列表项的构建次数

我们在前面说到,Flutter的ListView会根据屏幕滑动时,自动根据组件是否出现在屏幕范围进行渲染和销毁的操作。所以当一个组件进入视野时,他就会被重新构建,退出设备视野时就会被销毁。而如果一个组件在列表中,被重新构建,这样会消耗很多性能。
这样的情况,不得不提到两个属性,addAutomaticKeepAlives 和 addRepaintBoundaries 。
addAutomaticKeepAlives这个构造函数适用于具有大量(或无限)子项的列表视图,因为对于实际可见的子项,构建器只会被调用一次。
addRepaintBoundaries这个构造函数适用于拥有大量(或无限)子项的列表视图,因为构建器只会为实际可见的子项调用,是将列表元素使用一个重绘边界(Repaint Boundary)包裹,从而使得滚动的时候可以避免重绘。而如果列表很容易绘制(列表元素布局比较简单的情况下)的时候,可以关闭这个特性来提高滚动的流畅度。
这两个参数默认值都是true,实际上很多时候我们不应该依赖于他

dart
addAutomaticKeepAlives: false,
addRepaintBoundaries: false,

注意,关闭这两个参数,需要自己来处理状态保持,否则效果相反,比如:使用AutomaticKeepAliveClientMixin来让组件保持状态,这样下次列表的子项目滚动进入视野时,build方法就不会被调用。 并且不会丢失状态,可以大幅度减少重绘的消耗。

dart
import 'package:flutter/material.dart';

class ExampleWidget extends StatefulWidget {
  const ExampleWidget({super.key});

  @override
  State<ExampleWidget> createState() => _ExampleWidgetState();
}

class _ExampleWidgetState extends State<ExampleWidget> with AutomaticKeepAliveClientMixin {

  @override
  bool get wantKeepAlive => true;

  @override
  Widget build(BuildContext context) {
    super.build(context);
    return const Placeholder();
  }
}

这样一来,即便在长列表中调用ExampleWidget,滚动的时候,ExampleWidget也只会被渲染一次。

确定性设置

itemExtent可以确定每个列表项的尺寸,避免在绘制的过程中,计算每个列表项的尺寸,产生不必要的运算,在已知的情况下,务必进行设置。 在Flutter中,没有名为itemExtentBuilderListView构造函数或属性。不过,ListView提供了一个名为itemExtent的属性,用于设置每个列表项的固定高度或宽度。 如果你希望为列表中的每个项设置不同的尺寸,你可以使用ListView.builder构造函数,并在itemBuilder中为每个项提供自定义的构建器。以下是一个示例:

dart
ListView.builder(
  itemCount: itemCount,
  itemBuilder: (context, index) {
    // 在这里构建每个列表项
    // 根据index决定特定项的尺寸
    double itemHeight = calculateItemHeight(index);
    return SizedBox(
      height: itemHeight,
      child: YourListItemWidget(),
    );
  },
)

在上面的示例中,通过ListView.builder构造函数,我们可以在itemBuilder中根据index来计算并设置每个列表项的尺寸。你可以根据自己的需求来定义calculateItemHeight方法或函数来返回不同的项尺寸。但这种方法已经是过去的版本中才需要的,现在你可以使用下面的新特性。
ListView.itemExtentBuilder是一个新的特性,ListView.itemExtent或者ListView.prototypeItem设置高度来提高Lazy Loading过程中的耗时,但这两个属性都是对所有items生效,如果items之间的高度不会绝对完全相同。

dart
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

class MyWidget extends StatelessWidget {
  const MyWidget({super.key});

  @override
  Widget build(BuildContext context) {
    return ListView.builder(
      itemCount: 500,
      itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
        if (index % 2 == 0) {
          return 30;
        } else {
          return 90;
        }
      },
      itemBuilder: (BuildContext context, int index) {
        const color = Colors.yellow;
        if (index % 2 == 0) {
          color = Colors.blue;
        }
        return ColoredBox(
          color: color,
          child: Center(
            child: Text('Render List Item at $index'),
          ),
        );
      },
    );
  }
}

通过上面的代码,你可以预知并为不同的Item设置高度这样一来,在绘制过程中,就不会计算每个列表项的尺寸,从而提高了性能。itemExtentBuilder入参为要获取高度的item的index索引。 SliverLayoutDimensions是Flutter中一个用于描述Sliver布局尺寸的类。在Flutter中,ListViewCustomScrollView等滚动视图组件使用Sliver来构建其子组件的布局。

SliverLayoutDimensions类包含以下属性:

  • scrollOffset:滚动偏移量,表示滚动视图中的内容相对于滚动位置的垂直距离。
  • visibleMainAxisExtent:可见主轴方向(垂直方向)的布局尺寸。
  • minScrollExtent:滚动范围的最小值,即内容的最上方离滚动位置的距离。
  • maxScrollExtent:滚动范围的最大值,即内容的最下方离滚动位置的距离。
  • hasVisualOverflow:是否有主轴方向上的溢出内容。

这些属性用于确定Sliver子组件的布局约束和渲染位置。Sliver布局中的子组件根据这些属性来计算其自身的布局。

const constructor

在Flutter中,const构造函数是一种优化技术,用于创建常量对象。常量对象是指在运行时无法改变的对象,例如字符串、数字、数组等。通过使用const构造函数,可以避免在运行时创建新的对象,从而提高性能。前面,我们已经提到过const构造函数,所以在列表中,尽可能的使用const构造函数,可以减少不必要的计算和内存分配。

cacheExtent的按需定义

在Flutter中,cacheExtent属性用于设置列表中预缓存的像素数。默认情况下,cacheExtent的值为0{double? cacheExtent},表示不预缓存任何内容。如果你希望在列表中预缓存一些内容,你可以将其设置为一个合适的值,例如1000.0。这可以提高列表的滚动性能,因为预缓存的像素数越多,滚动时加载的像素数就越少,从而减少了绘制和布局的次数。然而,需要注意的是,预缓存的像素数越多,内存占用越多,所以需要根据实际情况来确定预缓存。
对于包含大量(或无限)子项的列表视图,此构造函数非常适用,因为构建器仅针对实际可见的子项进行调用。
当调用时,itemBuilder应该始终创建小部件实例。避免使用返回先前构建的小部件的构建器;如果列表视图的子项在先前创建,或者在创建[ListView]本身时一次性全部创建,那么使用[ListView]构造函数更高效。然而,更高效的方法是使用这个构造函数的itemBuilder回调按需创建实例。
findChildIndexCallback对应于SliverChildBuilderDelegate.findChildIndexCallback属性。如果为null,在从children构建器返回的子项的顺序发生更改时,子部件可能无法映射到其现有的RenderObject。这可能导致状态丢失。如果子项的顺序可能在以后改变,就需要实现此回调。
注意当你将cacheExtent的大小设置得非常大(99999999)时,实际上是让ListView绘制当前显示项前后的所有子项,这样就消除了"卡顿"的行为。但是你必须明智地使用缓存,因为将其设置得太大会导致不愉快的副作用,比如用户的网络流量显著增加。延迟加载是有原因的。

dart
ListView.builder(
      itemCount: 500,
      itemExtentBuilder: (int index, SliverLayoutDimensions dimensions) {
        if (index % 2 == 0) {
          return 30;
        } else {
          return 90;
        }
      },
      cacheExtent: 1000,
      //...,
      }
)

避免使用较大内存的组件

注意在ListView中嵌入内存占用较大的组件,是永远不推荐的,比如较大的图片,这会占用大量内存,导致应用崩溃。或者Webview等PlatformView组件,滑动时也会这会占用大量内存。

避免布局变化

在列表滚动的时候,页面布局发生变化,或者不停更新列表项的尺寸,这是非常不明智的做法。布局变化,那么剩余空间的高度就得重新计算。

Performance overlay开启

MaterialApp中有showPerformanceOverlay的选项,在调试长列表时,一定要开启,并观测是否有重绘发生,或者掉帧的情况,针对掉帧或者渲染较慢的组件进行优化。开启showPerformanceOverlay是一个好习惯,就像你在web开发时,习惯打开devtool>console,观察页面的渲染情况的习惯,是一样的。

仅用于培训和测试,通过使用本站代码内容随之而来的风险与本站无关。版权所有,未经授权请勿转载,保留一切权利。
ICP备案号:滇ICP备15009214号-13   公安网备:滇公网安备 53312302000061号