阅读 4817

【- Flutter 性能 -】都 2021 年了,你的动画还在用 setState ?

1.前置知识

对于每个 UI 帧来说,主要依次执行 AnimateBuildLayoutCompositing bitsPaintCompositing。每当界面发生变化时,都是一帧触发会更新的结果。如下每两格代表一帧的UI 时间(左)和 Raster 时间(右)。 当左侧很高时,说明你的界面写的有问题。看下面的两个 UI 帧, 可以看出 Build 占了很大部分,就说明 UI 可能存在某些低效率情况。

image-20201216082750492

image-20201216083822903


你可以向下看整个 Build 遍历的深度,如果树过深表示可能存在问题。这时应该看一下,是否对不必要的部分进行了更新。

33333


但是要注意,对于全局主题、文字等更新,必然会从顶节点进行遍历,这是无法避免的,虽然会让产生一定延迟,但这些都是视觉不敏感操作,操作次数也不是非常频繁。但会动画而言就不同了,掉几帧就会感觉卡卡的,不流畅,另一方面,动画会持续一段时间进行不断渲染,所以要特别注意性能问题。另外不要在 debug 模式看性能不要在 debug 模式看性能不要在 debug 模式看性能!用 profile 模式。

image-20201216092358356


2. 反面教材!!!

动画如下,中间的圆形渐变扩大动画,上下的方块不动。

44444

程序入口

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
        title: 'Flutter Demo',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: HomePage());
  }
}
复制代码

_HomePageState 混入 SingleTickerProviderStateMixin,创建动画器 controller,监听动画器,每次触发时调用 _HomePageStatesetState 方法,来使 _HomePageState 中持有的 Element 进行更新。点击中间时进行动画触发。

class HomePage extends StatefulWidget {
  @override
  _HomePageState createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
  AnimationController controller;

  @override
  void initState() {
    super.initState();
    controller = AnimationController(
        lowerBound: 0.3,
        upperBound: 1.0,
        vsync: this,
        duration: const Duration(milliseconds: 500));
    controller.addListener(() {
      setState(() {});
    });
  }

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    print('---------_HomePageState#build------');
    return Scaffold(
        appBar: AppBar(
          title: Text("动画测试"),
        ),
        body: Column(
          children: [
            Expanded(
              child: Padding( padding: EdgeInsets.only(top: 20),
                child: buildBoxes(),
              ),
            ),
            Expanded(
              child: Center(
                child: buildCenter(),
              ),
            ),
            Expanded(
              child: Padding( padding: EdgeInsets.only(bottom: 20),
                child: buildBoxes(),
              ),
            ),
          ],
        ));
  }

  Widget buildCenter() => GestureDetector(
                onTap: () {
                  controller.forward(from: 0.3);
                },
                child: Transform.scale(
                  scale: controller.value,
                  child: Opacity(opacity: controller.value, child: Shower()),
                ),
              );

  Widget buildBoxes() => Wrap(
        spacing: 20,
        runSpacing: 20,
        children: List.generate( 24,
            (index) => Container(
                  alignment: Alignment.center,
                  width: 40,
                  height: 40,
                  color: Colors.orange,
                  child: Text('$index',style: TextStyle(color: Colors.white),),
                )),
      );
}
复制代码

为了方便测试,这里将中间组件抽离成 Shower。用 StatefulWidget 方便测试动画执行中 _ShowerState 回调函数的情况。

class Shower extends StatefulWidget {
  @override
  _ShowerState createState() => _ShowerState();
}

class _ShowerState extends State<Shower> {
  @override
  void initState() {
    super.initState();
    print('-----Shower#initState----------');
  }

  @override
  Widget build(BuildContext context) {
    print('-----Shower#build----------');
    return Container(
      width: 150,
      height: 150,
      alignment: Alignment.center,
      decoration: BoxDecoration(color: Colors.orange, shape: BoxShape.circle),
      child: Column(
        mainAxisAlignment: MainAxisAlignment.spaceEvenly,
        children: [
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
            children: [
              Container(
                height: 30,
                width: 30,
                decoration:
                    BoxDecoration(color: Colors.white, shape: BoxShape.circle),
              ),
              Container(
                height: 30,
                width: 30,
                decoration:
                    BoxDecoration(color: Colors.white, shape: BoxShape.circle),
              )
            ],
          ),
          Text(
            'Toly',
            style: TextStyle(fontSize: 40, color: Colors.white),
          ),
        ],
      ),
    );
  }
}
复制代码

然后会发现,_HomePageState#buildShower#build 会不断触发。其根本原因是在较高的层级进行了 setState ,导致其下树被遍历,在这种情况下执行动画,是不可取的。我们需要做的是降低更新元素节点层级。Flutter 为我们提供了 AnimatedBuilder

image-20201216091036721

33333


3. 正面面教材 AnimatedBuilder

需要做的改变: 1、移除监听动画器 2、使用 AnimatedBuilder

@override
void initState() {
  super.initState();
  controller = AnimationController(
      vsync: this,
      lowerBound: 0.3,
      upperBound: 1.0,
      duration: const Duration(milliseconds: 500)); // 1、移除监听动画器
}

Widget buildCenter() => GestureDetector(
  onTap: () {
    controller.forward(from: 0);
  },
  child: AnimatedBuilder( //  2、使用 AnimatedBuilder
      animation: controller,
      builder: (ctx, child) {
        return Transform.scale(
          scale: controller.value,
          child: Opacity(opacity: controller.value, child: child),
        );
      },
      child: Shower()),
);
复制代码

仅此而已,让我们看一下效果,动画执行正常

44444

控制台什么都没有,是很过分呢?这不是啪啪啪打我 setState 的脸吗?

从下面的 UI 帧中 可以看出,同样的情景,使用 AnimatedBuilder 进行动画可以很有效地使 Build 过程缩短。

image-20201216092703510

image-20201216092810767


4.AnimatedBuilder 源码解析

首先,AnimatedBuilder 继承自 AnimatedWidget,成员有构造器 builder 和子组件 child,对象创建时还需要 Listenable 对象 animation

class AnimatedBuilder extends AnimatedWidget {
  const AnimatedBuilder({
    Key key,
    @required Listenable animation,
    @required this.builder,
    this.child,
  }) : assert(animation != null),
       assert(builder != null),
       super(key: key, listenable: animation);

  final TransitionBuilder builder;
  final Widget child;

  @override
  Widget build(BuildContext context) {
    return builder(context, child);
  }
}

typedef TransitionBuilder = Widget Function(BuildContext context, Widget child);
复制代码

AnimatedBuilder 很简单,使用核心应该都在 AnimatedWidget 中。可以看出 AnimatedWidget 是一个 StatefulWidget 有更改状态的需要。

abstract class AnimatedWidget extends StatefulWidget {
  const AnimatedWidget({
    Key key,
    @required this.listenable,
  }) : assert(listenable != null),
       super(key: key);

  final Listenable listenable;

  @protected
  Widget build(BuildContext context);

  @override
  _AnimatedState createState() => _AnimatedState();
}
复制代码

_AnimatedState 中处理也非常简单,监听传入的 listenable,执行 _handleChange, 而 _handleChange 执行的是.....,没错:你大爷终究还是你大爷。更新还是要靠 setState。但比起上面的那个setState ,这里的 setState 的影响就小很多。

class _AnimatedState extends State<AnimatedWidget> {
  @override
  void initState() {
    super.initState();
    widget.listenable.addListener(_handleChange);
  }

  @override
  void didUpdateWidget(AnimatedWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.listenable != oldWidget.listenable) {
      oldWidget.listenable.removeListener(_handleChange);
      widget.listenable.addListener(_handleChange);
    }
  }

  @override
  void dispose() {
    widget.listenable.removeListener(_handleChange);
    super.dispose();
  }

  void _handleChange() {
    setState(() {
      // The listenable's state is our build state, and it changed already.
    });
  }

  @override
  Widget build(BuildContext context) => widget.build(context);
}
复制代码

当执行 build 时,执行的是widget.build(context) ,也就是将当前的上下文回调给widget.build方法,而widget.build 方法执行的是 : builder (context, child) 也就是我们写的那个 builder (下图),可以看出回调的这个 child 仍是传入的 child,这样不会构建新的 Shower 组件,也不会触发 Shower 组件对应 State 的 build 方法,一切动画需要的都在 builder 方法中进行,刷新的东西也被 AnimatedBuilder 包在了局部。就这样,岁月静好,波澜不惊

@override
Widget build(BuildContext context) {
  return builder(context, child);
}
复制代码

image-20201216100725103


这样来看,AnimatedBuilder 似乎也没有什么神秘的,了解了这些,再去看 Flutter 框架中的封装的各种动画组件,你就会豁然开朗,这便是知一而通百。总结一下,并不是说 setState 不好,而是用的时机对不对。AnimatedBuilder 本质上也是使用 setState 进行触发更新的,所以看待问题不要片面和激进。对于应界面 UI 来说,我们需要关注的是如何将 Build 过程的消耗降到最低,特别是对于动画、滑动这样会持续跟新渲染的场景。

@张风捷特烈 2020.12.16 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~