Flutter 渲染流程与三棵树详解

目录

  • 一、Flutter 渲染架构全览
  • 二、三棵树是什么
  • 三、三棵树的协作流程
  • 四、各层职责与修改时机
  • 五、常见场景:该动哪一层?
  • 六、总结

一、Flutter 渲染架构全览

Flutter 的渲染流程从 Dart 代码到屏幕像素,经过了多个层次的处理:

你写的 Widget
     ↓
Widget Tree(配置描述)
     ↓
Element Tree(生命周期 + 状态管理)
     ↓
RenderObject Tree(布局 + 绘制)
     ↓
Layer Tree(合成)
     ↓
Skia / Impeller(光栅化)
     ↓
GPU → 屏幕

这套架构的核心设计目标是把"描述"和"执行"分离,让 Widget 可以廉价重建,而真正昂贵的布局和绘制操作尽量复用。


二、三棵树是什么

2.1 Widget Tree — 配置树

Widget 是不可变的配置描述,你每次调用 setStatebuild 都会产生全新的 Widget 对象。它极其轻量,创建和销毁的成本几乎可以忽略。

// Widget 只是一个数据类,描述"我想要什么"
Text('Hello', style: TextStyle(fontSize: 16))

Widget 本身不持有状态,也不负责渲染,它只是一份蓝图

2.2 Element Tree — 实例树

Element 是 Widget 在运行时的实例,负责:

  • 持有对应 Widget 和 RenderObject 的引用
  • 管理组件生命周期(mount / unmount / activate)
  • 决定 Widget 更新时是否能复用现有 RenderObject(通过 canUpdate 比较 runtimeTypekey
  • 维护 BuildContextBuildContext 其实就是 Element 本身)
abstract class Element implements BuildContext {
  Widget _widget;
  RenderObject? renderObject;
  // ...
}

Element 树的结构与 Widget 树一一对应,但生命周期更长——Widget 重建时 Element 会尝试复用,而不是销毁重建。

2.3 RenderObject Tree — 渲染树

RenderObject 负责真正的布局(layout)和绘制(paint),是性能敏感的核心。

  • layout():通过父传入的 Constraints 计算自身尺寸,再把约束传给子节点
  • paint():把自己绘制到 PaintingContext 提供的 Canvas
  • 每个 RenderObject 缓存自己的布局结果,只在 markNeedsLayout() / markNeedsPaint() 被调用后才重新执行

常见子类:

类名职责
RenderBox二维盒模型布局,绝大多数 Widget 的基础
RenderSliver滚动视口内的懒加载布局(ListView 用到)
RenderFlexRow / Column 的弹性布局实现
RenderCustomPaint自定义绘制入口

三、三棵树的协作流程

首次渲染

runApp(MyWidget())
  → WidgetsBinding.attachRootWidget
  → Element.mount()          // 创建 Element 树
  → RenderObjectElement 同时创建 RenderObject 并插入渲染树
  → RendererBinding.drawFrame()
      → PipelineOwner.flushLayout()   // 深度优先 layout
      → PipelineOwner.flushPaint()    // 深度优先 paint
      → SceneBuilder 合成 Layer Tree
      → Window.render() → GPU

setState 触发更新

setState(() { ... })
  → Element.markNeedsBuild()
  → SchedulerBinding 在下一帧调用 BuildOwner.buildScope()
  → Element.rebuild() → widget.build()
  → 对每个子 Widget 调用 Element.updateChild()
      ├─ canUpdate == true  → 复用 Element,调用 element.update(newWidget)
      └─ canUpdate == false → 卸载旧 Element,mount 新 Element
  → 若 RenderObject 属性变化 → markNeedsLayout / markNeedsPaint
  → 下一帧重新 layout + paint

关键点:Widget 重建不等于 RenderObject 重建。只要 runtimeTypekey 不变,Element 和 RenderObject 都会被复用,仅更新属性。这是 Flutter 性能的核心保障。


四、各层职责与修改时机

Widget 层

何时动这一层: 绝大多数业务开发都在这里。组合已有 Widget、控制 UI 结构和状态。

// 普通业务 UI:只需要组合 Widget
class MyCard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16),
      child: Text('内容'),
    );
  }
}

注意: Widget 层动不了底层渲染行为,改的只是"配置"。如果你想自定义布局规则或绘制,Widget 层就不够用了。


Element 层

何时动这一层: 需要精细控制组件复用逻辑、或者实现跨 Widget 树的数据透传。直接操作 Element 的场景非常少见,常见案例:

1. 自定义 Key 控制复用

// 不加 Key:列表顺序变化时 State 会错位
// 加 Key:强制 Flutter 按 key 匹配 Element
ListView(
  children: items.map((e) => ItemWidget(key: ValueKey(e.id), item: e)).toList(),
)

2. GlobalKey 跨树访问 State

final key = GlobalKey<FormState>();
Form(key: key, child: ...);
// 在其他地方访问:
key.currentState?.validate();

GlobalKey 的底层实现就是在 Element 树上注册全局索引。

3. 实现 InheritedWidget

InheritedElement 维护了一张依赖表,当数据变化时精确通知依赖它的 Element 重建,而不是整棵树。Provider、Theme、MediaQuery 都基于此。

class MyInheritedWidget extends InheritedWidget {
  final int value;
  // ...
  @override
  bool updateShouldNotify(MyInheritedWidget old) => value != old.value;
}

RenderObject 层

何时动这一层: 需要自定义布局算法绘制逻辑,现有 Widget 无法满足需求时。

1. 自定义绘制 — CustomPainter

class WavePainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    final paint = Paint()..color = Colors.blue;
    // 直接操作 Canvas,画任意图形
    final path = Path();
    path.moveTo(0, size.height * 0.5);
    for (double x = 0; x <= size.width; x++) {
      path.lineTo(x, size.height * 0.5 + sin(x * 0.05) * 20);
    }
    canvas.drawPath(path, paint);
  }

  @override
  bool shouldRepaint(WavePainter old) => false;
}

2. 自定义布局 — CustomMultiChildLayout / MultiChildRenderObjectWidget

当 Row/Column/Stack 都满足不了你的布局需求时,直接继承 RenderBox

class RenderRadialLayout extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, RadialParentData> {
  @override
  void performLayout() {
    // 把子节点排列成圆形
    double angle = 0;
    RenderBox? child = firstChild;
    while (child != null) {
      child.layout(constraints, parentUsesSize: true);
      final parentData = child.parentData as RadialParentData;
      parentData.offset = Offset(
        cos(angle) * radius,
        sin(angle) * radius,
      );
      angle += 2 * pi / childCount;
      child = childAfter(child);
    }
    size = constraints.biggest;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    // 按计算好的 offset 绘制子节点
    RenderBox? child = firstChild;
    while (child != null) {
      final parentData = child.parentData as RadialParentData;
      context.paintChild(child, offset + parentData.offset);
      child = childAfter(child);
    }
  }
}

3. RepaintBoundary — 隔离重绘区域

// 把频繁重绘的子树隔离在独立 Layer,避免污染父节点
RepaintBoundary(
  child: AnimatedWidget(...),
)

底层原理:RenderRepaintBoundary 会在 Layer Tree 中创建新的 OffsetLayer,让子树的 paint 结果缓存在独立纹理上。


五、常见场景:该动哪一层?

场景应该动哪层具体手段
普通 UI 布局、状态管理WidgetStatelessWidget / StatefulWidget
列表顺序变化导致状态错乱Element给 Widget 加 Key
跨组件共享数据ElementInheritedWidget / Provider
跨树直接操作 StateElementGlobalKey
自定义图形、动画、粒子效果RenderObjectCustomPainter
现有布局 Widget 无法满足排列需求RenderObject继承 RenderBox + MultiChildRenderObjectWidget
局部频繁重绘性能优化RenderObjectRepaintBoundary
需要在 layout 阶段拿到子节点尺寸RenderObjectLayoutBuilder 或自定义 RenderBox
实现类似 Sliver 的懒加载布局RenderObject继承 RenderSliver

六、总结

  • Widget 树:轻量、不可变的配置描述,随时重建没有压力,绝大多数业务代码都在这里
  • Element 树:运行时实例,管理生命周期和复用逻辑,通常不直接操作,但 Key 和 InheritedWidget 的工作都在这一层
  • RenderObject 树:真正的布局和绘制,性能敏感,只有在需要自定义渲染行为时才下到这一层

理解三棵树的分工,核心是记住一句话:Widget 描述想要什么,Element 决定怎么复用,RenderObject 负责真正干活。