Flutter中从runApp开始看Element树是如何实现第一次挂载的

543 阅读5分钟
import 'package:flutter/material.dart';

main() {
  runApp(MaterialApp(
    home: TestStatelessPage(),
  ));
}

class TestStatelessPage extends StatelessWidget {
  const TestStatelessPage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Container(
        color: Colors.amber,
        child: const Center(
          child: Text("abc"),
        ),
      ),
    );
  }
}

MaterialAppTestStatelessPageScaffoldContainerCenterText

一个简简单单的Flutter页面,黄黄的背景中心有一个text文本;

直接进入正题,MaterialApp作为参数被传入runApp之后做了什么

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}

WidgetsBinding

@protected
void scheduleAttachRootWidget(Widget rootWidget) {
  Timer.run(() {
    attachRootWidget(rootWidget);
  });
}
void attachRootWidget(Widget rootWidget) {
  final bool isBootstrapFrame = renderViewElement == null;
  _readyToProduceFrames = true;
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget, // 这里被作为child参数传入
  ).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
  if (isBootstrapFrame) {
    SchedulerBinding.instance!.ensureVisualUpdate();
  }
}

最终作为child参数被传入RenderObjectToWidgetAdapter (实际就是一个RenderObjectWidget)中;

以上这段代码中,实例化了一个RenderObjectToWidgetAdapter对象,传入了3个参数而后调用了attachToRenderTree方法,传入了buildOwnerrenderViewElement参数;

这里简单提及一下BuildOwner,它是Flutter架构中非常重要的类,在WidgetsBinding 被初始化时实例,负责管理Flutter中所有的Element,包括构建、更新、管理脏元素列表等功能;

第二个参数renderViewElement就是调用attachToRenderTree 之后才进行赋值的,所以首次进入该方法时,必然是以null值传入;

RenderObjectToWidgetAdapter

RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {

  RenderObjectToWidgetAdapter({
    this.child,
    required this.container,
    this.debugShortDescription,
  }) : super(key: GlobalObjectKey(container));

  final Widget? child;

}

上文提到的,RenderObjectToWidgetAdapterRenderObjectWidget 的子类;

至于名字为什么不是一个***Widget,因为它与其他直接继承RenderObjectWidget的子Widget不同,还声明了一个extend RenderObject的泛型,定义上来说它的作用是连接Flutter的Widget系统和底层的RenderObject系统,暂时先不深入;

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
  if (element == null) {
    owner.lockState(() {
      element = createElement();
      assert(element != null);
      element!.assignOwner(owner);
    });
    owner.buildScope(element!, () {
      element!.mount(null, null);
    });
  } else {
    element._newWidget = this;
    element.markNeedsBuild();
  }
  return element!;
}

进入attachToRenderTree方法,根据前面分析,element此时为null,所以进入了第一个if分支;

这里调用了自己内部的方法createElement,创建了Flutter架构中的第一个Element。这里提一下createElement方法是声明在Widget类中的一个抽象方法,每一个具体实现的Widget子类都要实现该方法,一般是在构建Flutter Element树时自上而下的调用,根据对应的Widget生成Element并返回。而此处的RenderObjectToWidgetAdapter 是顶级的Widget,所以它就需要该attachToRenderTree方法来调用自己的createElement方法;

继续向下看,调用element!.assignOwner(owner); ,这里是为顶级Element制定了owner参数,即前面提到的BuildOwner,这个类贯穿整个Element树,自上而下的每一个Element被创建时,其内部的owner变量都会被赋值为parent持有的owner对象,所以最终所有的Element内的owner变量均指向一个,即顶级ElementassignOwner方法中传入的BuildOwner

owner.buildScope(element!, () {
  element!.mount(null, null);
});

最后终于调用了mount方法,这里的owner.buildScope方法我们先简单去理解,简化示例代码:

BuildOwner

@pragma('vm:notify-debugger-on-exception')
void buildScope(Element context, [ VoidCallback? callback ]) {
  if (callback == null && _dirtyElements.isEmpty)
    return;
  /// 省略...
  try {
    _scheduledFlushDirtyElements = true;
    if (callback != null) {
      try {
        callback();
      } finally {
        /// 省略...
      }
    }
    _dirtyElements.sort(Element._sort);
    _dirtyElementsNeedsResorting = false;
    int dirtyCount = _dirtyElements.length;
    int index = 0;
    while (index < dirtyCount) {
      try {
        _dirtyElements[index].rebuild();
      } catch (e, stack) {
         /// 省略...
      }
      index += 1;
      if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting!) {
        _dirtyElements.sort(Element._sort);
        _dirtyElementsNeedsResorting = false;
        dirtyCount = _dirtyElements.length;
        while (index > 0 && _dirtyElements[index - 1].dirty) {
          index -= 1;
        }
      }
    }
  } finally {
     /// 省略...
  }
}

以上代码可以看到,传入的第二个参数实际上就是经过一系列判断之后要调用执行的方法,简单的去解释这个方法就是需要在callback函数调用前后分别执行部分逻辑;

总结下这个方法的主要功能:

  • 确保一些前置条件可以通过,比如检查当前是否有其他buildScope在运行,若有则抛出异常,因为每次只能有一个buildScope在运行
  • 执行你传入的callback方法
  • 遍历所有被标记为“脏”的元素,并调用它们的rebuild方法重新构建对应的Widget

最终进入Elementmount方法:

RenderObjectToWidgetElement

@override
void mount(Element? parent, Object? newSlot) {
  assert(parent == null);
  super.mount(parent, newSlot);
  _rebuild();
  assert(_child != null);
}

这里的两个参数简单介绍下:

  • parent:较好理解,Element树构建是自上而下,所以向下每个Element要挂载时就需要知道自己的父元素,即parent参数
  • newSlotslot 参数是一个更复杂的概念。在 Flutter 的元素树中,每个元素都有一个父元素(除了根元素),并且可以有多个子元素。slot 是用来标识子元素在其父元素中的位置的。它可以是任何类型的对象,具体的类型和含义取决于父元素。一般来说,只有那些需要区分多个子元素的父元素才会使用 slot

由于自己是第一个Element,所以这两个参数都传递了null

至此就开始了Flutter Element树体系中的第一个mount方法的执行,之后所有WidgetElement的挂载逻辑都是从以上的方法中向下遍历进行执行;

mount方法中不断super,追溯到顶层父类Element类的mount方法中:

void mount(Element? parent, Object? newSlot) {
  _parent = parent;
  _slot = newSlot;
  _lifecycleState = _ElementLifecycle.active;
  _depth = _parent != null ? _parent!.depth + 1 : 1;
  if (parent != null) {
    _owner = parent.owner;
  }
  assert(owner != null);
  final Key? key = widget.key;
  if (key is GlobalKey) {
    owner!._registerGlobalKey(key, this);
  }
  _updateInheritance();
}

可以看到该方法中处理了许多变量的赋值,有我们上文中提到的owner变量;

这里看下对_depth变量的赋值,若当前Elementparent,则赋值为_parentdepth+1值,当前Elementparent,所以depth==1,为元素树中的1号元素;

在之前的RenderObjectToWidgetAdapter#attachToRenderTree 方法中断点,在执行了element.mount之后,我们也可以看到element_depth值确实为1;

image.png

以上梳理了自我们常见的runApp入口函数开始到第一个Element被挂载到Element树体系上的逻辑步骤;

其中可以看到许多的关键类和方法,在整个Flutter体系中构建、重绘流程都有使用,后续文章再继续分析!若有疑问欢迎提出,若文中有错误,更欢迎指出!