Appearance
长列表优化
所有的前端应用,都会涉及到长列表渲染,长列表优化几乎是所有优化课程的课题。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中,没有名为itemExtentBuilder
的ListView
构造函数或属性。不过,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中,ListView
、CustomScrollView
等滚动视图组件使用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,观察页面的渲染情况的习惯,是一样的。