深入系列:从源码解读Flutter构建

777 阅读6分钟

我正在参加「初夏创意投稿大赛」详情请看:初夏创意投稿大赛

夏天来了,吃个 Flutter 源码的瓜。

关于三棵树

Flutter 的核心设计思想是一切皆组件 。Flutter 将组件的概念进行了扩展,把组件的组织和渲染抽象为三部分,即 Widget,Element 和 RenderObject。

Widget

Widget 只是一个配置,里面存储的是有关视图渲染的配置信息,包括布局、渲染属性、事件响应信息等。

Widget 是不可变的,无法更新,数据更新是以重建 Widget 树的方式进行,会涉及对象的销毁重建和垃圾回收,所以但是因为只有配置信息,不涉及渲染绘制,所以重建的成本很低。

Element

Element 是 Widget 的一个实例化对象,它承载了视图构建的上下文数据,是连接结构化的配置信息到完成最终渲染的桥梁。

Element 是可变的。Element 树这一层将 Widget 树的变化(类似 React 虚拟 DOM diff)做了抽象,可以只将真正需要修改的部分同步到真实的 RenderObject 树中,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树重建。

当新的 Widget 替换旧的 Widget,导致 Element 变化,也就是说,多个 Widget 对应一个 Element。

RenderObject

RenderObject 是主要负责实现视图渲染的对象。

RenderObject 树在 Flutter 的展示过程分为四个阶段,即布局、绘制、合成和渲染。 其中,布局和绘制在 RenderObject 中完成,Flutter 采用深度优先机制遍历渲染对象树,确定树中各个对象的位置和尺寸,并把它们绘制到不同的图层上。绘制完毕后,合成和渲染的工作则交给 Skia 搞定。

BuildContext

BuildContext 对象实际上是 Element 对象。BuildContext 接口用于阻止对 Element 对象的直接操作。

我们日常开发中,一般接触的都是 Widget,并没有使用到 Element,其实我们也在一直操作着 Element,BuildContext 对象实际上就是 Element 对象, Element 实现了 BuildContext,告诉了使用者控件在哪里、可以做什么。BuildContext 接口设计用于阻止对 Element 对象的直接操作。

我们可以把 Widget 当做菜谱,Element 是配菜,RenderObject 是烧菜和出菜。

流程

RenderObjectWidget 介绍

StatelessWidget 和 StatefulWidget 只是用来组装控件的容器,并不负责组件最后的布局和绘制。在 Flutter 中,布局和绘制工作实际上是在 Widget 的另一个子类 RenderObjectWidget 内完成的。比如 Text:

class Text extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
  	// 返回的是 RichText
  	Widget result = RichText(...)
  	...
  }
}

class RichText extends MultiChildRenderObjectWidget{}

abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {}

我们再来看一下 RenderObjectWidget 的源码,来看看如何使用 Element 和 RenderObject 完成图形渲染工作。


abstract class RenderObjectWidget extends Widget {
  @override
  RenderObjectElement createElement();
  @protected
  RenderObject createRenderObject(BuildContext context);
  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
  ...
}

对于 Element 的创建,Flutter 会在遍历 Widget 树时,调用 createElement 去同步 Widget 自身配置,从而生成对应节点的 Element 对象。而对于 RenderObject 的创建与更新,其实是在 RenderObjectElement 类中完成的。


abstract class RenderObjectElement extends Element {
  RenderObject _renderObject;

  @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    _dirty = false;
  }
   
  @override
  void update(covariant RenderObjectWidget newWidget) {
    super.update(newWidget);
    widget.updateRenderObject(this, renderObject);
    _dirty = false;
  }
  ...
}

在 Element 创建完毕后,Flutter 会调用 Element 的 mount 方法。在这个方法里,会完成与之关联的 RenderObject 对象的创建,以及与渲染树的插入工作,插入到渲染树后的 Element 就可以显示到屏幕中了。

构建

mount被调用,发生第一次构建或者 Widget 改变update被调用,会在rebuild中调用performRebuild,在此方法中,父元素调用updateChild方法。在此构建阶段,父元素检查子元素是否可以更新、删除或添加到元素树中。

  @protected
  @pragma('vm:prefer-inline')
  Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {}

通过注释我们知道,这个是 widgets 系统的核心。每次要根据更新的配置添加、更新或删除子元素时,都会调用它。

  • child:由其父元素检查以在当前构建过程中添加、删除、更新或重用的元素。

  • newWidget:子元素将在当前构建过程中引用的小部件 Widget 。

我们从第一个 case 开始看:

    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }

如果新小部件为 null,并且子元素不为空,则框架从元素树中删除子元素。父元素调用deactivateChild将子元素置于非活动状态的方法,并将子元素的相应渲染对象从渲染树中分离出来。对应的场景例如:刷新列表后列表数据为空。

再看第二个 case

   if (child != null) {
			...
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      }
      ...

如果子元素和新小部件都是非空的,并且子元素的旧小部件和新小部件是相同的实例,那么当前子元素被重用而不需要更新。因此,不会调用相应子小部件的构建方法。就性能而言,这是最理想的情况。也就是负责配置的 Widget 没有改变,那么会重用这个元素,直接返回。

再看第三个 case

else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        assert(child.widget == newWidget);
        assert(() {
          child.owner!._debugElementWasRebuilt(child);
          return true;
        }());
        newChild = child;
      }

如果子元素和新小部件都是非空的,并且新旧小部件不是同一个实例,但 canUpdate 方法返回 true,通过新的配置更新子元素。对应的场景例如:Text的文本发生改变后引起的构建。

再看第四个 case

else {
        deactivateChild(child);
        assert(child._parent == null);
        newChild = inflateWidget(newWidget, newSlot);
      }

如果子元素和新小部件都是非空的,并且新旧小部件不相同但canUpdate返回false,那么deactivateChild删除子元素,相应的渲染对象与渲染树分离。最后,将新小部件的新元素返回到元素树。这是在性能方面最昂贵的情况,因为实例化了新元素节点和渲染对象节点。对应的场景例如:Text变成了Button

再看第五个 case

 else {
      newChild = inflateWidget(newWidget, newSlot);
    }

如果子元素为 null,并且其新子元素为非 null,那么说明构建阶段的树的此位置有一个新的小部件。因此,父元素首先调用inflateWidget,在其中调用新小部件方法的createElement方法,并返回新小部件的新元素。此时,父级也为创建的元素设置了一个槽。对应的场景例如:在空的row里添加了一个Button

当然,第一个构建时,都会走到这个 case

更新

StatefulWidget被加载时,StatefulElement会被创建,StatefulElement的构造方法中,通过createState方法创建State,并将StatefulWidgetState对象将永久关联。

  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),

mount中,调用_firstBuild,然后调用state.initState() ,初始化State,并且之后调用一次didChangeDependencies

  @override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    ...
    _firstBuild();
		...
  }
  @override
  void _firstBuild() {
    assert(state._debugLifecycleState == _StateLifecycle.created);
    try {
      _debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
      final Object? debugCheckForReturnedFuture = state.initState() as dynamic;
      ...
      state.didChangeDependencies();
			...
			}

而在上面第三个case中,child.update(newWidget)会先调用state.didUpdateWidget(oldWidget),再build,所以我们重写didUpdateWidget方法时,State由于保证该build方法会在didUpdateWidget之后被调用,因此无需在didUpdateWidget中显式触发构建。

 @override
  void update(StatefulWidget newWidget) {
    super.update(newWidget);
		...
    final Object? debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) as dynamic;
		...
    rebuild();
  }