【- Flutter 性能 -】都 2021 年了,你的动画还在用 setState ?
主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black贡献主题:https://github.com/...
主题列表:juejin, github, smartblue, cyanosis, channing-cyan, fancy, hydrogen, condensed-night-purple, greenwillow, v-green, vue-pro, healer-readable, mk-cute, jzman, geek-black
贡献主题:https://github.com/xitu/juejin-markdown-themes
theme: condensed-night-purple
highlight:
1.前置知识
对于每个 UI 帧来说,主要依次执行 Animate
、Build
、Layout
、Compositing bits
、Paint
、Compositing
。每当界面发生变化时,都是一帧触发会更新的结果。如下每两格代表一帧的UI 时间(左)和 Raster 时间(右)。 当左侧很高时,说明你的界面写的有问题。看下面的两个 UI 帧, 可以看出 Build 占了很大部分,就说明 UI 可能存在某些低效率情况。
你可以向下看整个 Build 遍历的深度,如果树过深表示可能存在问题。这时应该看一下,是否对不必要的部分进行了更新。
但是要注意,对于
全局主题、文字
等更新,必然会从顶节点进行遍历,这是无法避免的,虽然会让产生一定延迟,但这些都是视觉不敏感操作
,操作次数也不是非常频繁
。但会动画而言就不同了,掉几帧就会感觉卡卡的,不流畅,另一方面,动画会持续一段时间进行不断渲染,所以要特别注意性能问题。另外不要在 debug 模式看性能
、不要在 debug 模式看性能
、不要在 debug 模式看性能
!用profile
模式。
2. 反面教材!!!
动画如下,中间的圆形渐变扩大动画,上下的方块不动。
程序入口
```dart 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
,监听动画器,每次触发时调用_HomePageState
的setState
方法,来使_HomePageState
中持有的 Element 进行更新。点击中间时进行动画触发。
```dart class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); }
class _HomePageState extends State 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
回调函数的情况。
```dart class Shower extends StatefulWidget { @override _ShowerState createState() => _ShowerState(); }
class _ShowerState extends State { @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#build
和Shower#build
会不断触发。其根本原因是在较高的层级进行了setState
,导致其下树被遍历,在这种情况下执行动画,是不可取的。我们需要做的是降低更新元素节点层级。Flutter 为我们提供了AnimatedBuilder
。
3. 正面面教材 AnimatedBuilder
需要做的改变: 1、移除监听动画器 2、使用
AnimatedBuilder
```dart @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()), ); ```
仅此而已,让我们看一下效果,动画执行正常
控制台什么都没有,是很过分呢?这不是啪啪啪打我
setState
的脸吗?
从下面的 UI 帧中 可以看出,同样的情景,使用
AnimatedBuilder
进行动画可以很有效地使Build
过程缩短。
4.AnimatedBuilder
源码解析
首先,
AnimatedBuilder
继承自AnimatedWidget
,成员有构造器builder
和子组件child
,对象创建时还需要Listenable
对象animation
。
```dart 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
有更改状态的需要。
```dart 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
的影响就小很多。
```dart class AnimatedState extends State { @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
包在了局部。就这样,岁月静好,波澜不惊
。
dart @override Widget build(BuildContext context) { return builder(context, child); }
这样来看,
AnimatedBuilder
似乎也没有什么神秘的,了解了这些,再去看 Flutter 框架中的封装的各种动画组件,你就会豁然开朗,这便是知一而通百
。总结一下,并不是说setState
不好,而是用的时机对不对。AnimatedBuilder
本质上也是使用setState
进行触发更新的,所以看待问题不要片面和激进。对于应界面 UI 来说,我们需要关注的是如何将Build
过程的消耗降到最低,特别是对于动画、滑动这样会持续跟新渲染的场景。
@张风捷特烈 2020.12.16 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)