0%

Ticker引起的Rebuild

Flutter应用在路由跳转时会遇到Rebuild场景。

当前文章Flutter示例运行版本:1.22.4

案例

当Widget混入TickerProvider或其子类后,若其通过Navigator进行界面跳转后或从其他界面返回,会导致页面Rebuild,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import 'package:flutter/material.dart';

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

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: FirstPage(),
);
}
}

class FirstPage extends StatefulWidget {
@override
_FirstPageState createState() => _FirstPageState();
}

class _FirstPageState extends State<FirstPage>
with SingleTickerProviderStateMixin {
@override
void initState() {
super.initState();
TabController(length: 0, vsync: this);
}

@override
Widget build(BuildContext context) {
print("First Page build");
return Scaffold(
appBar: AppBar(),
body: Column(children: [
Container(
height: 160,
child: Text("First Page"),
),
FlatButton(
onPressed: () {
print("Go!");
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => SecondPage(),
));
},
child: Text("Go"),
)
]),
);
}
}

class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
print("Second Page build");
return Scaffold(
appBar: AppBar(),
body: Text("Second Page"),
);
}
}

运行后点击按钮跳转时,会有如下输出:

1
2
3
4
5
6
I/flutter (31909): First Page build
I/flutter (31909): Go!
I/flutter (31909): Second Page build
I/flutter (31909): First Page build
// 点击AppBar的返回按钮
I/flutter (31909): First Page build

说明从FirstPage跳转到SecondPage,及从SecondPage返回到FirstPage时,FirstPage都会Rebuild一次。

原因

这个问题也被报告在Flutter的issue上:issue1issue2

追根溯源,可以发现引起Rebuild的原因是TickerMode(涉及内容太多,此处省略跟踪源码的细节),实际上,去掉TabControllerSingleTickerProviderStateMixin,FirstPage修改成以下代码,仍然会Rebuild。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
TickerMode.of(context);
print("First Page build");
return Scaffold(
appBar: AppBar(),
body: Column(children: [
Container(
height: 160,
child: Text("First Page"),
),
FlatButton(
onPressed: () {
print("Go!");
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => SecondPage(),
));
},
child: Text("Go"),
)
]),
);
}
}

TickerMode.of(context)实际上是调用了context.dependOnInheritedWidgetOfExactType<_EffectiveTickerMode>,这导致FirstPage会跟随TickerMode一起Rebuild。而TickerMode源码是这样的(省略了无关代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TickerMode extends StatelessWidget {
const TickerMode({
Key key,
@required this.enabled,
this.child,
}) : assert(enabled != null),
super(key: key);
...
@override
Widget build(BuildContext context) {
return _EffectiveTickerMode(
enabled: enabled && TickerMode.of(context),
child: child,
);
}
...
}

它是一个StatelessWidget,仅仅只是对child再包裹了一层_EffectiveTickerMode。而_EffectiveTickerMode是一个InheritedWidget,且其本身也只定义了一个enabled属性。

查阅官方文档便能知道TickerMode作用是控制Widget树的tickers,简单点说就是控制Widget在可见和不可见时动画的播放及暂停(enabled的变动)。

但这个TickerMode到底是什么时候放到Widget树的呢?运行APP,检查Widget树,可以看到MaterialApp下的Overlay包裹着一个TickerMode,然后才是我们自己的FirstPage

因此可以这么理解:

FirstPage -> SecondPage,FirstPage进入hide状态,TickerModeenabled由true变为false,需要播放动画的终止帧

SecondPage -> FirstPage时,FirstPage进入show状态,TickerModeenabled由false变为true,需要播放动画的起始帧

以上两种情况都需要Rebuild。

影响

由于Flutter独特的渲染机制,实际上这个Rebuild对性能影响并不大,并且该Rebuild并不是bug,真的只是个Feature罢了。但我们也需要了解该细节,以避免在build方法执行耗时较长的操作。尤其对于很多刚入门Flutter的新手,会在build方法里执行网络请求,这经常会导致奇怪的现象。