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 创建反而可能带来性能开销。

相关推荐
LawrenceLan14 小时前
Flutter 零基础入门(九):构造函数、命名构造函数与 this 关键字
开发语言·flutter·dart
一豆羹15 小时前
macOS 环境下 ADB 无线调试连接失败、Protocol Fault 及端口占用的深度排查
flutter
行者9615 小时前
OpenHarmony上Flutter粒子效果组件的深度适配与实践
flutter·交互·harmonyos·鸿蒙
行者9618 小时前
Flutter与OpenHarmony深度集成:数据导出组件的实战优化与性能提升
flutter·harmonyos·鸿蒙
小雨下雨的雨18 小时前
Flutter 框架跨平台鸿蒙开发 —— Row & Column 布局之轴线控制艺术
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨18 小时前
Flutter 框架跨平台鸿蒙开发 —— Center 控件之完美居中之道
flutter·ui·华为·harmonyos·鸿蒙
小雨下雨的雨19 小时前
Flutter 框架跨平台鸿蒙开发 —— Icon 控件之图标交互美学
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨19 小时前
Flutter 框架跨平台鸿蒙开发 —— Placeholder 控件之布局雏形美学
flutter·ui·华为·harmonyos·鸿蒙系统
行者9620 小时前
OpenHarmony Flutter弹出菜单组件深度实践:从基础到高级的完整指南
flutter·harmonyos·鸿蒙
前端不太难20 小时前
Flutter / RN / iOS,在长期维护下的性能差异本质
flutter·ios