文章

Flutter 动画性能优化深度解析

Flutter 动画性能:AnimationController、隐式/显式动画与 Impeller 渲染后端。 讲清动画掉帧原因与优化手段,掌握后能答出「复杂动画如何稳定 60fps」。

Flutter 动画性能优化深度解析

一句话概括

Flutter 动画性能的核心就一句话:能跑在合成层就别碰绘制层。Transform.translate 移动 100 个像素,GPU 花 0.1ms;Padding 改变 100 个像素,CPU 跑一遍 layout + paint——效果一样,代价差 20 倍。

核心知识点

1. 三层动画,代价相差一个数量级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 第三层(最便宜):合成层动画 — GPU 直接处理
Transform.translate(
  offset: Offset(dx, 0),  // 只更新 TransformLayer 的 matrix
  child: expensiveWidget,  // 子 Widget 绘制结果被缓存在纹理
);

// 第二层(中等):绘制层动画 — 触发 paint,不触发 layout
DecoratedBox(
  decoration: BoxDecoration(
    color: Color.lerp(Colors.blue, Colors.red, anim.value),  // 重新绘制
  ),
);

// 第一层(最贵):布局层动画 — 触发 layout + paint + build 全链路
Padding(
  padding: EdgeInsets.all(anim.value * 20),  // 每帧重新布局
  child: expensiveWidget,
);

记法:哪种属性变了 → 查 Flutter 文档 “要不要 layout”。位移/旋转/缩放/透明度 → 不 layout → 便宜。尺寸/padding/margin → 要 layout → 贵。

2. AnimationController 的生命周期铁律

1
2
3
4
5
6
7
8
9
10
11
12
class _MyState extends State<MyWidget> with SingleTickerProviderStateMixin {
  late final _controller = AnimationController(
    vsync: this,  // 绑定到当前 State 的生命周期
    duration: const Duration(seconds: 1),
  );

  @override
  void dispose() {
    _controller.dispose();  // 不 dispose = Ticker 永不停 = 内存泄漏 + 帧白耗
    super.dispose();
  }
}

vsync: this 的本质:State 被 dispose 时,Ticker 自动停。如果你传 null,动画不跑;如果你忘记 dispose,页面销毁了动画还在帧帧触发——用户看不到,但 CPU 还在烧。

3. TweenAnimationBuilder:零 Controller 的一次性动画

1
2
3
4
5
6
7
8
// 不需要 StatefulWidget + AnimationController + dispose
TweenAnimationBuilder<double>(
  tween: Tween(begin: 0.0, end: 1.0),
  duration: const Duration(milliseconds: 500),
  curve: Curves.easeInOut,
  builder: (_, value, child) => Opacity(opacity: value, child: child),
  child: const Text('淡入效果'),
);

TweenAnimationBuilder 自动管理 Ticker——Widget 挂载时启动,卸载时停止。duration 或 tween 变了自动重启动画。StatelessWidget 就能用,少写 15 行模板代码。

4. FadeTransition vs Opacity 的底层差别

1
2
3
4
5
// ❌ Opacity:在 GPU 创建离屏缓冲区 → 渲染子 Widget → 乘 alpha → 合成回去
Opacity(opacity: anim.value, child: HeavyWidget())

// ✅ FadeTransition:不创建离屏缓冲区,直接在合成阶段调 alpha
FadeTransition(opacity: anim, child: HeavyWidget())

Opacity 需要 saveLayer() 开一个离屏帧缓冲,相当于把子 Widget 画到一张额外的纹理上,再合成到主画布。FadeTransition 直接修改 Layer 的 alpha 属性——零额外开销。同理:ScaleTransition > Transform.scale 手动调。

5. RepaintBoundary + 动画 = 最佳搭档

1
2
3
4
5
6
7
AnimatedBuilder(
  animation: _controller,
  builder: (_, child) => Transform.rotate(angle: _controller.value * 2 * pi, child: child),
  child: RepaintBoundary(
    child: ComplexChart(),  // 图表只绘制一次,后续帧只更新 Transform
  ),
)

动画只更新 Transform.rotate 的 matrix——合成层属性。ComplexChart() 被 RepaintBoundary 缓存为纹理,后续帧直接复用。一帧 16ms 预算,你用 0.1ms 就搞定。

其实你每天都在用

  • 页面切换的滑动动画 — SlideTransition 包裹路由,GPU 移动纹理,不重建 Widget
  • 按钮的弹性反馈 — TweenAnimationBuilder + Curves.elasticOut,按下缩放到 0.95 弹回
  • 下拉刷新的旋转箭头 — AnimatedBuilder + RotationTransition,每帧只更新角度
  • 骨架屏闪烁 — AnimationController.repeat() + FadeTransition,不碰 Widget 树
  • 列表 Item 的入场动画 — SlideTransition + FadeTransition 叠在 ListView.builder 的 Item 上

常见误解(FAQ)

  • ❌ 误区:「动画卡顿是因为动画太多了」 20 个 FadeTransition 跑得比 1 个 Padding 动画还流畅。问题不是数量,是每个动画跑在哪一层。20 个合成层动画 = GPU 20 次 matrix 乘法(几乎免费)。

  • ❌ 误区:「Impeller 引擎让所有动画都变快了」 Impeller 消除了着色器编译卡顿(首次渲染复杂渐变不再卡),但布局动画的开销不变——Impeller 管 GPU 端,不管 CPU 端。

  • ❌ 误区:「AnimatedContainer 是性能最优的动画方案」 AnimatedContainer 每次变化都创建新的 Tween 并重建内部 Controller。频繁变化的场景下(比如跟随手指拖拽),用 AnimationController + AnimatedBuilder 手动控制更高效。

  • ❌ 误区:「Ticker 回调里直接 setState 就行」 Ticker 回调在 handleBeginFrame 阶段触发,此时还没到 build 阶段。你在回调里 setState 没问题——Flutter 会在同一帧的 build 阶段处理。但不要在 Ticker 回调里做 layout 或 paint(这些阶段还没到)。

一句话总结

动画性能的秘诀不是 “少做动画”,而是 “在正确的层级做动画”——布局层做一次,合成层做一万次。Transform 三件套(位移/旋转/缩放)+ FadeTransition 四件套,覆盖 90% 动画需求,一帧 0.1ms 搞定。

本文由作者按照 CC BY 4.0 进行授权