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

相关推荐
阅文作家助手开发团队_山神35 分钟前
第三章: 解决Android iPad蓝牙键盘联想词UI不跟随光标问题
flutter
阅文作家助手开发团队_山神35 分钟前
第四章:Flutter自定义Engine本地依赖与打包流程
前端·flutter
程序员老刘1 小时前
Flutter 3.35 更新要点解析
flutter·ai编程·客户端
阅文作家助手开发团队_山神3 小时前
第一章: Mac Flutter Engine开发准备工作
前端·flutter
EmmaGuo20155 小时前
flutter3.7.12版本设置TextField的contextMenuBuilder的文字颜色
前端·flutter
鹏多多.7 小时前
flutter-使用device_info_plus获取手机设备信息完整指南
android·前端·flutter·ios·数据分析·前端框架
来来走走12 小时前
Flutter开发 网络请求
android·flutter
SoaringHeart1 天前
Flutter进阶:高内存任务的动态并发执行完美实现
前端·flutter
吴Wu涛涛涛涛涛Tao1 天前
Flutter 实现类似抖音/TikTok 的竖向滑动短视频播放器
android·flutter·ios
猪哥帅过吴彦祖1 天前
Flutter 插件工作原理深度解析:从 Dart 到 Native 的完整调用链路
android·flutter·ios