Flutter三棵树的创建流程
一、Flutter常见的家族成员
Widget常见的家族成员

Element常见的家族成员
Render常见的家族成员
二、示例代码对应的Flutter Inspector树
示例代码:MyApp->MyHomePage->ErrorWidget,包含了StatelessWidget、StatefulWidget、LeafRenderObjectWidget,其中StatelessWidget、StatefulWidget都属于组合Widget,它们通过build或者state.build返回自己的子节点,最后一级的ErrorWidget是LeafRenderObjectWidget,是个叶子节点,它下面没有子节点。
三、Flutter树根节点的创建和关联
从上图的创建流程可知,根节点RenderView是在RendererBinding初始化的时候创建的,创建时机最早,接下来调用WidgetsBinding.attachRootWidget方法创建Widget的根节点RenderObjectToWidgetAdapter对象,并把开发者自定义的根Widget MyApp挂载到RenderObjectToWidgetAdapter下面,接下来调用RenderObjectToWidgetAdapter.attachToRenderTree方法创建对应的RenderObjectToWidgetElement对象,这个对象就是Element树的根节点,在创建RenderObjectToWidgetElement对象时通过构造方法持有了RenderObjectToWidgetAdapter对象,然后调用RenderObjectToWidgetElement.mount方法持有了RenderView对象。这样Element就同时持有了其对应的Widget、Render。
四、Flutter树子节点的创建和关联
1. MyApp层级
在RenderObjectToWidgetElement.mount方法里,会继续调用RenderObjectToWidgetElement_rebuild->Element.updateChild->Element.inflateWidget,创建MyApp对应的StatelessElement对象,然后将该对象通过updateChild方法返回给上一级Element的子节点,在创建Element对象时通过构造方法持有了MyApp对象,这样Element就持有了Widget。MyApp是StatelessWidget不是RenderObjectWidget,所以MyApp没有对应的createRenderObject方法,StatelessElement是ComponentElement不是RenderObjectElement,其mount方法里也不会调用widget的createRenderObject方法,所以在这个层级,只有Widget节点(MyApp)和与其对应的Element节点(StatelessElement对象),没有对应的Render节点。
2. MyHomePage层级
在创建完MyApp对应的StatelessElement方法后,会调用其mount方法,然后经过一系列的方法调用,会调用到StatelessElement的build方法,然后调用MyApp的build方法,在这个build方法里会创建MyHomePage对象并返回,然后将MyHomePage作为参数继续调用Element.updateChild->Element.inflateWidget,然后创建MyHomePage对应的StatefulElement对象,然后将该对象通过updateChild方法返回给上一级Element的子节点,在创建Element对象时通过构造方法持有了MyHomePage对象,这样Element就持有了Widget。MyHomePage是StatefulWidget不是RenderObjectWidget,所以MyHomePage没有对应的createRenderObject方法,StatefulElement是ComponentElement不是RenderObjectElement,其mount方法里也不会调用widget的createRenderObject方法,所以在这个层级,只有Widget节点(MyHomePage)和与其对应的Element节点(StatefulElement对象),没有对应的Render节点。
3. ErrorWidget层级
在创建完MyHomePage对应的StatefulElement方法后,会调用其mount方法,然后经过一系列的方法调用,会调用到StatefulElement的build方法,然后调用_MyHomePageState的build方法,在这个build方法里会创建ErrorWidget对象并返回,然后将ErrorWidget作为参数继续调用Element.updateChild->Element.inflateWidget,然后创建ErrorWidget对应的LeafRenderObjectElement对象,然后将该对象通过updateChild方法返回给上一级Element的子节点,在创建Element对象时通过构造方法持有了ErrorWidget对象,这样Element就持有了Widget。LeafRenderObjectElement是RenderObjectElement,会调用ErrorWidget的createRenderObject方法创建renderObject对象(RenderErrorBox),然后将RenderErrorBox对象赋值给Element的_renderObject变量保存下来,然后调用attachRenderObject方法将renderObject插入到Render树里,同时Element也持有了Render对象。
LeafRenderObjectElement是叶子节点类型的Element,没有子节点了,调用mount创建完对应的Render后,执行就结束了,没有后续子节点的创建调用流程了,整个树的创建流程到这里就结束了,各级对应的Widget、Element、Render节点都创建关联完成。
五、示例代码生成的树结构
从上面的创建过程可知,整棵树的创建过程都是在Element的驱动下进行的,对于有子节点的Element,会递归调用Element.mount->Element.updateChild->Element.inflateWidget->创建下一级的Element对象->Element.mount->…递归循环创建整棵树。
在调用mount的过程如果ELement是RenderObjectElement类型的,还会为其创建对应的Render节点。
在整棵树的创建过程中发现,Widget对象创建完成后是保存到对应的Element上的,不会保存到上一级Widget上,Widget是没有直接的父子关系的,Widget这颗树可以理解为是虚拟的,是逻辑上存在的,它的树结构是通过Element实体树来反映的。
原文链接:https://blog.csdn.net/huideveloper/article/details/127710013
Flutter 的 runApp 与三棵树诞生流程源码分析
Flutter 程序入口
我们编写的 Flutter App 一般入口都是在 main 方法,其内部通过调用 runApp 方法将我们自己整个应用的 Widget 添加并运行,所以我们直接去看下 runApp 方法实现,如下:
/*** 位置:FLUTTER_SDK\packages\flutter\lib\src\widgets\binding.dart* 注意:app参数的Widget布局盒子约束constraints会被强制为填充屏幕,这是框架机制,自己想要调整可以用Align等包裹。* 多次重复调用runApp将会从屏幕上移除已添加的app Widget并添加新的上去,* 框架会对新的Widget树与之前的Widget树进行比较,并将任何差异应用于底层渲染树,有点类似于StatefulWidget
调用State.setState后的重建机制。*/
void runApp(Widget app) {WidgetsFlutterBinding.ensureInitialized()..scheduleAttachRootWidget(app)..scheduleWarmUpFrame();
}
可以看到上面三行代码代表了 Flutter 启动的核心三步(级联运算符调用):
- WidgetsFlutterBinding 初始化(ensureInitialized())
- 绑定根节点创建核心三棵树(scheduleAttachRootWidget(app))
- 绘制热身帧(scheduleWarmUpFrame())
WidgetsFlutterBinding 实例及初始化
直接看源码,如下:
class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {static WidgetsBinding ensureInitialized() {if (WidgetsBinding.instance == null)WidgetsFlutterBinding();return WidgetsBinding.instance!;}
}
WidgetsFlutterBinding 继承自 BindingBase,并且 with 了大量的 mixin 类。WidgetsFlutterBinding 就是将 Widget 架构和 Flutter Engine 连接的核心桥梁,也是整个 Flutter 的应用层核心。通过 ensureInitialized() 方法我们可以得到一个全局单例的 WidgetsFlutterBinding 实例,且 mixin 的一堆 XxxBinding 也被实例化。
BindingBase 抽象类的构造方法中会调用initInstances()方法,而各种 mixin 的 XxxBinding 实例化重点也都在各自的initInstances()方法中,每个 XxxBinding 的职责不同,如下:
- WidgetsFlutterBinding:核心桥梁主体,Flutter app 全局唯一。
- BindingBase:绑定服务抽象类。
- GestureBinding:Flutter 手势事件绑定,处理屏幕事件分发及事件回调处理,其初始化方法中重点就是把事件处理回调_handlePointerDataPacket函数赋值给 window 的属性,以便 window 收到屏幕事件后调用,window 实例是 Framework 层与 Engine 层处理屏幕事件的桥梁。
- SchedulerBinding:Flutter 绘制调度器相关绑定类,debug 编译模式时统计绘制流程时长等操作。
- ServicesBinding:Flutter 系统平台消息监听绑定类。即 Platform 与 Flutter 层通信相关服务,同时注册监听了应用的生命周期回调。
- PaintingBinding:Flutter 绘制预热缓存等绑定类。
- SemanticsBinding:语义树和 Flutter 引擎之间的粘合剂绑定类。
- RendererBinding:渲染树和 Flutter 引擎之间的粘合剂绑定类,内部重点是持有了渲染树的根节点。
- WidgetsBinding:Widget 树和 Flutter 引擎之间的粘合剂绑定类。
从 Flutter 架构宏观抽象看,这些 XxxBinding 承担的角色大致是一个桥梁关联绑定,如下:
本文由于是启动主流程相关机制分析,所以初始化中我们需要关注的主要是 RendererBinding 和 WidgetsBinding 类的initInstances()方法,如下:
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {@overridevoid initInstances() {....../***1、创建一个管理Widgets的类对象*BuildOwner类用来跟踪哪些Widget需要重建,并处理用于Widget树的其他任务,例如管理不活跃的Widget等,调试模式触发重建等。*/_buildOwner = BuildOwner();//2、回调方法赋值,当第一个可构建元素被标记为脏时调用。buildOwner!.onBuildScheduled = _handleBuildScheduled;//3、回调方法赋值,当本地配置变化或者AccessibilityFeatures变化时调用。window.onLocaleChanged = handleLocaleChanged;window.onAccessibilityFeaturesChanged = handleAccessibilityFeaturesChanged;......}
}mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {@overridevoid initInstances() {....../*** 4、创建管理rendering渲染管道的类* 提供接口调用用来触发渲染。*/_pipelineOwner = PipelineOwner(onNeedVisualUpdate: ensureVisualUpdate,onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,);//5、一堆window变化相关的回调监听window..onMetricsChanged = handleMetricsChanged..onTextScaleFactorChanged = handleTextScaleFactorChanged..onPlatformBrightnessChanged = handlePlatformBrightnessChanged..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged..onSemanticsAction = _handleSemanticsAction;//6、创建RenderView对象,也就是RenderObject渲染树的根节点initRenderView();......}void initRenderView() {......//RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>//7、渲染树的根节点对象renderView = RenderView(configuration: createViewConfiguration(), window: window);renderView.prepareInitialFrame();}//定义renderView的get方法,获取自_pipelineOwner.rootNodeRenderView get renderView => _pipelineOwner.rootNode! as RenderView;//定义renderView的set方法,上面initRenderView()中实例化赋值就等于给_pipelineOwner.rootNode也进行了赋值操作。set renderView(RenderView value) {assert(value != null);_pipelineOwner.rootNode = value;}
}
到此基于初始化过程我们已经得到了一些重要信息,请记住 RendererBinding 中的 RenderView 就是 RenderObject 渲染树的根节点。上面这部分代码的时序图大致如下:

通过 scheduleAttachRootWidget 创建关联三棵核心树
WidgetsFlutterBinding 实例化单例初始化之后先调用了scheduleAttachRootWidget(app)方法,这个方法位于 mixin 的 WidgetsBinding 类中,本质是异步执行了attachRootWidget(rootWidget)方法,这个方法完成了 Flutter Widget 到 Element 到 RenderObject 的整个关联过程。源码如下:
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {@protectedvoid scheduleAttachRootWidget(Widget rootWidget) {//简单的异步快速执行,将attachRootWidget异步化Timer.run(() {attachRootWidget(rootWidget);});}void attachRootWidget(Widget rootWidget) {//1、是不是启动帧,即看renderViewElement是否有赋值,赋值时机为步骤2final bool isBootstrapFrame = renderViewElement == null;_readyToProduceFrames = true;//2、桥梁创建RenderObject、Element、Widget关系树,_renderViewElement值为attachToRenderTree方法返回值_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(//3、RenderObjectWithChildMixin类型,继承自RenderObject,RenderObject继承自AbstractNode。//来自RendererBinding的_pipelineOwner.rootNode,_pipelineOwner来自其初始化initInstances方法实例化的PipelineOwner对象。//一个Flutter App全局只有一个PipelineOwner实例。container: renderView, debugShortDescription: '[root]',//4、我们平时写的dart Widget appchild: rootWidget,//5、attach过程,buildOwner来自WidgetsBinding初始化时实例化的BuildOwner实例,renderViewElement值就是_renderViewElement自己,此时由于调用完appach才赋值,所以首次进来也是null。).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);if (isBootstrapFrame) {//6、首帧主动更新一下,匹配条件的情况下内部本质是调用SchedulerBinding的scheduleFrame()方法。//进而本质调用了window.scheduleFrame()方法。SchedulerBinding.instance!.ensureVisualUpdate();}}
}
上面代码片段的步骤 2 和步骤 5 需要配合 RenderObjectToWidgetAdapter 类片段查看,如下:
//1、RenderObjectToWidgetAdapter继承自RenderObjectWidget,RenderObjectWidget继承自Widget
class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {......//3、我们编写dart的runApp函数参数中传递的Flutter应用Widget树根final Widget? child;//4、继承自RenderObject,来自PipelineOwner对象的rootNode属性,一个Flutter App全局只有一个PipelineOwner实例。final RenderObjectWithChildMixin<T> container;......//5、重写Widget的createElement实现,构建了一个RenderObjectToWidgetElement实例,它继承于Element。 //Element树的根结点是RenderObjectToWidgetElement。@overrideRenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);//6、重写Widget的createRenderObject实现,container本质是一个RenderView。//RenderObject树的根结点是RenderView。@overrideRenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;@overridevoid updateRenderObject(BuildContext context, RenderObject renderObject) { }/***7、上面代码片段中RenderObjectToWidgetAdapter实例创建后调用*owner来自WidgetsBinding初始化时实例化的BuildOwner实例,element 值就是自己。*该方法创建根Element(RenderObjectToWidgetElement),并将Element与Widget进行关联,即创建WidgetTree对应的ElementTree。*如果Element已经创建过则将根Element中关联的Widget设为新的(即_newWidget)。*可以看见Element只会创建一次,后面都是直接复用的。*/RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {//8、由于首次实例化RenderObjectToWidgetAdapter调用attachToRenderTree后才不为null,所以当前流程为nullif (element == null) {//9、在lockState里面代码执行过程中禁止调用setState方法owner.lockState(() {//10、创建一个Element实例,即调用本段代码片段中步骤5的方法。//调用RenderObjectToWidgetAdapter的createElement方法构建了一个RenderObjectToWidgetElement实例,继承RootRenderObjectElement,又继续继承RenderObjectElement,接着继承Element。element = createElement();assert(element != null);//11、给根Element的owner属性赋值为WidgetsBinding初始化时实例化的BuildOwner实例。element!.assignOwner(owner);});//12、重点!mount里面RenderObject owner.buildScope(element!, () {element!.mount(null, null);});} else {//13、更新widget树时_newWidget赋值为新的,然后element数根标记为markNeedsBuildelement._newWidget = this;element.markNeedsBuild();}return element!;}......
}
对于上面步骤 12 我们先进去简单看下 Element (RenderObjectToWidgetElement extends RootRenderObjectElement extends RenderObjectElement extends Element)的 mount 方法,重点关注的是父类 RenderObjectElement 中的 mount 方法,如下:
abstract class RenderObjectElement extends Element {//1、Element树通过构造方法RenderObjectToWidgetElement持有了Widget树实例。(RenderObjectToWidgetAdapter)。@overrideRenderObjectWidget get widget => super.widget as RenderObjectWidget;//2、Element树通过mount后持有了RenderObject渲染树实例。@overrideRenderObject get renderObject => _renderObject!;RenderObject? _renderObject;@overridevoid mount(Element? parent, Object? newSlot) {......//3、通过widget树(即RenderObjectToWidgetAdapter)调用createRenderObject方法传入Element实例自己获取RenderObject渲染树。//RenderObjectToWidgetAdapter.createRenderObject(this)返回的是RenderObjectToWidgetAdapter的container成员,也就是上面分析的RenderView渲染树根节点。_renderObject = widget.createRenderObject(this);......}
}
到这里对于 Flutter 的灵魂“三棵树”来说也能得出如下结论:
- Widget 树的根结点是 RenderObjectToWidgetAdapter(继承自 RenderObjectWidget extends Widget),我们 runApp 中传递的 Widget 树就被追加到了这个树根的 child 属性上。
- Element 树的根结点是 RenderObjectToWidgetElement(继承自 RootRenderObjectElement extends RenderObjectElement extends Element),通过调用 RenderObjectToWidgetAdapter 的 createElement 方法创建,创建 RenderObjectToWidgetElement 的时候把 RenderObjectToWidgetAdapter 通过构造参数传递进去,所以 Element 的 _widget 属性值为 RenderObjectToWidgetAdapter 实例,也就是说 Element 树中 _widget 属性持有了 Widget 树实例RenderObjectToWidgetAdapter 。
- RenderObject 树的根结点是 RenderView(RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>),在 Element 进行 mount 时通过调用 Widget 树(RenderObjectToWidgetAdapter)的createRenderObject方法获取 RenderObjectToWidgetAdapter 构造实例化时传入的 RenderView 渲染树根节点。
上面代码流程对应的时序图大致如下:
结合上一小结可以很容易看出来三棵树的创建时机(时序图中紫红色节点),也可以很容易看出来 Element 是 Widget 和 RenderObject 之前的一个“桥梁”,其内部持有了两者树根,抽象表示如下:
以上就是三棵树的诞生流程.
热身帧绘制
到此让我们先将目光再回到一开始runApp方法的实现中,我们还差整个方法实现中的最后一个scheduleWarmUpFrame()调用,如下:
mixin SchedulerBinding on BindingBase {void scheduleWarmUpFrame() {......Timer.run(() {assert(_warmUpFrame);handleBeginFrame(null);});Timer.run(() {assert(_warmUpFrame);handleDrawFrame();//重置时间戳,避免热重载情况从热身帧到热重载帧的时间差,导致隐式动画的跳帧情况。resetEpoch();......if (hadScheduledFrame)scheduleFrame();});//在此次绘制结束前该方法会锁定事件分发,可保证绘制过程中不会再触发新重绘。//也就是说在本次绘制结束前不会响应各种事件。lockEvents(() async {await endOfFrame;Timeline.finishSync();});}
}
这段代码的本质这里先不详细展开,因为本质就是渲染帧的提交与触发相关,我们后边文章会详细分析 framework 层绘制渲染相关逻辑,那时再展开。在这里只用知道它被调用后会立即执行一次绘制(不用等待 VSYNC 信号到来)。
这时候细心的话,你可能会有疑问,前面分析 attachRootWidget 方法调用时,它的最后一行发现是启动帧则会调用window.scheduleFrame()然后等系统 VSYNC 信号到来触发绘制,既然 VSYNC 信号到来时会触发绘制,这个主动热身帧岂不是可以不要?
是的,不要也是没问题的,只是体验不是很好,会导致初始化卡帧的效果。因为前面window.scheduleFrame()发起的绘制请求是在收到系统 VSYNC 信号后才真正执行,而 Flutter app 初始化时为了尽快呈现 UI 而没有等待系统 VSYNC 信号到来就主动发起一针绘制(也被形象的叫做热身帧),这样最长可以减少一个 VSYNC 等待时间。
总结
上面就是 Flutter Dart 端三棵树的诞生流程,关于三棵树是如何互相工作的
flutter: 建树流程
树的含义
当然是Element树!虽然对于熟悉以往界面开发的人来说这个结论有点让人狐疑,但我们应该明确的得到肯定:就是这样,因为从任意一个控件抽象Widget
出发,无法到达Widget
根节点或者任何Widget
子节点,也就是无法实施遍历操作,当然也就不是树形数据结构了。对于Web开发的人来说比较容易接受,经常在涉及Web的开发谈到Element,android的开发现在需要习惯这种指称,默认的树指的就是Element树,否则理解就容易产生歧义,同时之前文章所说的Widget树这种说法是错误的,因为根本就没有Widget树!
如前文所述像RenderObjectToWidgetAdapter
这样的Widget
不就显式的持有了一个Widget
作为child
成员吗?的确,但这样的持有是具体类子类的持有,还是无法通过访问成员再访问到它的子节点,这个联系根本就是中断的。
所以建树就是建立Element树,访问Widget
也只能通过Element
间接访问:在Element
定义中可以看到它直接持有了一个Widget
,访问到了Element
也就访问到了Widget
,这是从android转过来的开发人员需要反复铭记的一点。Element
有一个_parent
作为其成员,因此可以上溯到根节点的Widget
,然而令人困惑的是Element
并没有Element
数组或者列表来代表子节点!那Element
是如何访问子节点的?
遍历子节点
基类Element
并没有直接持有数组或者列表来访问子节点,而是通过visitChildren
的空实现体方法,方法参数(ElementVisitor
)本身是一个方法(typedef ElementVisitor = void Function(Element element);
framework.dart:1794)。
这不就是个访问者模式吗,然而为什么要这么搞?这么做的意图是希望完全由Element子类型来决定访问Element子节点的顺序,为遍历操作提供更大的灵活性,子节点的持有还是需要的,只不过由Element子类型具体实现。这是可以想到的,显然,如果我们在基类型持有了子节点,那遍历子节点就有了默认顺序。譬如android中的ViewGroup
, 从头到尾的子视图列表顺序代表了由下到上的层次关系(ZOrder),但不得不再提供类似getChildDrawingOrder
方法来让子类型有改变访问顺序的机会。
遍历形式从直接持有变成方法传递,这样做也是有缺点和风险的,那就是可能在运行期动态的改变访问子节点的顺序而造成视图数据的紊乱!所以在这个方法上也有明确的注释说明访问顺序保持一致的重要性:
/// There is no guaranteed order in which the children will be visited, though
/// it should be consistent over time.
在建立树的过程中也不能调用这方法,因为访问的可能是旧的子节点或者子节点还没有完全建立。这样看来直接持有Element
子节点未必就不好。
建立树的过程
Element对象是如何一步步构建成树形结构的?虽然在Element
代码定义上有一些注释可以参考建树的关键步骤,但最好还是从入口调用分析来看:
WidgetsBinding.attachRootWidgetRenderObjectToWidgetAdapter.attachToRenderTreeBuildOwner.buildScopeRenderObjectToWidgetElement.mountRootRenderObjectElement.mount(null, null)RenderObjectElement.mountElement.mountRenderObjectWidget.createRenderObject => RenderObjectToWidgetAdapter.createRenderObjectRenderObjectToWidgetElement._rebuildElement.updateChildElement.inflateWidgetWidget.createElement => MyAppElement.mount
这里涉及了一大坨Element类型及其方法,有些是自有方法,有些是覆盖方法,有些是基类方法,这个时候只能一步步分析,避免混乱。
RenderObjectToWidgetElement
是RenderObjectToWidgetAdapter
这个Widget
具体创建的Element
类型,显式的调用了mount
方法,并且传入的参数均为(null, null),前面的文章已说明RenderObjectToWidgetElement是真正的Element根节点。关键是它是如何串连起其它Element对象的?
由以上调用序列可知RenderObjectToWidgetElement.mount
最终调用了Element.mout
,Element.mout
其实就是建立指向关系,但它是根节点,不用再指向父节点,只需要关注其子节点创建,再看是如何关联子节点的。RenderObjectToWidgetElement有一个显式的成员_child
, 是一个Element类型,发现其是在RenderObjectToWidgetElement._rebuild
中被赋值的,而_rebuild又是在RenderObjectToWidgetElement.mount
的实现体中被调用,这样走到了一个关键方法Element.updateChild
,从其注释就可以看出来:
This method is the core of the widgets system.
通过两个重要参数为null与否,Element.updateChild
区分了4种具有不同含义的操作,当前只需关注child != null && newWidget != null
这种情况,从其注释看这正是创建子节点的途径!细分的调用序列如下:
Element.updateChildElement.inflateWidgetWidget.createElement => MyAppElement.mount
针对child != null && newWidget != null
这种情况Element.updateChild
最终调用的是Element.inflateWidget
,注意这个名称有误导性,从代码可知当前Element没有对Widget有任何操作,只是调用了Widget.createElement
, 而这个Widget
对象是从外部传入的,不是当前Element自己持有的!具体的,这个Widget
对象应该是当前Element关联的Widget对象的子对象(widget.child
widgets/binding.dart:939),对应的正是我们自定义的MyApp!
所以新创建的子Element是由子Widget创建,接着又调用了子Element的mount
方法,传入的parent参数是this(newChild.mount(this, newSlot);
framework.dart:3084),即将当前Element作为父节点与新建节点Element关联,这个mount
非常形象的表现了一个新建节点挂在一个即有节点之上的操作,于是子节点的mount
继续以上过程直至建立最终的节点。
如此看来,flutter的Element更像是一个衣物挂钩,它建立的树形结构更像前向单链表网,而钩子正是Element._parent
。
再看Widget关联
最开始说Widget并不持有子Widget,那么Element在mount的时候当前Widget又是如何提供子Widget来创建子Element的呢?
答案是还是要看当前Element具体操作mount的方式。譬如我们的根ElementRenderObjectToWidgetElement
直接用了自身持有的根WidgetRenderObjectToWidgetAdapter
持有的child
来关联了我们传入的MyApp
作为子Widget。
再譬如一个比较重要的Element类型ComponentElement
:它是在mount的时候调用了一个自身的抽象方法Widget build()
(framework.dart:3950), 这里返回的Widget
对象正是当前Element需要创建的子Widget。而ComponentElement
有两个最重要的实现类覆盖了Widget build()
方法:StatelessElement
是通过持有的StatelessWidget
对象再去创建一个子Widget对象;StatefulElement
是通过持有的StatefulWidget
对象创建的State<StatefulWidget>
(framework.dart:3989)再去创建子Widget的。我们的MyApp
再去创建它的子Widget时就是通过此类方式,因为MyApp
是一个StatelessWidget
对象,MyApp
创建的Element是StatelessElement
类型。
再譬如RenderObjectElement
在mount
时还创建了RenderObject
,并且关联父RenderObject
,而这个父RenderObject
未必是父Element
关联的RenderObject
(_findAncestorRenderObjectElement
framework.dart:4950);
所以大部分Widget的父子关系并不是持有关系而是创建关系,并且是在Element.mount
的时机创建的,创建后也并不持有!
结论
建立Element树最重要的操作就是Element.mount
。
每一种具体类型的Element,实现了如何将当前Element挂接(mount
)到父节点上的操作;这个挂接操作除了与父Element建立指向关系外,还规定了当前Element的一些其它属性的创建时机和操作。
创建一个Element最重要的操作就是Element.updateChild
。
更具体的是Element.inflateWidget
方法;通过创建子Widget方式的不同,区分了两大类Element和Widget: (StatelessElement, StatelessWidget)和(StatefulElement, StatefulWidget)
所谓的Element树更像是前向单链表网,单链表有共同的表头。
父类Element不持有Element子节点,而是通过Element.visitChildren
把遍历操作交给具体的Element子类型来实现。
但是RenderObject
却像普通的单链表,因为通过mixin RenderObjectWithChildMixin<RenderObject>
提供的child, RenderObject
能够直接遍历子节点。
Flutter三棵树构建过程
flutter的渲染机制基本就是靠Widget、Element、RenderObject三棵树去实现的,这篇博客就来讲讲这三棵树是怎么创建的。
首先我们来看看这三者到底是个啥:
-
Widget: 描述一个UI元素的配置数据,不可变,修改信息需要重新new
-
Element: 通过Widget配置实例化出来的对象,它是可变的
-
RenderObject: 真正的渲染对象
让我们用一个简单的demo来做讲解:
void main() {runApp(MyApp());
}class MyApp extends StatelessWidget {@overrideWidget build(BuildContext context) {return MaterialApp(title: 'Flutter Demo',home: HelloWorldPage(),);}
}class HelloWorldPage extends StatelessWidget {@overrideWidget build(BuildContext context) {return Center(child: Text("Hello World", style: TextStyle(color: Colors.blue)),);}
}
上面的代码正在屏幕的中间显示了一个Hello World字符串。
runApp
在main函数里面只有一行runApp调用,追踪下去我们可以看到它主要做了三件事情:
void runApp(Widget app) {WidgetsFlutterBinding.ensureInitialized()..scheduleAttachRootWidget(app)..scheduleWarmUpFrame();
}void scheduleAttachRootWidget(Widget rootWidget) {Timer.run(() {attachRootWidget(rootWidget);});
}void attachRootWidget(Widget rootWidget) {..._renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(container: renderView,debugShortDescription: '[root]',child: rootWidget,).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);...
}RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {...element = createElement();...element!.mount(null, null);...return element!;
}
- 创建RenderObjectToWidgetAdapter作为Widget树的根,将传入的Widget挂上去
- 调用RenderObjectToWidgetAdapter.createElement创建Element
- 调用Element.mount将它挂到Element树上,Element树的根节点的parent为null
Element.mount
Element的mount方法是三棵树创建流程的关键步骤,不同类型的Element mount的流程不太一样。
1.RenderObjectElement会创建RenderObject
如果Element是RenderObjectElement类型的,那么它对应的Widget一定是RenderObjectWidget类型的,这是它的构造函数决定的:
abstract class RenderObjectElement extends Element {RenderObjectElement(RenderObjectWidget widget) : super(widget);...
}
它在mount的时候会调用RenderObjectWidget.createRenderObject创建RenderObject然后将它挂到RenderObject树上:
RenderObject get renderObject => _renderObject!;RenderObject? _renderObject;void mount(Element? parent, Object? newSlot) {..._renderObject = widget.createRenderObject(this);...attachRenderObject(newSlot);...
}void attachRenderObject(Object? newSlot) {...// 插入RenderObject树_ancestorRenderObjectElement = _findAncestorRenderObjectElement();_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);...
}
这个_findAncestorRenderObjectElement方法比较魔性,找的是祖先RenderObjectElement,其实就是往parent一层层查找,直到找的RenderObjectElement:
RenderObjectElement? _findAncestorRenderObjectElement() {Element? ancestor = _parent;while (ancestor != null && ancestor is! RenderObjectElement)ancestor = ancestor._parent;return ancestor as RenderObjectElement?;
}
insertRenderObjectChild方法将创建的RenderObject插入成为祖先RenderObjectElement的RenderObject的子节点,这样就把创建的RenderObject挂到了RenderObject树上。
2.创建子Element并mount到Element树
处理完本节点的RenderObject之后,就会创建子Element将它的parent设置成自己,mount到Element树上。
Element都是通过Widget.createElement创建的,而Element会保存创建它的Widget。所以可以通过这个Widget去获取子Widget,然后用子Widget去创建子Element。
子Widget的获取有两种方式,如果是在Widget的构造函数传入的,那么直接可以拿到它,例如上面的RenderObjectToWidgetAdapter,然后用它去createElement创建子Element:
// 子widget是child参数传进去的
RenderObjectToWidgetAdapter<RenderBox>(container: renderView,debugShortDescription: '[root]',child: rootWidget,
)void mount(Element? parent, Object? newSlot) {..._rebuild();...
}void _rebuild() {...// widget.child拿到构造函数传进去的子widget,即rootWidget_child = updateChild(_child, widget.child, _rootChildSlot);...
}Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {...newChild = inflateWidget(newWidget, newSlot);...
}Element inflateWidget(Widget newWidget, Object? newSlot) {...// 创建子Elementfinal Element newChild = newWidget.createElement();...// 调用子Element的mount方法将它挂到Element树上,parent是第一个参数thisnewChild.mount(this, newSlot);...return newChild;
}
像StatelessWidget这种子widget是build出来的,则在mount的时候会调用它的build方法创建子widget,然后用它去createElement创建子Element:
void mount(Element? parent, Object? newSlot) {..._firstBuild();...
}void _firstBuild() {rebuild();
}void rebuild() {...performRebuild();...
}void performRebuild() {...built = build();//updateChild在上面也有追踪这里就不列出来了,内部调用了built.createElement创建子Element并返回_child = updateChild(_child, built, slot);...
}Widget build() => widget.build(this);
最终得到的三棵树大概长下面的样子,由于没有分成所以看上去是链表而不是树,但是这不影响我们理解,一旦某些节点有多个child节点就是输了:
Element通过widget成员持有Widget,如果是RenderObjectElement还通过renderObject成员持有RenderObject,可以看出来Element是连接Widget和RenderObject的桥梁。三个树的构建也都是通过递归mount Element去实现的。
当RenderObject树创建出来之后,Flutter的引擎就能遍历它去执行绘制将画面渲染出来了。
mount流程解析
从上面的代码可以看得出来,mount是一个递归的过程,总结下来有下面几个步骤
- Element如果是RenderObjectElement则创建RenderObject,并从祖先找到上一个RenderObjectElement,然后调用祖先RenderObjectElement的RenderObject的insertRenderObjectChild方法插入创建的RenderObject
- 如果子widget需要build出来就调用build方法创建子widget,如果不需要直接在成员变量可以拿到子widget
- 调用子widget的createElement创建子Element
- 调用子Element的mount方法将子Element的parent设置成自己,然后子Element去到第1步
下面的动图展示了整个流程:
或者可以下载PPT查看
相关文章:

Flutter三棵树的创建流程
一、Flutter常见的家族成员 Widget常见的家族成员 Element常见的家族成员 Render常见的家族成员 二、示例代码对应的Flutter Inspector树 示例代码:MyApp->MyHomePage->ErrorWidget,包含了StatelessWidget、StatefulWidget、LeafRenderObjectWid…...
思维训练第二课 独立主格
系列文章目录 文章目录 系列文章目录前言一、独立主格特点 二、独立主格的构成1.名词/人称代词主格现在分词2. 名词/人称代词主格过去分词3. 名词/人称代词主格形容词/副词4. 名词/人称代词主格不定式5. 名词/人称代词主格介词短语6.介词复合宾语 三、独立主格结构的句法功能1、…...

一致性哈希揭秘,深入解析其工作原理
前言 在进行一致性哈希介绍前,先思考2个问题: 什么是Hash一致性Hash和Hash的关系是什么 对于第一个问题Hash的定义 Hash也成散列,基本原理就是把任意长度的输入,通过hash算法变成固定长度的输出。 对于第二个问题,…...

前端环境的安装 Node npm yarn
一 node npm 1.下载NodeJS安装包 下载地址:Download | Node.js 2.开始安装 打开安装包后,一直Next即可。当然,建议还是修改一下安装位置,NodeJS默认安装位置为 C:\Program Files 3.验证是否安装成功 打开DOS命令界面&#…...

基于机器视觉的银行卡识别系统 - opencv python 计算机竞赛
1 前言 🔥 优质竞赛项目系列,今天要分享的是 基于深度学习的银行卡识别算法设计 该项目较为新颖,适合作为竞赛课题方向,学长非常推荐! 🧿 更多资料, 项目分享: https://gitee.com/dancheng…...
大数据工具-kafkaUi-lite
1、kafkaUI-lite v1.0 已经发布,此版本更新内容包括: 可以实现 kafak/zookooper/redis 的界面化操作 kafka: 多环境管理、生产消息、消费消息、创建 topic、删除 topiczookeeper: 多环境管理、查看节点、查看节点数据redis: 多环境管理、查询数据2、kafkaUI-lite 介绍 史上…...
Vdue之模版语法指令过滤器计算属性监听属性
模板语法 Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析。vue将模板编译成虚拟dom, 结合响应系统,V…...

Mysql权限控制语句
1.创建用户 create user ky32localhost IDENTIFIED by 123456 create user:创建用户开头 ky32:用户名 localhost 新建的用户可以在哪些主机上登录 即可以使用ip地址,网段主机名 ky32localhost ky32192.168.233.22 ky32192.168.233.0/2…...

小程序如何导入配送账号
为了提高配送效率和用户体验,可以导入配送账号(包括电子面单快递物流账号、同城外卖配送账号)到小程序中。导入后,可以实现一键发货,无需手动回填单号。而且在小程序中可以查看到物流状态,对于同城配送&…...

ubuntu(18.04) 安装 blast 并在php中调用
1、下载 https://ftp.ncbi.nlm.nih.gov/blast/executables/blast/LATEST/2、解压,配置环境变量 tar zvxf ncbi-blast-2.14.1-x64-linux.tar.gz解压后改名为 blast 配置环境变量,可以不配置 使用的时候直接绝对路径使用(本次使用绝对路径&am…...

UML—时序图是什么
目录 前言: 什么是时序图: 时序图的组成元素: 1. 角色(Actor) 2. 对象(Object) 3. 生命线(LifeLine) 4. 激活期(Activation) 5. 消息类型(Message) 6.组合片段(Combined fragment) 时序图的绘制规则: 绘制时序图的3步: 1.划清边界…...
【每日一题Day364】LC2003每棵子树内缺失的最小基因值 | dfs
每棵子树内缺失的最小基因值【LC2003】 有一棵根节点为 0 的 家族树 ,总共包含 n 个节点,节点编号为 0 到 n - 1 。给你一个下标从 0 开始的整数数组 parents ,其中 parents[i] 是节点 i 的父节点。由于节点 0 是 根 ,所以 parent…...
调试记录 单片机GD32F103C8T6(兆易创新) 程序烧写完成但是没有现象 (自己做的板子)
1. 单片机GD32F103C8T6 的资料 CPU内核:ARM Cortex-M3 CPU最大主频:108MHz 工作电压范围:2.6V~3.6V 程序存储容量:64KB 程序存储器类型:FLASH RAM, 总容量:20KB GPIO端口数量:37 最…...

Leetcode刷题笔记--Hot91--100
1--汉明距离(461) 主要思路: 按位异或,统计1的个数; #include <iostream> #include <vector>class Solution { public:int hammingDistance(int x, int y) {int z x ^ y; // 按位异或int res 0;while(…...
算法训练一——链表
文章目录 已做...

【JAVA】类与对象的重点解析
个人主页:【😊个人主页】 系列专栏:【❤️初识JAVA】 文章目录 前言类与对象的关系JAVA源文件有关类的重要事项static关键字 前言 Java是一种面向对象编程语言,OOP是Java最重要的概念之一。学习OOP时,学生必须理解面向…...
ES6对象扩展
ES6对象扩展是指在ES6中新增的一些对象属性和方法,包括对象属性的简写、计算属性名、对象方法的简写、对象的可迭代性、拓展运算符等。 下面是一些常用的ES6对象扩展: 对象属性的简写 ES6中,当对象的属性名和赋值变量名相同时,…...

docker应用部署---Tomcat的部署配置
1. 搜索tomcat镜像 docker search tomcat2. 拉取tomcat镜像 docker pull tomcat3. 创建容器,设置端口映射、目录映射 # 在/root目录下创建tomcat目录用于存储tomcat数据信息 mkdir ~/tomcat cd ~/tomcatdocker run -id --namec_tomcat \ -p 8080:8080 \ -v $PWD:…...

TestCenter测试管理工具
estCenter(简称TC)一款广受好评的测试管理工具,让测试工作更规范、更有效率,实现测试流程无纸化,测试数据资产化。 产品概述 TC流程图 产品功能 一、案例库 案例库集中化管理,支持对测试用例集中管理&…...
索引切片复习
# loc方法 data2.loc[:4,[ymd, bWendu]]# iloc方法 —— 连续取字段 data2.iloc[:4,1:3]# iloc方法 —— 非连续取字段 data2.iloc[:4,[1,4]]# 直接选取单个字段 —— Series data2[ymd]# 直接选取单个字段 —— DataFrame data2[[ymd]]# 直接选取多个字段 —— DataFrame data…...

使用docker在3台服务器上搭建基于redis 6.x的一主两从三台均是哨兵模式
一、环境及版本说明 如果服务器已经安装了docker,则忽略此步骤,如果没有安装,则可以按照一下方式安装: 1. 在线安装(有互联网环境): 请看我这篇文章 传送阵>> 点我查看 2. 离线安装(内网环境):请看我这篇文章 传送阵>> 点我查看 说明:假设每台服务器已…...

日语AI面试高效通关秘籍:专业解读与青柚面试智能助攻
在如今就业市场竞争日益激烈的背景下,越来越多的求职者将目光投向了日本及中日双语岗位。但是,一场日语面试往往让许多人感到步履维艰。你是否也曾因为面试官抛出的“刁钻问题”而心生畏惧?面对生疏的日语交流环境,即便提前恶补了…...
Spring Boot 实现流式响应(兼容 2.7.x)
在实际开发中,我们可能会遇到一些流式数据处理的场景,比如接收来自上游接口的 Server-Sent Events(SSE) 或 流式 JSON 内容,并将其原样中转给前端页面或客户端。这种情况下,传统的 RestTemplate 缓存机制会…...

中南大学无人机智能体的全面评估!BEDI:用于评估无人机上具身智能体的综合性基准测试
作者:Mingning Guo, Mengwei Wu, Jiarun He, Shaoxian Li, Haifeng Li, Chao Tao单位:中南大学地球科学与信息物理学院论文标题:BEDI: A Comprehensive Benchmark for Evaluating Embodied Agents on UAVs论文链接:https://arxiv.…...

376. Wiggle Subsequence
376. Wiggle Subsequence 代码 class Solution { public:int wiggleMaxLength(vector<int>& nums) {int n nums.size();int res 1;int prediff 0;int curdiff 0;for(int i 0;i < n-1;i){curdiff nums[i1] - nums[i];if( (prediff > 0 && curdif…...
渲染学进阶内容——模型
最近在写模组的时候发现渲染器里面离不开模型的定义,在渲染的第二篇文章中简单的讲解了一下关于模型部分的内容,其实不管是方块还是方块实体,都离不开模型的内容 🧱 一、CubeListBuilder 功能解析 CubeListBuilder 是 Minecraft Java 版模型系统的核心构建器,用于动态创…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...
leetcodeSQL解题:3564. 季节性销售分析
leetcodeSQL解题:3564. 季节性销售分析 题目: 表:sales ---------------------- | Column Name | Type | ---------------------- | sale_id | int | | product_id | int | | sale_date | date | | quantity | int | | price | decimal | -…...
是否存在路径(FIFOBB算法)
题目描述 一个具有 n 个顶点e条边的无向图,该图顶点的编号依次为0到n-1且不存在顶点与自身相连的边。请使用FIFOBB算法编写程序,确定是否存在从顶点 source到顶点 destination的路径。 输入 第一行两个整数,分别表示n 和 e 的值(1…...
【Go语言基础【12】】指针:声明、取地址、解引用
文章目录 零、概述:指针 vs. 引用(类比其他语言)一、指针基础概念二、指针声明与初始化三、指针操作符1. &:取地址(拿到内存地址)2. *:解引用(拿到值) 四、空指针&am…...