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

news/2025/2/14 3:32:39/

前言

我们可以通过MediaQuery.of(context)方法获取到一些设备和系统的相关信息,比如状态栏的高度、当前是否是黑暗模式等等,使用起来相当方便,但是也要注意可能引起的页面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的机制,可能会引起大量使用此方法的界面发生重绘操作,造成页面卡顿、帧率下降。我们详细分析了背后的源码逻辑,介绍了解决办法,希望能给大家的调优工作提供些许帮助。

作者:京东物流 沈明亮

来源:京东云开发者社区


http://www.ppmy.cn/news/101960.html

相关文章

ADC子系統參數設計

需求描述: 輸入信號10~50Hz要求準確測量頻率和有效值要求能夠測量到至少150倍基準頻率信號FFT的運算能力首先,有效數據空間可能只有400Bytes。所有的測量值需要達到百分之一的測量精度。 參數演算: 始終記得,你不需要畫蛇添足&…

An illegal reflective access operation has occurred警告

问题描述 今天 在 install Maven项目的时候,控制台出现警告如下: WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by com.thoughtworks.xstream.core.util.Fields (file:/E:/DevelopmentKit/Maven/…

年度发布 | MeterSphere一站式开源持续测试平台发布v2.10 LTS版本

2023年5月25日,MeterSphere一站式开源持续测试平台正式发布v2.10 LTS版本。这是继2022年5月发布v1.20 LTS版本后,MeterSphere开源项目发布的第三个LTS(Long Term Support)版本。MeterSphere开源项目组将对MeterSphere v2.10 LTS版…

程序员大战黄牛党,破解演唱会门票秒光之困

反黄牛,为何是场持久战? 撰文 | 林秋艺 编辑 | 龚 正 这个五月,似乎都被五月天霸屏了。从5月9日的30万张五月天演唱会门票被5秒扫光;到粉丝群起反抗,喊出“宁可鸟巢门口站,也不能让黄牛赚”,…

java ThreadLocal

private ThreadLocal threadLocal new ThreadLocal(); threadLocal.set(0); (int) threadLocal.get(); 上面三行代码分别是定义、赋值和取值。 介绍: 我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。虽然所有的线程都能访问到这个T…

Axure教程—堆积面积图

本文将教大家如何用AXURE制作堆积面积图 一、效果介绍 如图: 预览地址:https://d4nsae.axshare.com 下载地址:https://download.csdn.net/download/weixin_43516258/87838160 二、功能介绍 简单填写中继器内容即可动态显示值 样式颜色等可…

JavaEE进阶(5/28)SpringMVC

目录 1.什么是SpringMVC? 2.学习SpringMVC学习了什么? 3.SpringMVC核心1 4.SpringBoot传递参数 5.SpringBoot传递对象参数 6.SpringBoot传递表单 7.SpringBoot后端参数重命名 8.SpringBoot后端用来接收json对象 1.什么是SpringMVC? 1.…

JVM学习笔记

JVM体系 JVM的位置 JVM是运行在操作系统之上的,它与硬件没有直接的交互 整体结构 程序计数器 是一个非常小的内存空间,几乎可以忽略不记。 每个线程都有一个程序计数器,是线程私有的,就是一个指针,指向方法区中的方…