7

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

 2 years ago
source link: https://www.cnblogs.com/jingdongkeji/p/17440872.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Flutter调优--深入探究MediaQuery引起界面Rebuild的原因及解决办法

我们可以通过MediaQuery.of(context)方法获取到一些设备和系统的相关信息,比如状态栏的高度、当前是否是黑暗模式等等,使用起来相当方便,但是也要注意可能引起的页面rebuild问题。本文会介我们可以通过MediaQuery.of(context)方法获取到一些设备和系统的相关信息,比如状态栏的高度、当前是否是黑暗模式等等,使用起来相当方便,但是也要注意可能引起的页面rebuild问题。本文会介绍一个典型的例子,并深入源码来探讨引起rebuild的原因,最后介绍避免rebuild的几个办法。
绍一个典型的例子,并深入源码来探讨引起rebuild的原因,最后介绍避免rebuild的几个办法。

以快递App中的查快递场景举例,首页用MediaQuery.of(context).padding.top获取了状态栏高度,用户点击“查快递”按钮会跳转到查快递界面,在查快递界面,用户输入单号可进行查询操作。

99ee34767149efcdb4c81258d84649c76ec7d0b5.jpeg

当首页的build方法被调用时,会输出我们提前加好的日志。我们发现,当查快递界面的键盘弹出时,首页的build方法被调用了多次:

1c71b25e3294787a554ff654302a1950b05f1e2c.png

主界面的build代码如下:

0e60e8a0008a952db4058f2f6c7c7f4d2156d1bf.png

既然是因为主界面在build方法里使用了MediaQuery.of(context),从而导致当键盘弹出/隐藏时进行rebuild操作,那么就先来看下MediaQuery类。

MediaQuery

ca93e1de11acadcdacc1be8bf1621c53ab4d4e67.png

其继承自InheritedWidget,自身并没有重写createElement方法,从flutter三棵树的角度讲,对应的Element即为InheritedElement。有两个属性,data和child,我们可以从data中获取一些设备/系统相关的属性。

另外还有两个比较重要的方法:

fromWindow(key : Key, child : Widget)

5b724d4ff9ae1364473ce47a81a5c0c4fcd247ef.png

此方法直接返回_MediaQueryFromWindow对象,后面会详细介绍。

of(context : BuildContext)

50.png

方法里调用了dependOnInheritedWidgetOfExactType,接下来我们详细分析下背后的调用流程。

MediaQuery.of(context) 调用流程

入参是context,本例中的主界面是StatelessWidget,那么这里的context便是StatelessElement。整体调用流程如下:

845592be3d798d7f664f343d1c3bc2e28b2ed114.png

dependOnInheritedWidgetOfExactType

0921532a03e10a237cce55d11b688cac76e3744e.png

_inheritedWidgets列表中查询是否有MediaQuery类型的InheritedElement,从三棵树的角度讲,就是从当前节点一直向上查找,找到最近的MediaQuery控件。如果找到,则调用dependOnInheritedElement方法(一般情况下是一定能找到的,下面再详细介绍)。

dependOnInheritedElement

3475047fddc5392b520ed244eaf9bde411a21349.png

此方法负责将找到的InheritedElement(也就是MediaQuery对应的Element)存起来,并且调用InheritedElement#updateDependencies方法。

updateDependencies

fc0db47c49a1f82bde4797e316087c40bda77d35.png

setDependencies

a33b8a6c17284ec860c808104a5e9b5796a3e247.png

最后两个方法很简单,其作用是将主页对应的StatelessElement存储到了MediaQuery对应的InheritedElement#_dependents中。

研究完MediaQuery.of(context)背后的原理,我们可以知道:通过调用of方法,主界面对应的ElementMediaQuery建立了绑定关系,MediaQuery对应的InheritedElement存储了主界面Element的引用。

Rebuild起点

当介绍dependOnInheritedWidgetOfExactType方法时,我们提道:从当前节点往父节点寻找,一般情况下是一定能找到的MediaQuery控件的。这是因为在WidgetsApp里会自动给我们创建一个根MediaQuery

main方法里,无论使用CupertinoApp还是MaterialApp,最后都会在内部创建WidgetsApp。我们直接看_WidgetsAppState#build方法里的一个代码片段:

b99a11825cca09b3db11d7d08c0f2f7e97b1b83a.png

会首先检查widget.useInheritedMediaQuery,这个属性默认为false。如果你创建MaterialApp/CupertinoApp时,没有设置useInheritedMediaQuery属性,或者设置了这个属性为null,但找不到MediaQueryData,那么这里就会调用MediaQuery.fromWindow方法。

上面介绍MediaQuery#fromWindow时,我们知道它会创建_MediaQueryFromWindow控件。

628215f6ff6f55a7e4253fd82aec42af96564435.png

_MediaQueryFromWindow的代码不是很多,把和本文相关的代码全部贴出来了,大家可以自己看下,代码如上图所示。

build方法里创建了MediaQuery控件,并实现了didChangeMetrics方法,当手机发生旋转、键盘弹出/隐藏时就会调用此方法,didChangeMetrics内部又调用了setSate,从而导致build方法被重新调用。

通过flutter三颗树的原理我们可以知道,上述所说的“build方法被重新调用”涉及到MediaQueryFromWindow对应的ElementupdateChild方法,简单看下updateChild的内部处理规则:

a33b8a6c17284ec860c808104a5e9b5796a3e247.png

对MediaQueryFromWindow而言,每次都会创建新的MediaQuery Widget,根据Element#updateChild源码(不是本文讨论重点,不再详细分析其源码)得知,最终会调用MediaQuery对应的Element的update方法。

经过一系列的跳转过后,最终会调用到下面的两个核心方法:

beb27e10f74c988d10e0de4fda450f52aec77be7.png

上面介绍的MediaQuery.of(context)方法最终会把入参Context放到_dependents变量里,而这里会遍历这个map,调用每一个ContextdidChangeDependecies方法,didChangeDependecies会将此Context置为dirty状态,下一帧来临时会被重新绘制,并调用此Contextbuild方法。

所以,破案了,当键盘弹起/隐藏时快递主页会被rebuild的原因找到了!

整体的rebuild调用流程如下,感兴趣的可以结合这个调用流程图去看源码:

bf648f2c2399def55a9fdb1a20260ef2fe858394.png

避免rebuild的办法

研究过源码后,解决方案就变的很简单。

  • 自定义useInheritedMediaQuery属性为true,并在最外面包一层MediaQuery,让WidgetsApp创建时使用MediaQuery,而不去使用监听了application尺寸变化的_MediaQueryFromWindow控件。

f048696ba22681d3cefab511748656b38a3d1098.png
  • 避免在页面中使用MediaQuery.of(context)方法,可以使用对应的替代方法,比如本例可以采用下面的代码进行替代,注意单位的转换。

17ebf14ed1e437106cd05b5c17f2da2aeb815213.png
  • 如果必须要使用MediaQuery.of(context)方法,可以使用Builder控件包裹下,of方法的入参传入此Buildercontext即可,这样被rebuild仅是Builder控件包裹下的widget子树。

5e9bafc27455cbba7c69653d1052f73c1ed71fa5.png

app界面逐渐复杂时,我们不得不考虑去优化界面性能。本文中介绍的例子在开发中是很常见的,如果不了解MediaQuery.of的机制,可能会引起大量使用此方法的界面发生重绘操作,造成页面卡顿、帧率下降。我们详细分析了背后的源码逻辑,介绍了解决办法,希望能给大家的调优工作提供些许帮助。

作者:京东物流 沈明亮

来源:京东云开发者社区


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK