写点什么

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

  • 2023-05-29
    北京
  • 本文字数:2259 字

    阅读完需:约 7 分钟

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 的机制,可能会引起大量使用此方法的界面发生重绘操作,造成页面卡顿、帧率下降。我们详细分析了背后的源码逻辑,介绍了解决办法,希望能给大家的调优工作提供些许帮助。


作者:京东物流 沈明亮

来源:京东云开发者社区

发布于: 刚刚阅读数: 3
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
Flutter调优--深入探究MediaQuery引起界面Rebuild的原因及解决办法 | 京东云技术团队_flutter_京东科技开发者_InfoQ写作社区