MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

基于 Flutter 的动画优化减少重绘提升流畅度

2022-09-282.5k 阅读

Flutter 动画基础与重绘原理

Flutter 动画系统概述

Flutter 提供了一套丰富且灵活的动画系统,允许开发者创建各种复杂而生动的动画效果。其核心类包括 AnimationAnimationControllerCurvedAnimation 等。

Animation 是一个抽象类,它表示动画的当前值。AnimationControllerAnimation 的一个具体子类,它控制动画的播放、暂停、反向播放等操作,并且可以设置动画的时长、起始值和结束值。CurvedAnimation 则用于定义动画的曲线,比如线性变化、加速减速等。

例如,以下代码创建了一个简单的 AnimationController

AnimationController _controller;
@override
void initState() {
  super.initState();
  _controller = AnimationController(
    vsync: this,
    duration: const Duration(seconds: 2),
  );
}
@override
void dispose() {
  _controller.dispose();
  super.dispose();
}

这里通过 AnimationController 创建了一个时长为 2 秒的动画控制器,vsync 参数通常设置为 this,它用于确保动画在屏幕刷新时同步更新,避免动画卡顿。

重绘机制剖析

在 Flutter 中,重绘(repaint)是指当 widget 的状态或数据发生变化时,Flutter 框架会重新构建(rebuild)该 widget 及其子树,并将其绘制到屏幕上。

每次重绘都会消耗一定的资源,过多的重绘会导致性能下降,影响应用的流畅度。Flutter 的渲染流程主要包括以下几个步骤:

  1. 构建阶段(Build Phase):当 widget 的状态发生变化时,Flutter 会调用 build 方法重新构建 widget 树。在这个过程中,Flutter 会根据 widget 的新状态计算出需要绘制的内容。
  2. 布局阶段(Layout Phase):确定每个 widget 在屏幕上的位置和大小。Flutter 会从根 widget 开始,递归地调用每个 widget 的 layout 方法,以确定其子 widget 的位置和大小。
  3. 绘制阶段(Paint Phase):将构建和布局阶段确定的内容绘制到屏幕上。Flutter 使用 Skia 图形库进行绘制,它会根据 widget 的属性和状态,将其绘制为位图或矢量图形。

如果在动画过程中频繁触发重绘,例如每次动画值变化都导致整个 widget 树重新构建,就会造成不必要的性能开销。例如,当一个 AnimatedContainerwidth 属性随着动画变化时,如果没有进行适当的优化,每次 width 值改变都可能导致该 AnimatedContainer 及其父 widget 重新构建和重绘。

动画中导致重绘的常见因素

不必要的 State 变化

在 Flutter 中,当 StatefulWidgetState 发生变化时,会触发 build 方法,从而导致重绘。在动画场景中,如果不合理地管理 State,就会引发不必要的重绘。

例如,假设有一个简单的动画,通过 AnimatedPositioned 来移动一个 Container

class UnoptimizedAnimatedWidget extends StatefulWidget {
  @override
  _UnoptimizedAnimatedWidgetState createState() =>
      _UnoptimizedAnimatedWidgetState();
}
class _UnoptimizedAnimatedWidgetState extends State<UnoptimizedAnimatedWidget>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );
    _controller.forward();
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return AnimatedPositioned(
      duration: const Duration(seconds: 2),
      top: _controller.value * 200,
      left: 0,
      child: Container(
        width: 100,
        height: 100,
        color: Colors.blue,
      ),
    );
  }
}

在这个例子中,AnimatedPositioned 根据 _controller.value 来改变 top 的值。虽然看起来很简单,但如果在 _UnoptimizedAnimatedWidgetState 中添加一些与动画无关的 State 变量,并在其他地方修改这些变量,就会导致 build 方法被调用,从而触发不必要的重绘。

不当的 Widget 使用

某些 Widget 的特性可能会在动画过程中导致过多的重绘。例如,InkWell 是一个用于响应用户点击的 Widget,但如果在动画过程中频繁改变 InkWell 的子 Widget,可能会导致不必要的重绘。

class InkWellWithAnimatedChild extends StatefulWidget {
  @override
  _InkWellWithAnimatedChildState createState() =>
      _InkWellWithAnimatedChildState();
}
class _InkWellWithAnimatedChildState extends State<InkWellWithAnimatedChild>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );
    _controller.forward();
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () {
        // 处理点击事件
      },
      child: AnimatedContainer(
        duration: const Duration(seconds: 2),
        width: _controller.value * 200,
        height: 100,
        color: Colors.red,
      ),
    );
  }
}

在这个例子中,AnimatedContainer 作为 InkWell 的子 Widget,当 AnimatedContainerwidth 随着动画变化时,InkWell 也会因为子 Widget 的变化而触发一定程度的重绘。如果 InkWell 的布局或绘制逻辑比较复杂,这种重绘可能会带来性能问题。

动画值频繁更新触发的重绘

动画值的频繁更新是导致重绘的一个重要原因。例如,当使用 AnimationControlleraddListener 方法来监听动画值变化,并在回调中更新 widget 的状态时,如果处理不当,就会导致大量重绘。

class AnimationListenerUnoptimized extends StatefulWidget {
  @override
  _AnimationListenerUnoptimizedState createState() =>
      _AnimationListenerUnoptimizedState();
}
class _AnimationListenerUnoptimizedState extends State<AnimationListenerUnoptimized>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  double _value = 0;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );
    _controller.addListener(() {
      setState(() {
        _value = _controller.value;
      });
    });
    _controller.forward();
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Container(
      width: _value * 200,
      height: 100,
      color: Colors.green,
    );
  }
}

在这个例子中,通过 addListener 监听 _controller 的值变化,并在回调中使用 setState 更新 _value,从而导致 build 方法被调用,引发重绘。由于动画在运行过程中 _controller.value 会频繁变化,这种方式会导致大量不必要的重绘。

基于 Flutter 的动画优化策略

合理管理 State

  1. 分离动画相关 State 为了避免不必要的 State 变化导致重绘,应该将动画相关的 State 与其他业务逻辑相关的 State 分离开来。例如,可以创建一个单独的 State 类来管理动画相关的状态。
class AnimationState extends State<AnimationWidget>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );
    _controller.forward();
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
}
class AnimationWidget extends StatefulWidget {
  @override
  AnimationState createState() => AnimationState();
}
class OtherWidget extends StatefulWidget {
  @override
  _OtherWidgetState createState() => _OtherWidgetState();
}
class _OtherWidgetState extends State<OtherWidget> {
  // 其他业务逻辑相关的 State
  bool _isChecked = false;
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        AnimationWidget(),
        Checkbox(
          value: _isChecked,
          onChanged: (value) {
            setState(() {
              _isChecked = value;
            });
          },
        )
      ],
    );
  }
}

在这个例子中,AnimationWidget 有自己独立的 AnimationState,而 OtherWidget 有自己的 _OtherWidgetState。这样,当 _OtherWidgetState 中的 _isChecked 状态改变时,不会影响到 AnimationWidget 的重绘,反之亦然。

  1. 使用 AnimatedBuilder 优化 State 管理 AnimatedBuilder 是一个专门用于处理动画的 widget,它可以在动画值变化时高效地重建其子 widget,而不会影响到整个 widget 树。
class AnimatedBuilderOptimized extends StatefulWidget {
  @override
  _AnimatedBuilderOptimizedState createState() =>
      _AnimatedBuilderOptimizedState();
}
class _AnimatedBuilderOptimizedState extends State<AnimatedBuilderOptimized>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );
    _controller.forward();
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (BuildContext context, Widget child) {
        return Container(
          width: _controller.value * 200,
          height: 100,
          color: Colors.yellow,
        );
      },
    );
  }
}

在这个例子中,AnimatedBuilder 只在 _controller 的值发生变化时重建其 builder 回调中的内容,而不会导致整个 AnimatedBuilderOptimized widget 及其父 widget 树的重绘,从而提高了性能。

优化 Widget 的使用

  1. 避免在动画中频繁改变 InkWell 等 Widget 的子 Widget 如果必须在 InkWell 等 widget 中使用动画,可以考虑将动画部分提取到一个独立的 widget 中,并使用 InkWellonTap 等回调来控制动画的播放、暂停等操作,而不是直接在 InkWell 的子 widget 中进行动画。
class OptimizedInkWellWithAnimation extends StatefulWidget {
  @override
  _OptimizedInkWellWithAnimationState createState() =>
      _OptimizedInkWellWithAnimationState();
}
class _OptimizedInkWellWithAnimationState extends State<OptimizedInkWellWithAnimation>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return InkWell(
      onTap: () {
        if (_controller.isCompleted) {
          _controller.reverse();
        } else {
          _controller.forward();
        }
      },
      child: AnimatedContainerWidget(controller: _controller),
    );
  }
}
class AnimatedContainerWidget extends StatelessWidget {
  final AnimationController controller;
  AnimatedContainerWidget({required this.controller});
  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
      duration: const Duration(seconds: 2),
      width: controller.value * 200,
      height: 100,
      color: Colors.purple,
    );
  }
}

在这个例子中,AnimatedContainerWidget 是一个独立的无状态 widget,它接收 AnimationController 并根据其值进行动画。InkWell 只负责处理点击事件来控制动画的播放方向,这样可以减少不必要的重绘。

  1. 使用 RepaintBoundary 限制重绘范围 RepaintBoundary 可以将其内部的 widget 与外部的 widget 隔离开来,当内部 widget 重绘时,不会影响到外部 widget。在动画场景中,如果动画部分是一个独立的模块,可以使用 RepaintBoundary 来限制重绘范围。
class RepaintBoundaryOptimized extends StatefulWidget {
  @override
  _RepaintBoundaryOptimizedState createState() =>
      _RepaintBoundaryOptimizedState();
}
class _RepaintBoundaryOptimizedState extends State<RepaintBoundaryOptimized>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );
    _controller.forward();
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        RepaintBoundary(
          child: AnimatedContainer(
            duration: const Duration(seconds: 2),
            width: _controller.value * 200,
            height: 100,
            color: Colors.orange,
          ),
        ),
        // 其他与动画无关的 widget
        Text('This is other content')
      ],
    );
  }
}

在这个例子中,RepaintBoundaryAnimatedContainer 包裹起来,当 AnimatedContainer 因为动画值变化而重绘时,不会导致 Text 等其他外部 widget 重绘,从而减少了重绘的范围和性能开销。

优化动画值更新机制

  1. 使用 AnimatedWidget 替代手动监听动画值变化 AnimatedWidget 是一个抽象类,它会自动监听 Animation 的变化,并在动画值变化时重建自身。通过继承 AnimatedWidget,可以更高效地处理动画值更新。
class CustomAnimatedWidget extends AnimatedWidget {
  CustomAnimatedWidget({required Animation<double> animation})
      : super(listenable: animation);
  @override
  Widget build(BuildContext context) {
    final Animation<double> animation = listenable as Animation<double>;
    return Container(
      width: animation.value * 200,
      height: 100,
      color: Colors.cyan,
    );
  }
}
class AnimatedWidgetUsage extends StatefulWidget {
  @override
  _AnimatedWidgetUsageState createState() => _AnimatedWidgetUsageState();
}
class _AnimatedWidgetUsageState extends State<AnimatedWidgetUsage>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );
    _controller.forward();
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return CustomAnimatedWidget(animation: _controller);
  }
}

在这个例子中,CustomAnimatedWidget 继承自 AnimatedWidget,它自动监听 _controller 的变化,并在 build 方法中根据动画值构建 widget。相比手动使用 addListenersetState,这种方式更加高效,减少了不必要的重绘。

  1. 使用 Tween 优化动画值的计算 Tween 用于定义动画值的范围和变化方式。通过合理使用 Tween,可以更精确地控制动画值的更新,避免不必要的重绘。
class TweenOptimized extends StatefulWidget {
  @override
  _TweenOptimizedState createState() => _TweenOptimizedState();
}
class _TweenOptimizedState extends State<TweenOptimized>
    with SingleTickerProviderStateMixin {
  AnimationController _controller;
  Animation<double> _animation;
  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );
    _animation = Tween<double>(begin: 0, end: 200).animate(_controller);
    _controller.forward();
  }
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }
  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _animation,
      builder: (BuildContext context, Widget child) {
        return Container(
          width: _animation.value,
          height: 100,
          color: Colors.brown,
        );
      },
    );
  }
}

在这个例子中,Tween<double>(begin: 0, end: 200) 定义了动画值从 0 到 200 的变化范围。通过 animate 方法将 Tween 应用到 AnimationController 上,使得 _animation 能够更精确地控制动画值的变化,避免了一些因为不合理的动画值计算而导致的不必要重绘。

性能监测与验证优化效果

使用 Flutter DevTools 监测重绘情况

Flutter DevTools 是一套用于调试和性能分析的工具集。其中的性能面板可以帮助开发者监测应用的重绘情况。

  1. 打开 Flutter DevTools:在终端中运行 flutter pub global run devtools,然后在浏览器中打开 DevTools 界面。
  2. 连接到应用:选择正在运行的 Flutter 应用实例。
  3. 监测重绘:在性能面板中,可以查看帧率图表以及重绘事件的相关信息。如果重绘频率过高,帧率图表会显示出明显的波动,同时可以通过查看重绘事件的详细信息来定位导致重绘的 widget。

例如,在优化之前,可能会看到帧率图表在动画运行过程中出现较大的波动,重绘事件频繁发生。而在应用上述优化策略后,帧率图表会变得更加平稳,重绘事件的频率会明显降低。

对比优化前后的性能指标

  1. 帧率对比:通过 Flutter DevTools 的性能面板,可以记录优化前后应用在动画过程中的平均帧率。例如,优化前平均帧率可能只有 30fps,而优化后可能提升到 60fps 左右,这表明优化有效地减少了重绘,提升了流畅度。
  2. CPU 和内存占用对比:同样在性能面板中,可以监测优化前后应用的 CPU 和内存占用情况。优化后,由于减少了不必要的重绘,CPU 和内存的占用通常会有所降低,进一步证明优化策略的有效性。

实际场景测试验证

除了使用工具进行性能监测,还需要在实际场景中进行测试验证。例如,在不同性能的设备上运行应用,观察动画的流畅度。在低端设备上,优化后的应用应该能够更稳定地运行动画,不会出现明显的卡顿现象。

同时,模拟用户的实际操作,如快速点击、滑动等,结合动画效果进行测试,确保优化后的应用在各种实际场景下都能保持良好的性能和流畅度。通过实际场景测试,可以更全面地验证优化策略是否真正提升了应用的用户体验。