type
status
date
slug
summary
tags
category
icon
本文不讨论混合栈,只讨论纯 flutter 栈,代码基于 1.12.13,1.17 上 navigator 有做大量改动,不适用于在文章里面分析(源码量大),原理上相同。阅读本文前请先阅读 {% post_link 谈谈-Flutter-的-build 谈谈 flutter 的 build %}
一般来说,flutter 的页面都是由 Navigator 这个组件来组织的,一个页面对应一个 Route<T>。不过我看到许多业务方同学对于这个页面的理解有些偏差,比如以下几个问题:
  1. PageRoute<T> 的 build 函数是否会多次执行?(比如常用的 MaterialPageRoute<T> 里面的 build 函数)
  1. 当 push 了一个新的页面的时候,不在栈顶的页面根 widget 是否会重新执行 build 函数?
  1. 非栈顶页面是否会渲染?
  1. 非栈顶页面的 state 是否会保存?
<!-- more -->
对于第一个问题和第二个问题,业务方犯的错比较多,都认为不会被再次调用到,但是其实都是 可能 会被调用到的(注意这里是可能)。所以一开始入门 flutter 的时候很容易写出以下错误代码:
这里不得不提下 flutter 开发的一个理念:build 函数会在任何时候被调用到,基于这个原则开发代码,才不会出现很多莫名其妙的 bug。
对于3、4 两个问题,我这里先说下答案:非栈顶页面不会被渲染出来,但是 state 会保留。
如果你对于上述问题不了解,或者只知道结论,那么就说明你对 flutter 里面的 build 机制不熟悉,不熟悉 Navigator 的内部结构。
别担心,接下来我就带大家来了解下 Navigator 这个组件。

Navigator push 到底发生了什么?

先来看下我们 push 页面的代码:
这里的 context 是 BuildContext,一般来说他就是当前组件对应的 Element,比如之前 _NewPageState.build(context) 中的 context 就是 NewPage widget 对应的 element。
Navigator 在 push 的时候需要 context 是因为它需要根据 context 来获取到 NavigatorState,所以上述代码也可以写成 Navigator.of(context).push(..) 的形式。
继续来看下 push 函数中做了啥:
push 函数中并没有添加页面 widget 相关的内容,navigator 把这部分逻辑收敛到了 route.install(_currentOverlayEntry) 里面
Route<T> 的 install 就是个空实现,但是它的子类 OverlayRoute<T> 就有添加组件相关的操作了: navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint); ,将当前 OverlayRoute 创建的 _overlayEntries 插入到 navigator 的 overlay 中
OverlayEntry是浮层实例,它是用在 Overlay 组件中的,阅读过 Navigator 的同学应该知道,Navigator 组件其实是个 StatefulWidget,它在 build 函数中构造了 Overlay 来作为页面的载体,也就是我们的一个页面就是一个 Overlay 中的浮层实例。
继续来看 createOverlayEntries() 的实现:
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,我们来看下它的一个大致结构:
可以看到 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(..) 的源码
可以看到基本没什么代码,就是把 entries 插入到 _entries,然后 setState((){}) 刷新下。
所以,在 push 一个新的页面的时候,就会往 Overlay 中插入 entries,插入这个动作会触发 OverlaysetState((){}),导致 Overlay 会重新执行 build 函数。
因为 Overlay rebuild 了,所以它下面所有的 child 都会 rebuild。
如果这么想,你就大错特错了!!!

4. 解析页面的 build 时机

正如我一开始说的第一、二个问题的答案:当 push 一个新的页面的时候,前一个页面 可能会 rebuild!
为什么这里是 可能会 rebuild 呢?答案在 Navigator插入的那个 OverlayEntry 中:
这里 Navigator 用了一个骚操作,widgte ??= Widget() 。如果看过我之前那篇 {% post_link 谈谈-Flutter-的-build 谈谈 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() 函数调用的次数?
  1. Overlay 中可以添加 initEntries,这部分 OverlayEntry 会在 Overlay 初始化的时候加入,请问下面用法有什么问题吗:
上面代码可以直接跑在 code pen 中
Cocos 事件分发机制源码解析(一)谈谈 Flutter 的 build
Loading...