在日常开发中,我们时常会碰到要对渲染性能进行优化的需求,那么这个时候就不得不提到一个常用的widget------RepaintBoundary。
在 Flutter 中,默认情况下,Flutter 的渲染树在子组件发生变化时,会逐级向上传播 markNeedsPaint,最终可能导致整棵树或较大区域被重绘。我们通过阅读官方文档可知,RepaintBoundary会为其子组件创建一个独立的display list(有关display list的概念,我在之前的文章《为什么要选择Impeller?》中有比较详细的解释,想要了解的小伙伴可以点击查看),并拥有独立的Layer,如果这个子树的重绘频率与周围组件不同,比如它保持静止而周围频繁变化,或相反,则使用 RepaintBoundary 将其隔离,有助于避免无谓的重绘。
关于原理部分我大概翻译了一下官方文档的原文:
当某个 RenderObject 被标记为需要绘制(通过 RenderObject.markNeedsPaint)时,Flutter 会向上查找最近的具有 RenderObject.isRepaintBoundary 为 true 的祖先 RenderObject(直到可能的根节点),并请求该节点进行重绘。该最近祖先的 RenderObject.paint 方法将会导致其所有的子 RenderObject 在同一个 Layer 中进行重绘。
因此,RepaintBoundary 在向上传播 markNeedsPaint 标记以及在通过 PaintingContext.paintChild 向下遍历渲染树时,都会被用来将重绘范围限制在发生视觉变化的渲染子树中,从而提升性能。这么做的原因在于,RepaintBoundary 所创建的 RenderObject 始终拥有一个独立的 Layer,从而使祖先渲染对象与后代渲染对象之间实现了解耦。
RepaintBoundary 还有一个额外的副作用:如果其内部的渲染子树足够复杂且在动画过程中保持静态,同时其外部频繁变化,它可能会提示引擎进行进一步优化动画性能。在这种情况下,引擎可能会选择一次性地将该子树栅格化并缓存像素值,以便在未来 GPU 重新渲染时提升速度。
上面的原理部分稍显抽象,下面我们通过简单的示例代码来验证这些结论是否成立,并观察 RepaintBoundary 在实际中的效果。
核心代码如下:
less
return Scaffold(
appBar: AppBar(title: const Text("RepaintBoundary 示例")),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text("父计数器: $counter", style: const TextStyle(fontSize: 24)),
const SizedBox(height: 20),
const StaticCircleWidget(),
// const RepaintBoundary(child: StaticCircleWidget()),
],
),
),
);
其中StaticCircleWidget的实现:
scala
class StaticCircleWidget extends StatelessWidget {
const StaticCircleWidget({super.key});
@override
Widget build(BuildContext context) {
print("StaticCircleWidget rebuilt");
return CustomPaint(size: const Size(120, 120), painter: _CirclePainter());
}
}
class _CirclePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
print("==> painter paint called");
final center = size.center(Offset.zero);
canvas.drawCircle(center, 50, Paint()..color = Colors.deepPurpleAccent);
}
@override
bool shouldRepaint(_) {
return false;
}
}
我通过设置一个timer来不断的更改计数器的数字,然后分别对照是否被RepaintBoundary来进行测试,之后观察控制台的输出。下面是我观察到的现象:
首先是没有使用RepaintBoundary包裹:
接下来是使用RepaintBoundary包裹:

通过控制台的输出我们可以得知flutter官方文档中所说的祖先渲染对象与后代渲染对象之间实现了解耦,具体是什么意思。真正去使用RepaintBoundary会发生什么现象。
被RepaintBoundary包裹的子组件只有在以下几种情况才会触发重绘:
-
该子组件对应的 Widget 被重新构建(rebuild) ,并导致其 RenderObject 内容发生变化;
- 例如调用 setState() 导致该子树重新构建;
- 如果 shouldRepaint 或 shouldRebuildSemantics 返回 true,也会触发重绘;
-
该子树内部某个 RenderObject 显式调用了 markNeedsPaint() ;
- 比如自定义 RenderBox 中你手动调用该方法;
-
绑定的 AnimationController、Ticker 等机制触发更新(通常内部会隐式调用 markNeedsPaint()) ;
- 比如 AnimatedBuilder、TweenAnimationBuilder;
. 父组件重建,但该 RepaintBoundary 本身或其 RenderObject 发生布局或属性变化,也可能触发内部重绘(不过相比未包裹的情况,大大减少了不必要的重绘)。
总结一下:被 RepaintBoundary 包裹的子组件要重绘,必须自身被显式标记 dirty(如 rebuild、markNeedsPaint()、setState())才行,否则外部的变动不会传导进去。在性能敏感的 Flutter 应用中,合理地使用 RepaintBoundary 可以显著降低无效的重绘区域,但也要避免滥用,过多的 Layer 创建反而可能带来性能开销。