Widget rebuild 原理及性能优化

111 阅读4分钟

知道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:

  1. 遍历旧的 Element 树;
  2. 拿当前 Element 对应的旧 Widget,与新的 Widget 比较:
    • 类型一样?
    • key 一样?
  1. 如果认为“可以复用”(same type, same key)→ 调用 Element.update(newWidget)
  2. 然后决定要不要 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;
  • 发现 widgetidentical 的,直接复用,不调用 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

待续........