乐趣区

关于flutter:Flutter调优深入探究MediaQuery引起界面Rebuild的原因及解决办法-京东云技术团队

前言

咱们能够通过 MediaQuery.of(context) 办法获取到一些设施和零碎的相干信息,比方状态栏的高度、以后是否是光明模式等等,应用起来相当不便,然而也要留神可能引起的页面 rebuild 问题。本文会介绍一个典型的例子,并深刻源码来探讨引起 rebuild 的起因,最初介绍防止 rebuild 的几个方法。

典型例子

以快递 App 中的查快递场景举例,首页用 MediaQuery.of(context).padding.top 获取了状态栏高度,用户点击“查快递”按钮会跳转到查快递界面,在查快递界面,用户输出单号可进行查问操作。

\
当首页的 build 办法被调用时,会输入咱们提前加好的日志。咱们发现,当 查快递界面 的键盘弹出时,首页 的 build 办法被调用了屡次:

主界面的 build 代码如下:

源码探索

既然是因为主界面在 build 办法里应用了 MediaQuery.of(context),从而导致当键盘弹出 / 暗藏时进行 rebuild 操作,那么就先来看下MediaQuery 类。

MediaQuery

其继承自 InheritedWidget,本身并没有重写createElement 办法,从 flutter 三棵树的角度讲,对应的 Element 即为InheritedElement。有两个属性,data 和 child,咱们能够从 data 中获取一些设施 / 零碎相干的属性。

另外还有两个比拟重要的办法:

fromWindow(key : Key, child : Widget)

此办法间接返回 _MediaQueryFromWindow 对象,前面会具体介绍。

of(context : BuildContext)

办法里调用了 dependOnInheritedWidgetOfExactType,接下来咱们详细分析下背地的调用流程。

MediaQuery.of(context) 调用流程

入参是 context,本例中的主界面是StatelessWidget,那么这里的context 便是StatelessElement。整体调用流程如下:

dependOnInheritedWidgetOfExactType

_inheritedWidgets 列表中查问是否有 MediaQuery 类型的 InheritedElement,从三棵树的角度讲,就是从以后节点始终向上查找,找到最近的MediaQuery 控件。如果找到,则调用 dependOnInheritedElement 办法(个别状况下是肯定能找到的,上面再具体介绍)。

dependOnInheritedElement

此办法负责将找到的 InheritedElement(也就是MediaQuery 对应的 Element)存起来,并且调用InheritedElement#updateDependencies 办法。

updateDependencies

setDependencies

最初两个办法很简略,其作用是将主页对应的 StatelessElement 存储到了 MediaQuery 对应的 InheritedElement#_dependents 中。

钻研完 MediaQuery.of(context) 背地的原理,咱们能够晓得:通过调用 of 办法,主界面对应的 ElementMediaQuery建设了绑定关系,MediaQuery对应的 InheritedElement 存储了主界面 Element 的援用。

Rebuild 终点

当介绍 dependOnInheritedWidgetOfExactType 办法时,咱们提道:从以后节点往父节点寻找,个别状况下是肯定能找到的 MediaQuery 控件的。这是因为在 WidgetsApp 里会主动给咱们创立一个根MediaQuery

main 办法里,无论应用 CupertinoApp 还是 MaterialApp,最初都会在外部创立WidgetsApp。咱们间接看_WidgetsAppState#build 办法里的一个代码片段:

会首先查看 widget.useInheritedMediaQuery,这个属性默认为false。如果你创立MaterialApp/CupertinoApp 时,没有设置 useInheritedMediaQuery 属性,或者设置了这个属性为 null,但找不到 MediaQueryData,那么这里就会调用MediaQuery.fromWindow 办法。

下面介绍 MediaQuery#fromWindow 时,咱们晓得它会创立 _MediaQueryFromWindow 控件。

_MediaQueryFromWindow的代码不是很多,把和本文相干的代码全副贴出来了,大家能够本人看下,代码如上图所示。

build办法里创立了 MediaQuery 控件,并实现了 didChangeMetrics 办法,当手机产生旋转、键盘弹出 / 暗藏时就会调用此办法,didChangeMetrics外部又调用了 setSate,从而导致build 办法被从新调用。

通过 flutter 三颗树的原理咱们能够晓得,上述所说的“build 办法被从新调用”波及到 MediaQueryFromWindow 对应的 ElementupdateChild办法,简略看下 updateChild 的外部解决规定:

对 MediaQueryFromWindow 而言,每次都会创立新的 MediaQuery Widget,依据 Element#updateChild 源码(不是本文探讨重点,不再详细分析其源码)得悉,最终会调用 MediaQuery 对应的 Element 的 update 办法。

通过一系列的跳转过后,最终会调用到上面的两个外围办法:

下面介绍的 MediaQuery.of(context) 办法最终会把入参 Context 放到 _dependents 变量里,而这里会遍历这个 map,调用每一个ContextdidChangeDependecies办法,didChangeDependecies会将此 Context 置为 dirty 状态,下一帧来长期会被从新绘制,并调用此 Contextbuild办法。

所以,破案了,当键盘弹起 / 暗藏时快递主页会被 rebuild 的起因找到了!

整体的 rebuild 调用流程如下,感兴趣的能够联合这个调用流程图去看源码:

防止 rebuild 的方法

钻研过源码后,解决方案就变的很简略。

  • 自定义 useInheritedMediaQuery 属性为 true,并在最外面包一层 MediaQuery,让WidgetsApp 创立时应用 MediaQuery,而不去应用监听了 application 尺寸变动的_MediaQueryFromWindow 控件。
  • 防止在页面中应用 MediaQuery.of(context) 办法,能够应用对应的代替办法,比方本例能够采纳上面的代码进行代替,留神单位的转换。
  • 如果必须要应用 MediaQuery.of(context) 办法,能够应用 Builder 控件包裹下,of 办法的入参传入此 Buildercontext即可,这样被 rebuild 仅是 Builder 控件包裹下的 widget 子树。

总结

app 界面逐步简单时,咱们不得不思考去优化界面性能。本文中介绍的例子在开发中是很常见的,如果不理解 MediaQuery.of 的机制,可能会引起大量应用此办法的界面产生重绘操作,造成页面卡顿、帧率降落。咱们详细分析了背地的源码逻辑,介绍了解决办法,心愿能给大家的调优工作提供些许帮忙。

作者:京东物流 沈亮堂

起源:京东云开发者社区

退出移动版