Flutter 渲染管道:从 build 到上屏的完整流程
渲染管道概览
Flutter 不依赖平台原生控件,而是自己从头绘制每一帧。这个过程形成一条「渲染管道」:
Vsync 信号
↓
1. Build(构建 Widget 树)
↓
2. Layout(约束传递,计算尺寸)
↓
3. Paint(生成绘制指令)
↓
4. Composite(合成图层)
↓
5. Rasterize(光栅化,GPU 渲染)
↓
上屏显示
理解这条管道,是优化 Flutter 性能的基础。
Vsync:节奏的起点
Vsync(垂直同步)是显示器硬件发出的「开始新帧」信号。60Hz 屏幕每 16.67ms 一次,120Hz 每 8.33ms 一次。
Flutter 的 Scheduler 注册了一个 Vsync 回调。每次收到信号,就启动新一帧的渲染管道。
// Flutter 内部(简化)
class SchedulerBinding {
void _handleBeginFrame(Duration timeStamp) {
// 通知所有注册了 transientCallbacks 的组件(如 AnimationController)
_invokeTransientCallbacks(timeStamp);
}
void _handleDrawFrame() {
// 执行 persistent callbacks → 触发 build/layout/paint
_invokeCallbacks(_persistentCallbacks);
// 执行 post-frame callbacks
_invokeCallbacks(_postFrameCallbacks);
}
}
第一阶段:Build
BuildOwner 遍历所有标记为 dirty 的 Element,调用它们的 rebuild:
// BuildOwner.buildScope(简化)
void buildScope(Element context, [VoidCallback? callback]) {
// 按深度排序 dirty elements(父先于子)
_dirtyElements.sort(Element._sort);
for (final element in _dirtyElements) {
element.rebuild();
}
}
// StatefulElement.rebuild → State.build
Widget build() => state.build(this);
关键优化:只有 dirty 的 Element 才会 rebuild,干净的 Element 直接跳过。这就是「精准 rebuild」的底层实现。
第二阶段:Layout
Layout 阶段从根 RenderObject 开始,递归完成。每个 RenderObject 的 performLayout 方法负责:
- 把约束(Constraints)传给子节点
- 收集子节点报回的 Size
- 决定子节点的 Offset(相对位置)
- 计算并返回自己的 Size
// 根 Render 节点调用
root.layout(BoxConstraints.tight(screenSize));
// 内部递归(以 RenderFlex 为例,即 Row/Column 的底层)
class RenderFlex extends RenderBox {
@override
void performLayout() {
// 1. 计算非 Flexible 子节点的尺寸
for (final child in children.where((c) => !c.isFlexible)) {
child.layout(constraints);
mainAxisExtent += child.size.main;
}
// 2. 计算 Flex 子节点的可用空间
final freeSpace = mainAxisMax - mainAxisExtent;
// 3. 分配剩余空间给 Flexible 子节点
for (final child in children.where((c) => c.isFlexible)) {
final flex = child.flexFactor;
child.layout(constraints.copyWith(main: freeSpace * flex / totalFlex));
}
// 4. 计算子节点 Offset,设置本节点 Size
size = computeSize();
_placeChildren();
}
}
Flutter 为什么布局是 O(N)?
每个节点的 performLayout 最多被调用一次(除非节点主动触发 relayout boundary 的跨越)。父节点不会重复 measure 子节点。
相比之下,Web 的 CSS 布局(如 Flexbox)可能触发多次回流。
第三阶段:Paint
Layout 完成后,每个 RenderObject 调用 paint(PaintingContext, Offset),把绘制指令写入 Canvas:
class RenderParagraph extends RenderBox {
@override
void paint(PaintingContext context, Offset offset) {
// 调用 TextPainter 把文字绘制指令写入 canvas
_textPainter.paint(context.canvas, offset);
}
}
图层(Layer)
不是所有绘制都在同一个 Canvas。某些 RenderObject 会请求一个独立的 Layer(如 RepaintBoundary),它们的绘制结果被缓存,下次帧如果这个 Layer 没有变化,直接复用缓存,不重绘。
class RenderRepaintBoundary extends RenderProxyBox {
@override
bool get isRepaintBoundary => true;
// 标记为 repaint boundary 的节点有独立的 OffsetLayer
// 不受父节点的影响,仅在自身内容变化时重绘
}
这就是为什么在频繁动画的区域包裹 RepaintBoundary 能提升性能:动画只污染自己的图层,不传播给整个树。
第四阶段:Compositing
所有 Layer 组成一棵 Layer Tree。合成阶段把这棵树「拍平」成一组 Scene,发送给 Flutter Engine。
Layer Tree:
TransformLayer(根)
├── OffsetLayer(正常内容)
├── OffsetLayer(RepaintBoundary,缓存)
│ └── PictureLayer(已缓存的绘制结果)
└── OpacityLayer(透明度叠加)
OpacityLayer、ClipRectLayer、TransformLayer 是「合成层操作」,由 GPU 硬件加速完成,不需要 CPU 重绘。这就是为什么 Opacity widget 改变 opacity 值不会触发 paint,只会触发合成。
第五阶段:Rasterize
最终的 Scene 传给 Skia(或 Impeller),在 GPU 上光栅化为像素。
Skia vs Impeller
| Skia | Impeller | |
|---|---|---|
| 着色器编译 | 运行时 JIT 编译(导致第一次渲染卡顿) | 提前编译(Pre-compiled,无 jank) |
| iOS 状态 | 已弃用 | 默认启用(Flutter 3.10+) |
| Android 状态 | 仍为默认 | 实验性可选(逐步迁移) |
| 优势 | 成熟稳定 | 消除着色器 jank |
Impeller 的核心改进是消除着色器编译引发的首帧卡顿(Shader Compilation Jank)——这是 Skia 时代 Flutter 在 iOS 上最痛的问题。
帧调度与丢帧
整个管道必须在 Vsync 间隔内完成(16.67ms at 60fps)。如果超时:
正常帧:
[vsync]──[build]─[layout]─[paint]─[composite]──[vsync]
↑————————————————————————16.67ms————↑
GPU渲染
丢帧(Jank):
[vsync]────────────[build]──────────────[layout]──...
↑————————————————————————16.67ms————↑
超时,这帧不上屏,显示器显示上一帧 → 视觉卡顿
用 Flutter DevTools 的 Timeline 可以看到每帧各阶段的耗时,找出超时的瓶颈。
实践:如何减少渲染开销
理解管道后,优化策略一目了然:
// 减少 Build 开销:const + 状态下沉
const Widget heavyPart = HeavyWidget(); // 不参与 rebuild
// 减少 Layout 开销:尽量明确尺寸,避免 unbounded constraints
SizedBox(
width: 200,
height: 100,
child: SomeWidget(), // 约束明确,无需试探性 layout
)
// 减少 Paint 开销:RepaintBoundary 隔离高频更新区域
RepaintBoundary(
child: AnimatedWidget(animation: _animation),
)
// 不要在 paint 阶段分配大量对象(GC 压力)
// ❌
class MyPainter extends CustomPainter {
void paint(Canvas canvas, Size size) {
final paint = Paint()..color = Colors.red; // 每帧 new Paint
}
}
// ✅
class MyPainter extends CustomPainter {
final _paint = Paint()..color = Colors.red; // 复用
void paint(Canvas canvas, Size size) {
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), _paint);
}
}