Logo
Memu
Action
Document Page

背景

接触过 Flutter 的同学相比对「BuildContext context」这个变量非常熟悉了,因为 Flutter 中许多特性/操作都离不开它。例如,使用 Navigator 进行路由跳转,使用 MediaQuery 查询主题配置,使用 ScaffoldMessenger 弹出一个提示,使用 Provider 获取一个对象, 使用 Scrollable 获取一个「ScrollController」,使用 Overlay 创建跨页面组建等。可见,BuildContext 的使用场景之广阔、应用程度之深入,但是如果使用 context的时机没有把握好,很可能会给自己埋雷。


那么问题来了,下面这段代码有什么问题?

重点请关注第 12 行代码执行的时机,你会发现它会在「TestPage」生命周期之外执行,会发生什么呢?


BuildContext 异步使用的风险

如果你点击了「Click me」,一秒后就能在控制台看到下面报错:

问题出在哪里?

为了理解到底发生了什么,需要深入了解一下 Navigator.of(context).pop()做了什么。

首先,这点表达式先执行计算是:Navigator.of(context),然后再执行的pop()方法。

  • Navigator.of(context)

上面是一段经过简化之后的代码,注意逻辑就是通过 context 机制获取父节点的「NavigatorState」对象。

MaterialApp 或 WidgetsApp 会自动创建 Navigator,因此不用担心找不到。

由于「findAncestorStateOfType」只是「BuildContext」定义的一个接口,真正实现它的地方在「Element」里面,所以「BuildContext」的本质其实就是「Element」哈。

上面代码逻辑,是从 context为起点,不断向上遍历由不同的 Element 组成的「Element Tree」,直到找到一个节点满足:

  • 「Element」类型为「StatefulElement」
  • 「state」属性的类型为「T」

如果找到了这个节点,就返回对应节点的保存的「state」属性,否则就返回空。

等一等,报错的逻辑在哪里?

自然是在第一行「assert」断言出现了错误:

Assert 断言的逻辑很简单,就是判断当前的「Element」对象的生命周期是否处于「active」状态,否则就会给出错误提示。


为了需要如此断言呢?让我们先来梳理一下「Element」的整个生命周期。

Element 的生命周期

先来看一下「_ElementLifecycle」枚举类型的定义:

如山所示,有 4 个状态。Assert 断言的时候只用了「active」这个状态,可以认为只有处于「active」状态整个「Element」对象才是可用的。在我们开头给的例子中,由于经过了「Future.delay」延迟后,整个「TestPage」已经不在「Widget Tree」中了,自然与之对应的「Element」也就不在处于「active」的状态中了。如果这个时候去调用「findAncestorStateOfType」自然是无法通过断言检查的,这也是使用非法使用「Navigator.of(context)」报错的根源。


接下来可以简单了解一下「Element」生命周期的一个简单转化过程:

在 Element 这里类中,一个有 5 个地方涉及修改「_lifecycleState」属性值。

大致的生命周期流程如上图所示:

  • Element 刚刚创建的时候是「initial」状态
  • 当 Element 被挂载到 Widget 树时,mount调用,变成「active」状态
  • 当 Element 对应的渲染对象不在显示的时候,diactivate调用,变成「inactive」状态。此时「Element」还有希望通过「active」方法复活。
  • 当最后一帧结束后,「Element」依然没有被“复活”,unmount调用,宣告 Element 终结,变成「defunct」状态。



简单梳理一下之后,回顾一开始的例子,可以清楚的认识到,当「TestPage」不在显示的时候,其对应的「Element」会经历从「active」变成「inactive」最后变成「defunct」状态。由于我们的 Future.delay 回掉对象依然持有 context 对象(持有 Element 的引用)因此,Element 还不会被 GC 回收。但是如果此时再去调用「findAncestorStateOfType」就会出现问题,因为此时由于 Element 已经不在「Element Tree」上面了,而「Element Tree」变化又比较频繁,因此很可能造成找到的「Element」对象是不可用的状态。所以,Framework 层会在 Debug 模式下加入许多 Assert 断言,用于预防此类不正常使用导致的不确定的行为。


如何避免呢?

知道了错误的原因,避免起来也很简单。就是

1、先持有后使用

啥意思呢?看下面的代码就知道了:

因为「Navigator.of」的本质就是寻找到一个「NavigatorState」的对象,而这个「NavigatorState」一般处于最顶层,其生命周期属于整个 APP 级别的,因此不用担心它的状态不可用。


2、mounted 判断

如果你的组件是「StatefulWidget」的话,可以通过:

通过「mounted」可以判断当前组件是否被挂载,处于个可用的状态。


3、Element 状态判断

有没有一个傻瓜一点的办法呢?能确保无论怎么调用都不会出幺蛾子吗?

只要我们在调用之前判断一下「Element」是否处于「active」不就行了吗?

理论上可行,下面开始实践:

Element 定义其 _ElementLifecycle _lifecycleState = _ElementLifecycle.initial;为一个私有的属性,没有对外暴露出来。

怎么办呢?通过阅读代码发现了:


通过「debugIsDefunct」可以判断 Debug 模式下 Element 是否处于「defunct」状态。

那么其它模式下如何判断呢?

进一步阅读后发现,通过「renderObject」可以间接的判断 Element 是否处于「defunct」状态下。


所以,就有了下面的代码:

通过判断「Element」是否处于「defunct」状态,来决定是否进行下一步的操作,可以让你很“安全”的调用“pop”方法。


此方法有其 hacky 之处,适用的场景也比较少,属于花里胡哨的东西,不推荐使用,还是老老实实写正轨代码比较好 😜。


总结

本篇站在一个新人的角度来理解 context 异步使用「Navigator.of(context)」时导致的异常,以及面对异常如何从源码入手一步一步发现出错点所在,然后通过自己的理解来规避此类问题。最后,还十分 Hack 的给出了一个看起来“安全”的规避手段,算是对 Flutter 有了进一步的认识,进而提高自己 Flutter 的代码素养。


最后,本篇内容参考了王叔的视频,讲解的非常详细,点赞:


以及「恋猫de小郭」的文章,点赞:


Footer