flutter UI更新机制

237 阅读7分钟

核心概念:三棵树

Flutter 的世界里,实际上有三棵并行的树,它们各司其职,共同构成了从代码到像素的完整流程。

  1. Widget 树 (The Widget Tree)

    • 是什么:就是你在代码中用 build 方法构建的树。它是一个配置蓝图
    • 特点
      • 不可变 (Immutable):一旦创建,Widget 对象本身不能被修改。build 方法每次执行都会返回一个全新的 Widget 树(或其子树)。
      • 轻量级:Widget 只是描述信息,创建和销毁它们的成本非常低。
    • 例子Container(color: Colors.blue, child: Text('Hello'))
  2. Element 树 (The Element Tree)

    • 是什么:这是 Flutter 框架在内部维护的树,是 Widget 树的实例化。每一个 Widget 在树上都有一个对应的 Element。
    • 特点
      • 可变 (Mutable):Element 的生命周期比 Widget 长得多。它不会在每次 build 时重建,而是会被更新以引用新的 Widget。
      • 中间协调者:Element 是连接 Widget(配置)和 RenderObject(渲染)的桥梁。它管理着 Widget 的生命周期、持有 State 对象,并知道自己在树中的位置。
    • 职责:执行 diffing 算法,决定是更新、移动还是重新创建。
  3. 渲染树 (The RenderObject Tree)

    • 是什么:这是真正负责布局和绘制的树。每个 Element 都有一个对应的 RenderObject(对于那些实际渲染内容的 Widget)。
    • 特点
      • 重量级:创建 RenderObject 的成本非常高,因为它涉及到内存分配、布局计算等复杂操作。Flutter 的整个更新机制,其核心目标就是尽可能少地操作 RenderObject 树
      • 负责具体工作RenderBox(RenderObject 的一个子类)负责处理笛卡尔坐标系下的布局(计算尺寸 size 和位置 offset),而 RenderParagraph 负责绘制文本等。
    • 例子:对于 Container(color: Colors.blue),可能对应一个 RenderDecoratedBox

三棵树的关系: Widget (配置) -> Element (生命周期管理者) -> RenderObject (绘制者)


UI 更新的三大阶段

当 UI 需要更新时(例如,你调用了 setState()),Flutter 会依次执行以下三个阶段。

阶段一:触发更新 (Marking as Dirty)

  1. 起点:通常是用户交互、动画ticker、网络响应等触发了状态改变,最终调用了 StatefulWidgetsetState() 方法。
  2. setState() 的作用:它并不会立即执行 build。它的核心作用是:
    • 执行你传入的回调函数,更新 State 对象的内部数据。
    • 通知 Flutter 框架:“我这个 State 对应的 Element 变脏了 (dirty)”。
  3. 调度帧 (Schedule a Frame):Flutter 的引擎会安排在下一个 VSync 信号(垂直同步信号,通常是 16.6ms 一次,对应 60fps)到来时,进行一次新的帧绘制。所有被标记为 "dirty" 的 Element 都会在这一帧中被处理。

阶段二:重建与比对 (Build and Reconciliation / Diffing)

这是整个更新过程的核心和最精妙的部分。当新的帧开始绘制时,Flutter 会从根部开始,遍历所有被标记为 "dirty" 的 Element。

  1. 调用 build():对于每一个 "dirty" Element,Flutter 会调用其对应的 build() 方法(对于 StatefulWidgetState.build(),对于 StatelessWidgetWidget.build())。这会生成一个新的 Widget 子树。

  2. 比对 (Reconciliation):现在,Element 手里有了一个的 Widget(来自 build 方法)和一个的 Widget(它在上一次更新时持有的引用)。它需要决定如何处理。这个过程被称为“和解”或“比对”。

    比对的规则非常高效,只有一条核心原则:深度优先遍历,逐一比较

    对于一个 Element,它会拿新的 Widget 和它当前引用的旧 Widget 进行比较:

    • 情况 A:可以更新 (Can Update)

      • 条件:如果新旧两个 Widget 的 runtimeType (运行时类型) 和 key 都相同
      • 操作
        1. Element 被保留,不会被销毁。
        2. Element 更新它对 Widget 的引用,指向这个新的 Widget。
        3. Element 对应的 RenderObject被保留。Flutter 只会根据新 Widget 的属性去更新 RenderObject 的属性(例如,Container 的颜色从蓝变红,只会更新 RenderDecoratedBoxdecoration 属性,而不会重新创建它)。
        4. 然后,Element 会递归地对自己所有的子 Element 进行同样的比对过程。
    • 情况 B:无法更新,需要停用和创建 (Deactivate and Create)

      • 条件:如果新旧两个 Widget 的 runtimeTypekey 不相同
      • 操作
        1. 旧的 Element 被认为是“过时”的,它会被停用 (deactivate),并连同其下的整个子树一起,从 Element 树中移除。这些被停用的 Element 会被放入一个临时的“失活列表”中。
        2. Flutter 会根据新的 Widget 创建一个全新的 Element,以及全新的 State(如果是 StatefulWidget)和全新的 RenderObject
        3. 这个新的 Element 被插入到 Element 树的相应位置。
  3. Key 的作用

    • LocalKey:在比对一个列表的子节点时,Flutter 不再是简单地按索引位置比较。它会先检查 Key。如果新 Widget 的 Key 在旧的兄弟 Element 中能找到,Flutter 会复用那个 Element,即使它的位置变了。这可以极大地优化列表重排的性能,避免了不必要的 Element 创建和销毁。
    • GlobalKeyGlobalKey 更强大。当一个带有 GlobalKey 的旧 Element 被停用时,它不会立即被销毁。Flutter 会在整帧的比对过程结束前,检查是否有新的 Widget 使用了同一个 GlobalKey。如果有,Flutter 会将那个被停用的 Element 重新激活 (reactivate) 并“移植”到新的位置,从而实现跨父节点的 Widget 状态保持。如果整帧结束都没有新的 Widget “认领”这个 GlobalKey,那么对应的 Element 才会被最终销毁。

阶段三:布局与绘制 (Layout and Paint)

经过第二阶段,Element 树已经被更新,并且 RenderObject 树也相应地被部分更新(只更新了必要的节点)。现在进入最后的渲染流水线。

  1. 布局 (Layout)

    • Flutter 的渲染引擎会从 RenderObject 树的根节点开始,进行一次深度优先遍历。
    • 这是一个“约束向下传递,尺寸向上传递”的过程:
      • 约束向下 (Constraints go down):父节点告诉子节点:“你可以在这个宽度和高度范围内布局”。
      • 尺寸向上 (Sizes go up):子节点根据约束计算出自己的尺寸,然后告诉父节点:“我决定占用这么大的空间”。
    • 这个过程完成后,RenderObject 树中所有节点的大小(size)都已经确定。
  2. 定位 (Positioning)

    • 在布局之后,父节点会根据子节点的尺寸来确定它们的具体位置(offset)。
  3. 绘制 (Paint)

    • 渲染引擎再次遍历 RenderObject 树。
    • 每个 RenderObject 会根据自己的属性(颜色、形状、文本等)以及计算好的尺寸和位置,在画布 (Canvas) 上进行绘制。
    • Flutter 引入了重绘边界 (Repaint Boundary) 的概念进行优化。如果一个 Widget 子树(例如,一个复杂的自定义动画)被 RepaintBoundary 包裹,并且其内容更新不影响外部布局,那么只有这个边界内的 RenderObject 需要重绘,极大地缩小了绘制范围。
  4. 合成 (Compositing)

    • 绘制操作会生成不同的图层 (Layers)。最后,这些图层会被合成,并发送给 GPU 进行最终的屏幕渲染。

总结

  • 核心目标:尽可能地复用 ElementRenderObject,因为它们是“昂贵”的。
  • 手段:通过 build 方法快速生成“廉价”的 Widget 配置树,然后与现有的 Element 树进行高效的 diffing
  • setState:不是立即重建,而是“标记为脏”并请求新的一帧。
  • Key:是给 Flutter 在 diffing 过程中的一个“提示”,帮助它更智能地识别和复用 Element,从而保留状态和提升性能。

理解了这个机制,你就能明白为什么 Key 如此重要,为什么在 ListView.builder 中不能创建 GlobalKey,以及为什么将 Widget 包裹在不必要的 StatefulWidget 中可能会影响性能。它构成了你优化 Flutter 应用性能的理论基础。