RepaintBoundary是什么?怎么用?

在日常开发中,我们时常会碰到要对渲染性能进行优化的需求,那么这个时候就不得不提到一个常用的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包裹的子组件只有在以下几种情况才会触发重绘:

  1. 该子组件对应的 Widget 被重新构建(rebuild) ,并导致其 RenderObject 内容发生变化;

    • 例如调用 setState() 导致该子树重新构建;
    • 如果 shouldRepaint 或 shouldRebuildSemantics 返回 true,也会触发重绘;
  2. 该子树内部某个 RenderObject 显式调用了 markNeedsPaint()

    • 比如自定义 RenderBox 中你手动调用该方法;
  3. 绑定的 AnimationController、Ticker 等机制触发更新(通常内部会隐式调用 markNeedsPaint())

    • 比如 AnimatedBuilder、TweenAnimationBuilder;

. 父组件重建,但该 RepaintBoundary 本身或其 RenderObject 发生布局或属性变化,也可能触发内部重绘(不过相比未包裹的情况,大大减少了不必要的重绘)。

总结一下:被 RepaintBoundary 包裹的子组件要重绘,必须自身被显式标记 dirty(如 rebuild、markNeedsPaint()、setState())才行,否则外部的变动不会传导进去。在性能敏感的 Flutter 应用中,合理地使用 RepaintBoundary 可以显著降低无效的重绘区域,但也要避免滥用,过多的 Layer 创建反而可能带来性能开销。

相关推荐
ITfeib4 小时前
Flutter基础
flutter
淹没9 小时前
🚀 告别复杂的HTTP模拟!HttpHook让Dart应用测试变得超简单
android·flutter·dart
吴Wu涛涛涛涛涛Tao14 小时前
Flutter 个人主页实践笔记
flutter
愿天深海14 小时前
Flutter 提取图像主色调 ColorScheme.fromImageProvider
android·前端·flutter
梦想改变生活1 天前
《Flutter篇第一章》基于GetX 和 Binding、Dio 实现的 Flutter UI 架构
flutter·ui·架构
耳東陈2 天前
[重磅发布] Flutter Chen Generator 必备脚本工具
flutter
亿刀2 天前
【学习VPN之路】NET技术
android·flutter
浅蓝色2 天前
Flutter平台判断问题,并适配鸿蒙
flutter