爆裂吧现实

不想死就早点睡

0%

谈谈 Flutter 的页面

本文不讨论混合栈,只讨论纯 flutter 栈,代码基于 1.12.13,1.17 上 navigator 有做大量改动,不适用于在文章里面分析(源码量大),原理上相同。阅读本文前请先阅读 谈谈 flutter 的 build

一般来说,flutter 的页面都是由 Navigator 这个组件来组织的,一个页面对应一个 Route<T>。不过我看到许多业务方同学对于这个页面的理解有些偏差,比如以下几个问题:

  1. PageRoute<T> 的 build 函数是否会多次执行?(比如常用的 MaterialPageRoute<T> 里面的 build 函数)
  2. 当 push 了一个新的页面的时候,不在栈顶的页面根 widget 是否会重新执行 build 函数?
  3. 非栈顶页面是否会渲染?
  4. 非栈顶页面的 state 是否会保存?

对于第一个问题和第二个问题,业务方犯的错比较多,都认为不会被再次调用到,但是其实都是 可能 会被调用到的(注意这里是可能)。所以一开始入门 flutter 的时候很容易写出以下错误代码:

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
void _jumpToNextPage() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return NewPage(
// 如果这里 route 的 build 函数重建了,那么 model 就会重建
// 之前的数据都会消失
model: ViewModel(),
);
}),
);
}

class NewPage extends StatefulWidget {
_NewPageState createState() => _NewPageState();
}

class _NewPageState extends State<NewPage> {
@override
Widget build(BuildContext context) {
// 业务方在这里加了一个添加监听的方法,但是只要 build 函数被重建,这个监听就会重新添加。
// 当时是因为只有一个页面,所以一直没发现问题
addListener(..);
return Scaffold(
body: NewPageList(
// bloc 是数据模块,生命周期是应该跟随页面的,这里这样写会在 build 触发的时候变成一个新的 bloc
// 导致之前的数据都消失了
bloc: NewPageBloc(),
),
);
}
}

这里不得不提下 flutter 开发的一个理念:build 函数会在任何时候被调用到,基于这个原则开发代码,才不会出现很多莫名其妙的 bug。

对于3、4 两个问题,我这里先说下答案:非栈顶页面不会被渲染出来,但是 state 会保留。

如果你对于上述问题不了解,或者只知道结论,那么就说明你对 flutter 里面的 build 机制不熟悉,不熟悉 Navigator 的内部结构。

别担心,接下来我就带大家来了解下 Navigator 这个组件。

先来看下我们 push 页面的代码:

1
2
3
4
5
6
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
return NewPage();
}),
);

这里的 context 是 BuildContext,一般来说他就是当前组件对应的 Element,比如之前 _NewPageState.build(context) 中的 context 就是 NewPage widget 对应的 element。

Navigator 在 push 的时候需要 context 是因为它需要根据 context 来获取到 NavigatorState,所以上述代码也可以写成 Navigator.of(context).push(..) 的形式。

继续来看下 push 函数中做了啥:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Future<T> push<T extends Object>(Route<T> route) {
final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null;
route._navigator = this;
// 关键:执行了 route 的 install,暂时忽略 _currentOverlayEntry
route.install(_currentOverlayEntry);
// 将 route 添加到 _histrory 栈中
_history.add(route);
// 通知 route 对应的 action
route.didPush();
route.didChangeNext(null);
if (oldRoute != null) {
oldRoute.didChangeNext(route);
route.didChangePrevious(oldRoute);
}
// 生命周期通知
for (NavigatorObserver observer in widget.observers)
observer.didPush(route, oldRoute);
RouteNotificationMessages.maybeNotifyRouteChange(_routePushedMethod, route, oldRoute);
_afterNavigation(route);
return route.popped;
}

push 函数中并没有添加页面 widget 相关的内容,navigator 把这部分逻辑收敛到了 route.install(_currentOverlayEntry) 里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
abstract class Route<T> {
@protected
@mustCallSuper
void install(OverlayEntry insertionPoint) { }
}

abstract class OverlayRoute<T> extends Route<T> {
@override
void install(OverlayEntry insertionPoint) {
// 将当前 创建的 overlay entry 加入到 _overlayEntries 中
_overlayEntries.addAll(createOverlayEntries());
// 将 _overlayEntries 插入到 navigator 的 overlay 中
navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
super.install(insertionPoint);
}

Iterable<OverlayEntry> createOverlayEntries();
}

Route<T> 的 install 就是个空实现,但是它的子类 OverlayRoute<T> 就有添加组件相关的操作了: navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint); ,将当前 OverlayRoute 创建的 _overlayEntries 插入到 navigator 的 overlay 中

OverlayEntry是浮层实例,它是用在 Overlay 组件中的,阅读过 Navigator 的同学应该知道,Navigator 组件其实是个 StatefulWidget,它在 build 函数中构造了 Overlay 来作为页面的载体,也就是我们的一个页面就是一个 Overlay 中的浮层实例。

继续来看 createOverlayEntries() 的实现:

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
/// TransitionRoute 继承自 OverlayRoute,主要来做页面切换动画用
abstract class ModalRoute<T> extends TransitionRoute<T> {
@override
Iterable<OverlayEntry> createOverlayEntries() sync* {
// 屏障,一般是触摸事件拦截,防止触摸事件传递到非栈顶页面,这里不做解析
yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
// 这里才是构建页面的地方
yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
}

/// 构建页面的方法
Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);

Widget _buildModalScope(BuildContext context) {
// 可以看到这里返回了一个 _ModalScope 组件
// 这个 _ModalScope 组件在 build 的时候会调用到 buildPage 方法
return _modalScopeCache ??= _ModalScope<T>(
key: _scopeKey,
route: this,
);
}
}

abstract class PageRoute<T> extends ModalRoute<T> {
}

class MaterialPageRoute<T> extends PageRoute<T> {

/// 实现了 ModalRoute 中构建页面的方法
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
// 这里就是我们传入的 build 函数
final Widget result = builder(context);
return Semantics(
scopesRoute: true,
explicitChildNodes: true,
child: result,
);
}
}

createOverlayEntries() 的实现是在 ModalRoute 中,它会在 Overlay 组件中添加两个浮层,底部的浮层是一个屏障浮层,用来指针事件(Pointer Event),防止指针事件传递到非栈顶页面,顶部浮层就是我们的业务页面,业务页面的构建是方法:buildPage(..)

指针事件的传递拦截原理如果不了解,可以阅读 [flutter 手势处理了解下(一)](flutter 手势处理了解下(一).md)

最后由我们常用的 MaterialPageRoute<T> 来实现 buildPage(..)

现在我们已经知道页面是加到 Overlay 中的,接下来继续参透下 Overlay 这个 widget。

Overlay 解析

1. OverlayEntry 介绍

官方对于 OverlayEntry 的解释是:A place in an [Overlay] that can contain a widget. 它里面有三个比较重要的属性:

  • opaque:表示当前 entry 是否是一个不透明的 entry(这里的不透明是指会盖住整个 Overlay)
  • maintainState:是否保存 entry 中 widget 的状态,后面会解释
  • _key:一个私有属性,对于理解 overlay 工作机制有很大的作用

2. Overlay 的内部组成

Overlay 是一个 StatefulWidget,我们来看下它的一个大致结构:

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
class Overlay extends StatefulWidget {
final List<OverlayEntry> initialEntries;

const Overlay({
Key key,
this.initialEntries = const <OverlayEntry>[],
}) : assert(initialEntries != null),
super(key: key);

@override
OverlayState createState() => OverlayState();
}

class OverlayState extends State<Overlay> with TickerProviderStateMixin {

@override
Widget build(BuildContext context) {
final List<Widget> onstageChildren = <Widget>[];
final List<Widget> offstageChildren = <Widget>[];
bool onstage = true;
for (int i = _entries.length - 1; i >= 0; i -= 1) {
// 从尾部开始遍历
final OverlayEntry entry = _entries[i];
// 一开始都是 on stage 的
if (onstage) {
onstageChildren.add(_OverlayEntry(entry)); // 注意这里的 _OverlayEntry 是个 widget
// 只要出现了一个不透明的,下面的浮层都属于 off stage
if (entry.opaque)
onstage = false;
} else if (entry.maintainState) {
// 如果 offstage 的 entry 有 maintainState 标记,加入到 offstageChildren 中
offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry)));
}
}
return _Theatre(
onstage: Stack(
fit: StackFit.expand,
children: onstageChildren.reversed.toList(growable: false),
),
offstage: offstageChildren,
);
}
}

可以看到 Overlay 的 build 函数很简单,大致可以总结为以下流程:把一个栈 _entries 分为两批,一批是可视的(onstage),另一批是不可视的(offstage),其中不可视的 entries 只有标记了 maintainState 为 true 才会被加入到 offstageChildren 中(意思就是这一批需要保存状态,其余的全部销毁)。

_Theatre 是一个 RenderObjectWidget,它通过自定义 RenderObjectElement 实现了只渲染 onstage 中的 children,不渲染 offstage 中的 children,但是 offstage 中的 children 会走 build,也会保存状态(这里就解答了一开始的 3、4 问题了)。

_Theatre 内部的解析这里不做介绍了,要仔细分析的话篇幅较多

接下来我们来看下 Navigator 使用的 api navigator.overlay?.insertAll(..)

3. 在 Overlay 中插入浮层

先来看下 insertAll(..) 的源码

1
2
3
4
5
6
7
8
9
10
11
12
void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry below, OverlayEntry above }) {
if (entries.isEmpty)
return;
for (OverlayEntry entry in entries) {
// 当插入的 entry 内的 _overlay 都指向当前的 overlay
entry._overlay = this;
}
setState(() {
// 根据 below 和 above,在 _entries 中适当的位置插入
_entries.insertAll(_insertionIndex(below, above), entries);
});
}

可以看到基本没什么代码,就是把 entries 插入到 _entries,然后 setState((){}) 刷新下。

所以,在 push 一个新的页面的时候,就会往 Overlay 中插入 entries,插入这个动作会触发 OverlaysetState((){}),导致 Overlay 会重新执行 build 函数。

因为 Overlay rebuild 了,所以它下面所有的 child 都会 rebuild。

如果这么想,你就大错特错了!!!

4. 解析页面的 build 时机

正如我一开始说的第一、二个问题的答案:当 push 一个新的页面的时候,前一个页面 可能会 rebuild!

为什么这里是 可能会 rebuild 呢?答案在 Navigator插入的那个 OverlayEntry 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
abstract class ModalRoute<T> extends TransitionRoute<T> {
@override
Iterable<OverlayEntry> createOverlayEntries() sync* {
yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
}

Widget _buildModalScope(BuildContext context) {
// 关键代码,注意这里是 ??=
return _modalScopeCache ??= _ModalScope<T>(
key: _scopeKey,
route: this,
);
}
}

这里 Navigator 用了一个骚操作,widgte ??= Widget() 。如果看过我之前那篇 谈谈 flutter 的 build ,应该知道在 element.updateChild(..) 的时候,如果新旧 widget 相等,那么是不会执行 child.update(newWidget)(也就是不会触发 newWidgetrebuild()) 。

当时我看到这里以为这应该所有页面除了自身 setState((){}) ,应该只会触发一次 rebuild 吧?结果打了我一耳光,测试的时候发现只要是 StatelessWidget 就不会触发 rebuildStatefulWiget 就会触发 rebuild

那么那些会 rebuildwidget 是哪里触发的呢?是因为 Overlay 在新插入 OverlayEntry 的时候,会导致里面子组件的层级发生变化,也就是 element.updateChild(..) 里面其实是走到最后的 inflateWidget(..) ,然后 OverlayEntry 里面又有一个 GlobalKey(可以参阅前面的代码),在这 种种条件套娃下,最后会遍历整棵树的 element.activate()

最后 StatefulElement 重写了 active() 函数,在里面调用了 markNeedsBuild() ,所以 StatefulWidgetrebuild

结论!!!: 上面分析来看,导致 build 的因素非常多,所以这里再次强调:随时考虑 build 函数会被调用。所以千万别在 build() 函数里面干耗时的事儿,尤其是动画阶段的 build

结束语:

  1. 前面说到 push 新页面的时候,StatefulWidget 会 rebuild,StatelessWidget 不会,那么我所有的根页面外面都套一层 StatelessWidget 是不是会减少 build() 函数调用的次数?
  2. Overlay 中可以添加 initEntries,这部分 OverlayEntry 会在 Overlay 初始化的时候加入,请问下面用法有什么问题吗:
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
class OverlayTrap extends StatefulWidget {
@override
_OverlayTrapState createState() => _OverlayTrapState();
}

class _OverlayTrapState extends State<OverlayTrap> {
int _index = 0;

@override
Widget build(BuildContext context) {
Widget content = Text('点我 +1: $_index');

Widget child = Overlay(
initialEntries: [
OverlayEntry(
builder: (context) {
return Center(
child: RaisedButton(
child: content,
onPressed: () {
setState(() {
_index++;
});
},
),
);
},
opaque: false,
)
],
);

child = Scaffold(
appBar: AppBar(
title: Text('Overlay initEntries 的陷阱'),
),
body: child,
);

return child;
}
}

上面代码可以直接跑在 code pen