Flutter三棵树的原理和协同关系

1,059 阅读6分钟

三棵树的关系

上篇文章我们从Flutter的main.dart入手, 一路探讨了三棵树Widget、Element、RenderObject的创建流程和三棵树的关系。简单回顾下三棵树的创建流程和关系:

  • Widget树的根节点是RenderObjectToWidgetAdapter(继承自 RenderObjectWidget extends Widget), 我们runApp中传递的Widget树就被追加到了这个树根的child属性上。
  • Element树的根节点是RenderObjectToWidgetElement(继承自RootRenderObjectElement extends RenderObjectElement extends Element)。通过调用 RenderObjectToWidgetAdaptercreateElement 方法创建。创建 RenderObjectToWidgetElement 的时候把 RenderObjectToWidgetAdapter (创建方法: RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);)通过构造参数传递进去,所以 Element 的 _widget 属性值为 RenderObjectToWidgetAdapter 实例,也就是说 Element 树中 _widget 属性持有了 Widget 树实例。RenderObjectToWidgetAdapter 。
  • RenderObject 树的根结点是 RenderView(RenderView extends RenderObject with RenderObjectWithChildMixin<RenderBox>),在 Element 进行 mount 时通过调用 Widget 树(RenderObjectToWidgetAdapter)的createRenderObject方法获取 RenderObjectToWidgetAdapter 构造实例化时传入的 RenderView 渲染树根节点。

简答的回顾, 我们可以知道如下几点:

  • Widget树: Widget是不可变的。开发者使用声明式写出来的UI界面, Widget仅仅只持有控件的配置信息, 是一个配置, 并不会参与UI渲染。所以即使Widget会频繁的创建和销毁, 也不会影响到渲染的性能。
  • Element树: Element是不稳定的。Element是树中特定位置Widget的一个实例化对象。Element 是通过遍历 Widget树时,调用 Widget 的方法创建的。Element同时持有Widget和RenderObject。Element 承载了视图构建的上下文数据,是连接结构化的配置信息到完成最终渲染的桥梁。
  • RenderObject树: 从名字可以直观知道, RenderObject是主要实现视图渲染的对象。RenderObject渲染视图分为四个阶段, 即布局、绘制、合成、渲染。其中, 布局、绘制在RenderObejct中完成, Flutter采用深度优先遍历渲染树对象, 确定树中各个对象的位置和尺寸, 并把它们绘制在不同的图层上。绘制完毕后, 合成、渲染的工作则交给Skia完成。

三棵树的关系图如下:

三棵树的关系.png

上面我们知道了三棵树是什么?

接下来我们一起学习下三棵树的作用。

三棵树的作用

三棵树是为了性能。Widget只是提供UI展示需要的配置信息, 所以Widget是非常轻量级的, 实例化耗费的性能很少。RenderObject是重量级的, 频繁的实例化和销毁RenderObject对性能的影响比较大, 所以Flutter需要复用Element从而减少频繁创建和销毁RenderObject。所有当Widget树的配置信息改变的时候, Flutter使用Element树来比较新的Widget树和原来的Widget树。那么, Element是怎样比较、根据什么来判断是更新Widget还是新建Widget呢?

abstract class Element extends DiagnosticableTree implements BuildContext {
  /// Creates an element that uses the given widget as its configuration.
  ///
  /// Typically called by an override of [Widget.createElement].
  Element(Widget widget)
    : assert(widget != null),
      _widget = widget;

  ......
  
  RenderObject? get renderObject {
    RenderObject? result;
    void visit(Element element) {
      assert(result == null); // this verifies that there's only one child
      if (element._lifecycleState == _ElementLifecycle.defunct) {
        return;
      } else if (element is RenderObjectElement) {
        result = element.renderObject;
      } else {
        element.visitChildren(visit);
      }
    }
    visit(this);
    return result;
  }
    
  @protected
  @pragma('vm:prefer-inline')
  Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }
    final Element newChild;
    if (child != null) {
      bool hasSameSuperclass = true;
      // When the type of a widget is changed between Stateful and Stateless via
      // hot reload, the element tree will end up in a partially invalid state.
      // That is, if the widget was a StatefulWidget and is now a StatelessWidget,
      // then the element tree currently contains a StatefulElement that is incorrectly
      // referencing a StatelessWidget (and likewise with StatelessElement).
      //
      // To avoid crashing due to type errors, we need to gently guide the invalid
      // element out of the tree. To do so, we ensure that the `hasSameSuperclass` condition
      // returns false which prevents us from trying to update the existing element
      // incorrectly.
      //
      // For the case where the widget becomes Stateful, we also need to avoid
      // accessing `StatelessElement.widget` as the cast on the getter will
      // cause a type error to be thrown. Here we avoid that by short-circuiting
      // the `Widget.canUpdate` check once `hasSameSuperclass` is false.
      assert(() {
        final int oldElementClass = Element._debugConcreteSubtype(child);
        final int newWidgetClass = Widget._debugConcreteSubtype(newWidget);
        hasSameSuperclass = oldElementClass == newWidgetClass;
        return true;
      }());
      if (hasSameSuperclass && child.widget == newWidget) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        newChild = child;
      } 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;
      } else {
        deactivateChild(child);
        assert(child._parent == null);
        /// 
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {
      
      /// 惊天秘密!!! 首次加载, 
      newChild = inflateWidget(newWidget, newSlot);
    }

    ......
    return newChild;
  }

  @mustCallSuper
  void update(covariant Widget newWidget) {
    // This code is hot when hot reloading, so we try to
    // only call _AssertionError._evaluateAssertion once.
    assert(
      _lifecycleState == _ElementLifecycle.active
        && widget != null
        && newWidget != null
        && newWidget != widget
        && depth != null
        && Widget.canUpdate(widget, newWidget),
    );
    _widget = newWidget;
  }
  
void updateSlotForChild(Element child, Object? newSlot) {
  ......
  
  /// 只会更新对应的element树, 并将对应的render树进行更新
  void visit(Element element) {
    element._updateSlot(newSlot);
    if (element is! RenderObjectElement)
      element.visitChildren(visit);
  }
  visit(child);
}

static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}

}

由以上源码可知以下几点:

  • Element调用canUpdate方法, 通过对比Widget树的类型、key, 来判断是否可更新树、还是重新创建树
  • 某一个位置的Widget和新的Widget不一致, 那么, 需要重新创建Element、RenderObject, 频繁创建, 耗费性能
  • 某一个位置的Widget和新的Widget一致时, 则只需要修改RenderObject的配置, 不用进行耗费性能的RenderObject的实例化工作了

因此, 可以看出, 即使外面的 widget 树经常变换重建,我们的 Element 可以维持相对稳定,不会重复创建,当然也就不会重复 mount, 生成 RenderObject,只需要以最小代价更新相关属性即可,最大可能减小了性能消耗。Widget 本身只是一些配置信息,简单的对象,它的变更重建不直接影响渲染,对性能影响很小。

总结

上面分析了Widget、Element、RenderObject的联系。下面简单来个总结。

Flutter遍历Widget树时, 调用Widget里面的createElement方法生成对应节点的Element对象, 同时Element对象持有该Widget对象。

特别地, StatefulElement对象创建的时候, 也执行StatefulWidget里面的createState方法创建state, 并且当前Widget也赋值给了state里的_widget, 然后state赋值给了Element里的_state属性。

StatefulElement 执行 build 方法的时候是执行的 state 里面的 build 方法,并且将自身传入,也就是 常见的 BuildContext。Widget build() => state.build(this);

问题

createState 方法在什么时候调用?state 里面为啥可以直接获取到 widget 对象?

答:Flutter 会在遍历 Widget 树时调用 Widget 里面的 createElement 方法去生成对应节点的 Element 对象,同时执行 StatefulWidget 里面的 createState 方法创建 state,并且赋值给 Element 里的 _state 属性,当前 widget 也同时赋值给了 state 里的_widget,state 里面有个 widget 的get 方法可以获取到 _widget 对象。

build 方法是在什么时候调用的?

答:Element 创建好以后 Flutter 框架会执行 mount 方法,对于非渲染的 ComponentElement 来说 mount 主要执行 widget 里的 build 方法,StatefulElement 执行 build 方法的时候是执行的 state 里面的 build 方法,并且将自身传入,也就是常见的 BuildContext

BuildContext 是什么?

答:StatefulElement 执行 build 方法的时候是执行的 state 里面的 build 方法,并且将自身传入,也就是 常见的 BuildContext。简而言之 BuidContext 就是 Element。

Widget 频繁更改创建是否会影响性能?复用和更新机制是什么样的?

答:不会影响性能,widget 只是简单的配置信息,并不直接涉及布局渲染相关。Element 层通过判断新旧 widget 的runtimeType 和 key 是否相同决定是否可以直接更新之前的配置信息,也就是替换之前的 widget,而不必每次都重新创建新的 Element。

创建 Widget 里面的 Key 到底是什么作用?

答:Key 作为 Widget 的标志,在widget 变更的时候通过判断 Element 里面之前的 widget 的 runtimeType 和 key来决定是否能够直接更新。

参考资料

Flutter中Widget的生命周期和渲染原理

Flutter渲染之Widget、Element 和 RenderObject

Flutter框架和原理剖析

Flutter渲染之通过demo了解Key的作用

一文读懂Flutter的三棵树渲染机制和原理

深入理解BuildContext