4、列表性能(上):ListView与GridView的复用机制,itemExtent优化,缓存区大小调整
列表,可以说是Flutter应用里最常用的组件了。聊天记录、商品列表、信息流……几乎每个App都离不开它。但很多同学跟我抱怨,说列表一长就卡顿,滑动起来掉帧严重。嗯,这其实不是Flutter的锅,而是你没搞懂它的复用机制。
今天我们就来聊聊ListView和GridView背后的那些事。说白了,就是搞清楚Flutter是怎么“偷懒”的,以及我们怎么帮它偷懒偷得更聪明。
一、复用机制:Flutter的“循环利用”哲学
先问大家一个问题:如果你的列表有10000条数据,你会不会创建10000个Widget?
当然不会。那样内存早就爆了。Flutter的做法是——只创建屏幕上可见的那几个Widget,然后不断“回收”和“重用”。
我刚开始接触Flutter时,以为它和Android的RecyclerView差不多。后来踩了个坑才发现,Flutter的复用机制其实更底层,也更灵活。
1.1 核心原理:Sliver与Viewport
ListView和GridView的底层,其实都是基于Scrollable + Viewport + Sliver这套架构。
- Viewport:就是那个“视口”,决定了当前能看到多少内容。
- Sliver:可以理解成“可滚动的碎片”。每个Sliver负责管理自己那一块区域的布局和回收。
当用户滑动时,Viewport会通知Sliver:“嘿,上面那个Widget已经滚出去了,你可以把它回收掉,然后创建下面那个新的。” 这就是复用。
关键点:Flutter不会销毁滚出屏幕的Widget,而是把它们放进一个“缓存池”。当新的item需要展示时,直接从池子里拿,而不是重新创建。
1.2 复用发生在哪里?
具体来说,复用发生在SliverList或SliverGrid的build方法中。每次调用itemBuilder时,Flutter会检查这个位置的Widget是否还在缓存中。
举个例子:
ListView.builder(
itemCount: 10000,
itemBuilder: (context, index) {
return ListTile(
title: Text('Item $index'),
);
},
)
这里itemBuilder只会在需要的时候被调用。屏幕上显示10个,它就只调用10次。滑出去一个,再滑进来一个,它才会再调用一次。
个人经验:我见过有人用ListView(children: [...])这种构造方式,直接把10000个Widget全塞进去。结果页面加载花了3秒,内存直接飙到200MB。记住,永远用.builder构造方法。
二、itemExtent优化:给列表一个“定心丸”
你有没有遇到过这种情况:列表滑动时,滚动条忽大忽小,滑动位置总是不准?
这其实是因为Flutter不知道每个item的高度。它必须等item构建完成后,才能测量出高度。对于不定高的item,每次测量都要重新布局,性能自然就差了。
2.1 什么是itemExtent?
itemExtent就是“固定item尺寸”。你告诉Flutter:“每个item的高度都是80像素,别量了,直接用这个值。”
这样一来,Flutter就能提前计算出列表的总高度、滚动位置、以及哪些item应该被创建。省去了测量的开销。
ListView.builder(
itemExtent: 80.0, // 每个item固定高度80
itemCount: 10000,
itemBuilder: (context, index) {
return Container(
color: Colors.blue[100 * (index % 9 + 1)],
child: Center(child: Text('Item $index')),
);
},
)
2.2 性能提升有多大?
我在项目中做过一个测试。一个包含5000个item的列表,每个item是一个简单的卡片布局。
| 方案 | 首次加载时间 | 滑动帧率 | 内存占用 |
|---|---|---|---|
| 不使用itemExtent | 320ms | 45fps | 85MB |
| 使用itemExtent | 180ms | 58fps | 62MB |
看到没?加载时间几乎减半,帧率也稳了。说白了,就是让Flutter少干了一件“测量”的苦力活。
注意:itemExtent只适用于所有item高度完全一致的情况。如果你的列表里有不同高度的item,千万别用。我曾经在一个混合布局的列表里强行用了itemExtent,结果UI直接崩了……
三、缓存区大小调整:给复用留点“余粮”
默认情况下,Flutter只会创建刚好填满屏幕的Widget。但这样有个问题:当你快速滑动时,新的item来不及创建,就会出现白屏或卡顿。
解决方案?增加缓存区。
3.1 什么是缓存区?
缓存区就是Viewport之外,额外保留的Widget数量。Flutter会提前创建好这些“预备役”Widget,等你滑到那里时,它们已经准备好了。
默认的缓存区大小是250像素。也就是说,屏幕上下各250像素范围内的item,都会被提前创建。
3.2 如何调整?
通过cacheExtent属性来调整:
ListView.builder(
cacheExtent: 500.0, // 缓存区扩大到500像素
itemCount: 10000,
itemBuilder: (context, index) {
return ListTile(title: Text('Item $index'));
},
)
或者用ScrollController来全局设置:
final controller = ScrollController(
initialScrollOffset: 0.0,
keepScrollOffset: true,
);
// 在initState中设置
controller.addListener(() {
// 动态调整缓存区
if (controller.position.extentAfter < 500) {
// 快到底部了,可以增加缓存
}
});
3.3 调多大合适?
这个没有标准答案。我个人的经验是:
- 普通列表:默认250像素就够了,别乱改。
- 快速滑动场景(比如相册、朋友圈):可以调到500-800像素。
- 超大列表(上万条):别调太大,否则内存会炸。我建议用
cacheExtent配合itemExtent一起用。
避坑指南:我曾经在一个聊天列表里把cacheExtent调到了2000像素,结果内存直接飙到300MB。后来发现,聊天列表里每条消息都包含图片,提前创建了太多图片Widget,内存自然扛不住。所以,缓存区大小要根据item的复杂度来定。
四、GridView的复用与优化
GridView和ListView的复用机制基本一样,只是布局方式不同。GridView是二维网格,所以它的缓存策略会稍微复杂一点。
4.1 GridView的缓存特点
GridView的缓存是按“行”来计算的。比如你设置了crossAxisCount: 3,那么缓存区会一次性创建3个item(一整行)。
这意味着,如果你把cacheExtent设得很大,GridView可能会一次性创建很多行,内存消耗比ListView更大。
4.2 优化建议
- 使用
gridDelegate的childAspectRatio:固定宽高比,可以减少测量开销。 - 避免在GridView中使用不定高的item:否则布局计算会非常复杂。
- 考虑用
SliverGrid替代GridView:如果你需要更精细的控制,Sliver系列组件更灵活。
GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.0, // 固定宽高比
),
itemCount: 1000,
itemBuilder: (context, index) {
return Container(
color: Colors.amber[100 * (index % 9 + 1)],
child: Center(child: Text('$index')),
);
},
)
五、总结与实战建议
好了,我们来捋一捋今天讲的核心点:
- 复用机制:Flutter通过Sliver + Viewport实现Widget的循环利用。记住,永远用.builder构造方法。
- itemExtent:固定item高度,让Flutter跳过测量步骤。适合所有item高度一致的场景。
- 缓存区大小:默认250像素,根据场景调整。快速滑动可以调大,复杂item要调小。
最后,给大家一个实战建议:
如果你在做一个商品列表,每个商品卡片高度固定,那就用itemExtent + 默认缓存区。如果你在做朋友圈,图片大小不一,那就别用itemExtent,但可以把cacheExtent调到500像素,保证快速滑动不白屏。
嗯,下一章我们会继续聊列表性能的进阶话题——懒加载、分页加载与无限滚动。到时候我会分享一个我在电商项目中踩过的坑,保证让你少走弯路。
一句话总结:列表性能优化的核心,就是让Flutter少干活——少创建、少测量、少布局。