前言

咱们能够通过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的机制,可能会引起大量应用此办法的界面产生重绘操作,造成页面卡顿、帧率降落。咱们详细分析了背地的源码逻辑,介绍了解决办法,心愿能给大家的调优工作提供些许帮忙。

作者:京东物流 沈亮堂

起源:京东云开发者社区