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>
。不过我看到许多业务方同学对于这个页面的理解有些偏差,比如以下几个问题:PageRoute<T>
的 build 函数是否会多次执行?(比如常用的MaterialPageRoute<T>
里面的 build 函数)
- 当 push 了一个新的页面的时候,不在栈顶的页面根 widget 是否会重新执行 build 函数?
- 非栈顶页面是否会渲染?
- 非栈顶页面的 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
,它通过自定义 RenderObject
和 Element
实现了只渲染 onstage 中的 children,不渲染 offstage 中的 children,但是 offstage 中的 children 会走 build,也会保存状态(这里就解答了一开始的 3、4 问题了)。_Theatre 内部的解析这里不做介绍了,要仔细分析的话篇幅较多
接下来我们来看下
Navigator
使用的 api navigator.overlay?.insertAll(..)
3. 在 Overlay 中插入浮层
先来看下
insertAll(..)
的源码可以看到基本没什么代码,就是把 entries 插入到 _entries,然后
setState((){})
刷新下。所以,在 push 一个新的页面的时候,就会往 Overlay 中插入 entries,插入这个动作会触发 Overlay 的
setState((){})
,导致 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)
(也就是不会触发 newWidget 的 rebuild()) 。当时我看到这里以为这应该所有页面除了自身 setState((){}) ,应该只会触发一次 rebuild 吧?结果打了我一耳光,测试的时候发现只要是
StatelessWidget
就不会触发 rebuild,StatefulWiget
就会触发 rebuild。那么那些会 rebuild 的 widget 是哪里触发的呢?是因为 Overlay 在新插入 OverlayEntry 的时候,会导致里面子组件的层级发生变化,也就是 element.updateChild(..) 里面其实是走到最后的
inflateWidget(..)
,然后 OverlayEntry
里面又有一个 GlobalKey
(可以参阅前面的代码),在这 种种条件套娃下,最后会遍历整棵树的 element.activate()
。最后
StatefulElement
重写了 active()
函数,在里面调用了 markNeedsBuild()
,所以 StatefulWidget
会 rebuild!结论!!!: 上面分析来看,导致 build 的因素非常多,所以这里再次强调:随时考虑 build 函数会被调用。所以千万别在 build() 函数里面干耗时的事儿,尤其是动画阶段的 build
结束语:
- 前面说到 push 新页面的时候,StatefulWidget 会 rebuild,StatelessWidget 不会,那么我所有的根页面外面都套一层 StatelessWidget 是不是会减少 build() 函数调用的次数?
- Overlay 中可以添加
initEntries
,这部分 OverlayEntry 会在 Overlay 初始化的时候加入,请问下面用法有什么问题吗:
上面代码可以直接跑在 code pen 中
- Author:NotionNext
- URL:https://tangly1024.com/article/5429d98b-e6fb-403f-82ec-80d41f992d51
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!
Relate Posts