flutter动画性能优化
作者:张疯捷特烈
1.前置知识
对于每个 UI 帧来说,主要依次执行 Animate、Build、Layout、Compositing bits、Paint、Compositing。每当界面发生变化时,都是一帧触发会更新放入结果。如下每两格代表一帧的UI 时间(左)和 Raster 时间(右)。 当左侧很高时,说明你的界面写的有问题。看下面的两个 UI 帧, 可以看出 Build 占了很大部分,就说明 UI 可能存在某些低效率情况。
你可以向下看整个 Build 遍历的深度,如果树过深表示可能存在问题。这时应该看一下,是否对不必要的部分进行了更新。
但是要注意,对于全局主题、文字等更新,必然会从顶节点进行遍历,这是无法避免的,虽然会让产生一定延迟,但这些都是视觉不敏感操作,操作次数也不是非常频繁。但会动画而言就不同了,掉几帧就会感觉卡卡的,不流畅,另一方面,动画会持续一段时间进行不断渲染,所以要特别注意性能问题。另外不要在 debug 模式看性能、不要在 debug 模式看性能、不要在 debug 模式看性能!用 profile 模式。
2. 反面教材!!!
动画如下,中间的圆形渐变扩大动画,上下的方块不动。
程序入口
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 进行更新。点击中间时进行动画触发。
class HomePage extends StatefulWidget {
_HomePageState createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> with SingleTickerProviderStateMixin {
AnimationController controller;
void initState() {
super.initState();
controller = AnimationController(
lowerBound: 0.3,
upperBound: 1.0,
vsync: this,
duration: const Duration(milliseconds: 500));
controller.addListener(() {
setState(() {});
});
}
void dispose() {
controller.dispose();
super.dispose();
}
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 {
_ShowerState createState() => _ShowerState();
}
class _ShowerState extends State<Shower> {
void initState() {
super.initState();
print('-----Shower#initState----------');
}
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
@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。
class AnimatedBuilder extends AnimatedWidget {
const AnimatedBuilder({
Key key,
Listenable animation,
this.builder,
this.child,
}) : assert(animation != null),
assert(builder != null),
super(key: key, listenable: animation);
final TransitionBuilder builder;
final Widget child;
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,
this.listenable,
}) : assert(listenable != null),
super(key: key);
final Listenable listenable;
Widget build(BuildContext context);
_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);
}
这样来看,AnimatedBuilder 似乎也没有什么神秘的,了解了这些,再去看 Flutter 框架中的封装的各种动画组件,你就会豁然开朗,这便是知一而通百。
总结一下,并不是说 setState 不好,而是用的时机对不对。AnimatedBuilder 本质上也是使用 setState 进行触发更新的,所以看待问题不要片面和激进。对于应界面 UI 来说,我们需要关注的是如何将 Build 过程的消耗降到最低,特别是对于动画、滑动这样会持续更新渲染的场景。
最后
最后,在这里我也分享一份干货,由大佬收录整理的Android学习PDF+架构视频+源码笔记 ,还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
更新时间:2024-12-18 20:26