前言
咱们能够通过 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 办法,主界面对应的 Element
和MediaQuery
建设了绑定关系,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
对应的 Element
的updateChild
办法,简略看下 updateChild
的外部解决规定:
对 MediaQueryFromWindow 而言,每次都会创立新的 MediaQuery Widget,依据 Element#updateChild 源码(不是本文探讨重点,不再详细分析其源码)得悉,最终会调用 MediaQuery 对应的 Element 的 update 办法。
通过一系列的跳转过后,最终会调用到上面的两个外围办法:
下面介绍的 MediaQuery.of(context)
办法最终会把入参 Context
放到 _dependents
变量里,而这里会遍历这个 map
,调用每一个Context
的didChangeDependecies
办法,didChangeDependecies
会将此 Context
置为 dirty 状态,下一帧来长期会被从新绘制,并调用此 Context
的build
办法。
所以,破案了,当键盘弹起 / 暗藏时快递主页会被 rebuild 的起因找到了!
整体的 rebuild 调用流程如下,感兴趣的能够联合这个调用流程图去看源码:
防止 rebuild 的方法
钻研过源码后,解决方案就变的很简略。
- 自定义
useInheritedMediaQuery
属性为 true,并在最外面包一层MediaQuery
,让WidgetsApp
创立时应用MediaQuery
,而不去应用监听了 application 尺寸变动的_MediaQueryFromWindow
控件。
- 防止在页面中应用
MediaQuery.of(context)
办法,能够应用对应的代替办法,比方本例能够采纳上面的代码进行代替,留神单位的转换。
- 如果必须要应用
MediaQuery.of(context)
办法,能够应用Builder
控件包裹下,of 办法的入参传入此Builder
的context
即可,这样被 rebuild 仅是Builder
控件包裹下的 widget 子树。
总结
app 界面逐步简单时,咱们不得不思考去优化界面性能。本文中介绍的例子在开发中是很常见的,如果不理解 MediaQuery.of 的机制,可能会引起大量应用此办法的界面产生重绘操作,造成页面卡顿、帧率降落。咱们详细分析了背地的源码逻辑,介绍了解决办法,心愿能给大家的调优工作提供些许帮忙。
作者:京东物流 沈亮堂
起源:京东云开发者社区