Flutter三棵树的创建流程

news2024/11/20 6:20:57

一、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 启动的核心三步(级联运算符调用):

  1. WidgetsFlutterBinding 初始化(ensureInitialized())
  2. 绑定根节点创建核心三棵树(scheduleAttachRootWidget(app))
  3. 绘制热身帧(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 {
  @override
  void 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 {
  @override
  void 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.rootNode
  RenderView 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 {
  @protected
  void scheduleAttachRootWidget(Widget rootWidget) {
  	//简单的异步快速执行,将attachRootWidget异步化
    Timer.run(() {
      attachRootWidget(rootWidget);
    });
  }

  void attachRootWidget(Widget rootWidget) {
  	//1、是不是启动帧,即看renderViewElement是否有赋值,赋值时机为步骤2
    final 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 app
      child: 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。
  @override
  RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
  //6、重写Widget的createRenderObject实现,container本质是一个RenderView。
  //RenderObject树的根结点是RenderView。
  @override
  RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;

  @override
  void 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,所以当前流程为null
    if (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数根标记为markNeedsBuild
      element._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)。
  @override
  RenderObjectWidget get widget => super.widget as RenderObjectWidget;

  //2、Element树通过mount后持有了RenderObject渲染树实例。
  @override
  RenderObject get renderObject => _renderObject!;
  RenderObject? _renderObject;

  @override
  void 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.attachRootWidget
  RenderObjectToWidgetAdapter.attachToRenderTree
    BuildOwner.buildScope
      RenderObjectToWidgetElement.mount
        RootRenderObjectElement.mount(null, null)
          RenderObjectElement.mount
            Element.mount
            RenderObjectWidget.createRenderObject => RenderObjectToWidgetAdapter.createRenderObject
        RenderObjectToWidgetElement._rebuild
          Element.updateChild
            Element.inflateWidget
              Widget.createElement => MyApp
              Element.mount

这里涉及了一大坨Element类型及其方法,有些是自有方法,有些是覆盖方法,有些是基类方法,这个时候只能一步步分析,避免混乱。

RenderObjectToWidgetElementRenderObjectToWidgetAdapter这个Widget具体创建的Element类型,显式的调用了mount方法,并且传入的参数均为(null, null),前面的文章已说明RenderObjectToWidgetElement是真正的Element根节点。关键是它是如何串连起其它Element对象的?

由以上调用序列可知RenderObjectToWidgetElement.mount最终调用了Element.moutElement.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.updateChild
  Element.inflateWidget
    Widget.createElement => MyApp
    Element.mount

针对child != null && newWidget != null这种情况Element.updateChild最终调用的是Element.inflateWidget,注意这个名称有误导性,从代码可知当前Element没有对Widget有任何操作,只是调用了Widget.createElement, 而这个Widget对象是从外部传入的,不是当前Element自己持有的!具体的,这个Widget对象应该是当前Element关联的Widget对象的子对象(widget.childwidgets/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类型。

再譬如RenderObjectElementmount时还创建了RenderObject,并且关联父RenderObject,而这个父RenderObject未必是父Element关联的RenderObject(_findAncestorRenderObjectElementframework.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 {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: HelloWorldPage(),
    );
  }
}

class HelloWorldPage extends StatelessWidget {
  @override
  Widget 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!;
}
  1. 创建RenderObjectToWidgetAdapter作为Widget树的根,将传入的Widget挂上去
  2. 调用RenderObjectToWidgetAdapter.createElement创建Element
  3. 调用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) {
    ...
  // 创建子Element
    final Element newChild = newWidget.createElement();
    ...
  // 调用子Element的mount方法将它挂到Element树上,parent是第一个参数this
  newChild.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是一个递归的过程,总结下来有下面几个步骤

  1. Element如果是RenderObjectElement则创建RenderObject,并从祖先找到上一个RenderObjectElement,然后调用祖先RenderObjectElement的RenderObject的insertRenderObjectChild方法插入创建的RenderObject
  2. 如果子widget需要build出来就调用build方法创建子widget,如果不需要直接在成员变量可以拿到子widget
  3. 调用子widget的createElement创建子Element
  4. 调用子Element的mount方法将子Element的parent设置成自己,然后子Element去到第1步

下面的动图展示了整个流程:

或者可以下载PPT查看


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1154937.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

hdlbits系列verilog解答(加减法器)-28

文章目录 一、问题描述二、verilog源码三、仿真结果一、问题描述 可以通过将其中一个输入变为负来从加法器构建加法器-减法器,这相当于将其输入反相然后加 1。最终结果是一个可以执行两个操作的电路:(a + b + 0) 和 (a + ~b + 1)。如果您想更详细地解释该电路的工作原理…

【小白专用】Mysql的安装配置教程(详细)

首先简单概述分为几个步骤&#xff1a; 一、下载Mysql 二、安装Mysql 三、验证Mysql安装是否成功 四、 配置环境变量 五、验证配置环境变量是否成功 一、下载Mysql 要在Windows或Mac上安装MySQL&#xff0c;首先从MySQL官方网站下载最新的MySQL Community Server版本&…

图形库篇 | EasyX | 图形绘制

图形库篇 | EasyX | 图形绘制 设置颜色 函数功能函数设置画线颜色void setlinecolor(COLORREF color)设置填充颜色void setfillcolor(COLORREF color) 设置画线颜色&#xff1a;void setlinecolor(COLORREF color) 具体功能&#xff1a;设置当前设备画线颜色返回值&#xff…

带IV的分组加密下密文分散存储且存在混淆密文片段的多项式时间解密方案

在使用带IV的分组加密模式下&#xff0c;考虑这样一个场景&#xff1a;分组加密后&#xff0c;每组密文都被分散保存&#xff0c;且在恢复的时候&#xff0c;每组密文会和n个混淆的密文一起提供&#xff0c;此时&#xff0c;若想完整的恢复明文&#xff0c;需要一个多项式时间的…

新手尝试硬件买单片机还是树莓派?

今日话题&#xff0c;新手尝试硬件买单片机还是树莓派&#xff1f;对于硬件初学者&#xff0c;建议首先学习单片机&#xff0c;如51单片机或STM32等。这些平台有丰富的学习资源和示例项目&#xff0c;程序相对简单&#xff0c;更贴近硬件&#xff0c;使初学者能够轻松入门&…

RecyclerView在点击事件的处理上也优于ListView

主要是细节的支持&#xff0c;ListView直接提供setOnItemClickListener,而RecyclerView没有&#xff0c;是因为RV考虑的更细节&#xff0c;比如我要点击条目上的某个图标生效&#xff0c;使用ListView会很难实现&#xff0c;但是RV实现会很容易&#xff0c;直接在适配器中由Vie…

财务管理系统 财务管理软件

财务管理系统 财务管理软件 系统功能介绍&#xff1a; 1、预算管理系统 预算管理是一个承上启下的业务衔接系统&#xff0c;主要用于记录本单位的年度预算指标数据&#xff0c;可以通过接口从财政年度预算数据中导入&#xff0c;也可以根据自身管理控制需要录入…

前端移动web详细解析四

移动 Web 第四天 01-vw适配方案 vw和vh基本使用 vw和vh是相对单位&#xff0c;相对视口尺寸计算结果 vw&#xff1a;viewport width&#xff08;1vw 1/100视口宽度 &#xff09; vh&#xff1a;lviewport height ( 1vh 1/100视口高度 ) vw布局 vw单位的尺寸 px 单位数…

神经网络的解释方法之CAM、Grad-CAM、Grad-CAM++、LayerCAM

原理优点缺点GAP将多维特征映射降维为一个固定长度的特征向量①减少了模型的参数量&#xff1b;②保留更多的空间位置信息&#xff1b;③可并行计算&#xff0c;计算效率高&#xff1b;④具有一定程度的不变性①可能导致信息的损失&#xff1b;②忽略不同尺度的空间信息CAM利用…

Allegro如何恢复器件的丝印,位号

在用Allegro Layout的过程中,有时候会无意间删除掉器件的丝印,位号等,那怎么恢复呢? 被无意删除的电容丝印框,如下图。 首先点击菜单Place 点击Update Symbols...(更新器件) 跳出下面的对话框,找到对应的封装类型打勾。 然后下面四项打勾,然后点击恢复按钮即可。 恢复…

国标GB28181安防视频平台EasyGBS显示设备无法注册是什么原因?该如何解决?

国标GB28181协议视频平台EasyGBS&#xff0c;是基于国标GB28181协议的视频云服务平台&#xff0c;支持多路设备同时接入&#xff0c;并对多平台、多终端分发出RTSP、RTMP、FLV、HLS、WebRTC等格式的视频流。平台可提供视频监控直播、云端录像、云存储、检索回放、智能告警、语音…

三维场景视图加载cgcs2000坐标系数据

需求&#xff1a; 高程服务&#xff1a;2000坐标系 底图&#xff1a;天地图2000坐标系 三维模型&#xff1a;2000坐标系 效果图&#xff1a; 测试数据&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1cJbMfzkMalY06wOrmXStSQ 提取码&#xff1a;uz2a 高程服务…

Xcode中如何操作Git

&#x1f468;&#x1f3fb;‍&#x1f4bb; 热爱摄影的程序员 &#x1f468;&#x1f3fb;‍&#x1f3a8; 喜欢编码的设计师 &#x1f9d5;&#x1f3fb; 擅长设计的剪辑师 &#x1f9d1;&#x1f3fb;‍&#x1f3eb; 一位高冷无情的编码爱好者 大家好&#xff0c;我是 DevO…

Tips:关于自己电脑重装python的流程

新换电脑&#xff0c;记录下安装python环境的流程。 1.先安装python 网上随便找教程 2.再安装pycharm https://blog.csdn.net/thefg/article/details/128881507?loginfrom_csdnhttps://blog.csdn.net/thefg/article/details/128881507?loginfrom_csdn3.再修改默认的pip为…

BI零售数据分析,告别拖延症,及时掌握一线信息

在日常的零售数据分析中&#xff0c;经常会因为数据量太大&#xff0c;分析指标太多且计算组合多变而导致数据分析报表难产&#xff0c;零售运营决策被迫拖延症。随着BI数据可视化分析技术的发展&#xff0c;智能化、可视化、自助分析的BI数据分析逐渐成熟&#xff0c;形成一套…

UG\NX二次开发 获取图层所在的图层类别

文章作者:里海 来源网站:《里海NX二次开发3000例专栏》 感谢粉丝订阅 感谢 大熊猫小竹子 订阅本专栏,非常感谢。 简介 获取图层所在的图层类别,比如获取20层所在的图层类别。已经封装好函数。 效果 代码 #include "me.hpp"using namespace std;//获取图层所在的…

JAVA毕业设计107—基于Java+Springboot+Vue的民宿酒店预订管理系统(源码+数据库)

基于JavaSpringbootVue的民宿酒店预订管理系统(源码数据库)107 一、系统介绍 本系统前后端分离 本系统分为用户、前台、管理员三种角色(角色菜单可以自行分配) 前台&#xff1a; 登录、注册、民宿浏览、民宿评价、民宿酒店下单预订、密码修改、个人信息修改。 管理后台&…

云计算助力史上首届“云上亚运”圆满成功!

201金&#xff0c;魔幻的BGM&#xff0c;以及崛起的中国科技&#xff0c;让杭州亚运会成功出圈。 很多网友表示太震撼了&#xff01;开幕式很漂亮&#xff0c;杭州为了奥运造新城真豪横&#xff0c;看完一整个文化自信住&#xff01; 赛场内外除了无数个令人感动的瞬间&#…

ReuseAndDiffuse笔记

https://arxiv.org/pdf/2309.03549.pdf https://mp.weixin.qq.com/s/pbSK4KOO2hqQU1-uwQzjBA 数据集&#xff1a; BLIP-2、MiniGPT4 等多模态大语言模型,对Moments-In-Time、Kinetics-700 和 VideoLT等数据集进行自动标注&#xff1b; Image-text datasets&#xff1a;平移缩…