前言
我们可以使用 MediaQuery.of(context) 方法来获取一些设备和系统相关的信息,比如状态栏的高度、当前是否处于深色模式等。使用起来相当方便,但是我们也必须注意可能的页面重建问题。 本文将介绍一个典型案例,并深入源码解释重建的原因,最后介绍几种防止重建的方法。
典型
以快递应用中的快递查收场景为例。 首页使用MediaQuery.of(context).padding.top获取状态栏的高度。 当用户点击“查询快递”按钮时,会跳转到查询快递界面。 在快递查询界面,用户输入单号即可查询。
当调用首页的build方法时,会输出我们提前添加的日志。 我们发现,当查询快递界面的按钮弹出时,首页的build方法被多次调用:
主界面的构建代码如下:
源代码探索
由于主界面在build方法中使用了MediaQuery.of(context),导致鼠标弹出/隐藏时要进行重建操作,所以我们先看一下MediaQuery类。
媒体查询
它继承自InheritedWidget,并且不会重绘createElement方法本身。 从flutter三棵树来看,对应的Element是InheritedElement。 有两个属性,data和child,我们可以从data中获取一些设备/系统相关的属性。
还有两个比较重要的方法:
fromWindow(key: Key, child: Widget)
该方法直接返回_MediaQueryFromWindow对象,后面会详细介绍。
的(上下文:BuildContext)
该方法调用了dependOnInheritedWidgetOfExactType,接下来我们详细分析其背后的调用过程。
MediaQuery.of(context)调用流程
输入参数是上下文。 在这个例子中,主界面是StatelessWidget,所以这里的上下文是StatelessElement。 整体调用流程如下:
dependentOnInheritedWidgetOfExactType
查询_inheritedWidgets列表中是否存在MediaQuery类型的InheritedElement。 从三棵树来看,就是从当前节点向下搜索,找到最近的MediaQuery控件。 如果找到了,就调用dependOnInheritedElement方法(一般情况下是一定能找到的,下面我会详细介绍)。
依赖于继承元素
该方法负责保存找到的InheritedElement(即MediaQuery对应的Element)并调用InheritedElement#updateDependency方法。
更新依赖关系
设置依赖关系
最后两种方法非常简单。 它们的作用就是将首页对应的StatelessElement存储在MediaQuery对应的InheritedElement#_dependents中。
研究了MediaQuery.of(context)背后的原理,我们可以知道,通过调用of方法elementui查询界面,主界面对应的Element和MediaQuery建立了绑定关系,而MediaQuery对应的InheritedElement则存储了该Element对MediaQuery的引用。主界面。
重建起点
在介绍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的更新技术。
经过一系列的跳转,最终会调用以下两个核心技术:
上面介绍的 MediaQuery.of(context) 方法最终会将输入参数 Context 放入 _dependents 变量中,这里会遍历map,调用各个Context的didChangeDependecies方法。 didChangeDependecies 会将 Context 设置为脏状态,当下一帧临近时,就会重绘,并调用这个 Context 的 build 方法。
这样,问题就解决了elementui查询界面,当按钮弹出/隐藏时,通过重建的激励就能找到快递主页!
整体重建调用流程如下。 如果有兴趣,可以结合这个调用流程图来看看源码:
避免重建的方法
研究了源码之后,解决方案就变得非常简单了。
总结
当app界面变得更加复杂时,我们就不得不考虑优化界面性能。 本文描述的案例在开发中很常见。 如果不了解MediaQuery.of的机制,可能会导致使用该方法的界面大量重绘,导致页面卡顿、帧率升高。 我们详细分析了其背后的源码逻辑并介绍了解决方案,希望能为您的调优工作提供一些帮助。