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