知道rebuild原理至关重要,因为rebuild会直接影响性能。
本文目的:
- rebuild的机制是什么?
- 如何减少rebuild,提升性能?
什么是rebuild?
widget的build方法被再次调用,我们说是这个widget被rebuild了。
为什么rebuild影响性能?
- widget创建成本:build方法被调用本身就会消耗cpu,如果返回的子孙widget比较多,会进一步降低性能
- 逻辑成本:很多程序员会在build方法内写一些逻辑,这些逻辑进一步降低性能
- widget的diff成本:Flutter 使用一种称为“diffing”的算法来比较新旧 Widget 树,以确定哪些部分需要更新。尽管这个过程是高效的,但如果树非常大或者变化频繁,这个过程本身也会消耗计算资源。
rebuild本质是什么?
在 Flutter 中,真正决定是否调用 build() 的不是 Widget 本身是否 const,而是对应的 Element 是否被标记为需要重建(Element标记它为 dirty)。
Element被标记为dirty后,就会调用对用widget的build方法。
那么我们只要知道Element什么时候被标记为dirty,就知道widget什么时候rebuild
Element什么时候被标记为dirty?
只要新旧 Widget 实例不是同一个(非 identical),即使类型一样,Flutter 会默认调用 update(),进而把 Element 标记为 dirty。
上面是一句看不懂的屁话,但是还是要看,我们举例说明
背景知识:Widget 和 Element 的配对
每次父 widget 的 build() 返回一个新的 Widget 树时,Flutter:
- 遍历旧的
Element树; - 拿当前
Element对应的旧Widget,与新的 Widget 比较:
-
- 类型一样?
- key 一样?
- 如果认为“可以复用”(same type, same key)→ 调用
Element.update(newWidget); - 然后决定要不要
markNeedsBuild()(也就是标记为 dirty)。
情况 1:StatefulWidget 调用了 setState()
这会显式调用 element.markNeedsBuild(),使得对应 StatefulElement 被重建。
情况 2:新旧 Widget 不是同一个对象(非 identical)
两个对象内存地址不同,也就是说不是同一个widget,哪怕构造参数一样,只要不是 const(没有被折叠成同一对象),Flutter 会:
- 调用
update(); - 默认将
Element标记为 dirty; - 下一帧会调用 build()。
情况 3:新旧 Widget 类型不同或 key 不同
Flutter 会销毁旧 Element,新建新节点,自然就 dirty。
情况 4:祖先 Widget 返回了新的结构,Flutter 无法复用原位置的 Element
比如:
children: [
MyWidget(), // 第 1 次是 A
AnotherWidget(),
]
下一帧变成:
children: [
AnotherWidget(), // 位置调换了
MyWidget(), // 原来 A,现在在另一个位置
]
没有加 Key 时,Flutter 无法判断它是不是原来的 MyWidget,就会认为是全新 widget → 重建。
情况 5:InheritedWidget 通知依赖者更新
如果某个 InheritedWidget 调用了 notifyClients()(通过 updateShouldNotify 返回 true),那么所有 dependOnInheritedWidgetOfExactType 订阅它的 Element 会:
- 自动
markNeedsBuild(); - 下一帧重建。
情况 6:Localizations、Theme、MediaQuery 等系统级上下文发生变化
依赖它们的 widget 会被 Flutter 自动标记 dirty 重建。
总结表格:Element 何时被 markNeedsBuild
| 情况 | 会被标记 dirty 吗? | 说明 |
|---|---|---|
新旧 widget 是同一个对象(identical) | ❌ 否 | const 或缓存复用 |
| 新旧 widget 是不同实例(但类型一样) | ✅ 是 | 默认会调用 update() |
setState()调用 | ✅ 是 | 显式调用 markNeedsBuild |
| InheritedWidget 通知依赖者 | ✅ 是 | 内部通过 dependents 刷新 |
| 新旧 widget 类型不同 | ✅ 是 | 不能复用 Element,必须重建 |
| 新旧 widget key 不同 | ✅ 是 | Flutter 认为是不同组件 |
| 父结构变化导致 Element 无法重用 | ✅ 是 | 如 List 中结构变化,未加 key |
如何减少rebuild?
尽可能使用const widget,也就是常量构造函数初始化的widget
class C extends StatelessWidget {
@override
Widget build(BuildContext context) {
print('C build');
return Text('C');
}
}
class B extends StatelessWidget {
const B();
@override
Widget build(BuildContext context) {
print('B build');
return C(); // 非 const
}
}
class A extends StatefulWidget {
@override
State<A> createState() => _AState();
}
class _AState extends State<A> {
int count = 0;
@override
Widget build(BuildContext context) {
print('A build');
return Column(
children: [
ElevatedButton(
onPressed: () => setState(() => count++),
child: Text('count: $count'),
),
const B(), // const B
],
);
}
}
当点击按钮的时候B和C不会rebuild。我看一下过程:
第一次构建:
A.build()执行;const B()是编译时常量;- 所以 Flutter 构造一个
StatelessElement对应的const B; - 然后 Flutter 调用
B.build(),产生C(); - 接着构造了
C的 Element(比如 StatelessElement); - 调用
C.build()→ 打印'C build'。
以后点击按钮触发 A.setState():
- Flutter 再次调用
A.build(); - 又写的是
const B(),这个Widget实例是编译时的 singleton(identical); - Flutter 拿到
const B的旧 Element; - 发现
widget是identical的,直接复用,不调用B.build(); - 而
C()是 B 上次 build 出来的 widget,所以它的 Element 也直接复用; - 所以
C.build()也不会被调用!
让B C rebuild
- 将36行const B(), 修改为B()
- 每次点击按钮B C都会rebuild,因为new B 与老的B不是同一个对象,所以B被标记为dirty,进而C也是一样
减少setState
待续........