flutter 三棵树 widget,element,renderObject

前几篇中介绍了flutter整体框架以及dart语言基础和特性。从这篇开始分享flutter的相关内容。本篇要分享的是flutter框架里的三棵树widget,element,renderObject。将从以下几个问题逐步探究这三棵树:

  • widget,element,renderObject对应关系
  • 三棵树是如何工作的?
    Flutter的理念是一切都是Widget(Everything is Widget)。开发者在开发Flutter app的时候主要都是在写很多Widget。对flutter有所了解的都知道flutter是声明式UI框架,与之对应的是命令式。举个例子:一个页面上有N个TextView,在Android开发中如果我们想给这N个TextView设置文案,那我们通常需要调用这N个TextView的setText方法来设置。而对于声明式UI框架的Flutter,需要将数据变更,重新构建WidgetTree。也就是我们在flutter开发过程中数据变更时做的setState。相信你一定会想到每次setState都会重新构建WidgetTree,应该对性能损耗很大吧。flutter号称高性能的跨平台UI框架,那么是怎么解决这样的问题呢?
    我们在使用Widget是都是widget的构造方法,另外我们使用的Widget大部分是继承自 StateLessWidget或者StatefulWidget。我们先看看StateLessWidget里都做了些什么呢?
abstract class StatelessWidget extends Widget {
  /// Initializes [key] for subclasses.
  const StatelessWidget({ Key key }) : super(key: key);

  /// Creates a [StatelessElement] to manage this widget's location in the tree.
  ///
  /// It is uncommon for subclasses to override this method.
  @override
  StatelessElement createElement() => StatelessElement(this);

  /// Describes the part of the user interface represented by this widget.
  @protected
  Widget build(BuildContext context);
}

通过这段源码我们不难看出,在createElement 方法里,widget将自己的引用传给了StatelessElement,我们在看下StatelessElement:

class StatelessElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatelessElement(StatelessWidget widget) : super(widget);

  @override
  StatelessWidget get widget => super.widget as StatelessWidget;

  @override
  Widget build() => widget.build(this);

  @override
  void update(StatelessWidget newWidget) {
    super.update(newWidget);
    assert(widget == newWidget);
    _dirty = true;
    rebuild();
  }
}

在这块代码中build()方法将widget跟element关联了起来,也就是widget的build方法持有的buildContext就是element。同时我们也可以发现并没有绘制,布局相关的内容,那只能继续跟进父类ComponentElement,Element了,跟进之后发现我们只能找到get renderObject却并没找到renderObject的创建,但是在Element里有这么一块代码:

/// The render object at (or below) this location in the tree.
  ///
  /// If this object is a [RenderObjectElement], the render object is the one at
  /// this location in the tree. Otherwise, this getter will walk down the tree
  /// until it finds a [RenderObjectElement].
  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;
  }

在获取的时候实际上是在element 树上去查找离当前节点最近的RenderObjectElement,也就是说此处返回的RenderObject是RenderObjectElement的element.renderObject 。到这里我们也可以得到一个结论就是:Widget 跟 Element是一一对应的,但是Element跟RenderObject并不是一一对应的。下面我们来看下RenderObjectElement:

@override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    assert(() {
      _debugDoingBuild = true;
      return true;
    }());
    _renderObject = (widget as RenderObjectWidget).createRenderObject(this); /// 此处会去创建RenderObject
    assert(!_renderObject!.debugDisposed!);
    assert(() {
      _debugDoingBuild = false;
      return true;
    }());
    assert(() {
      _debugUpdateRenderObjectOwner();
      return true;
    }());
    assert(_slot == newSlot);
    attachRenderObject(newSlot);
    _dirty = false;
  }

mount方法在生命周期里有讲过,会在页面创建的时候调用,也是在此时renderObject跟RenderObjectElement,RenderObjectWidget也就关联上了,并且RenderObject持有element的引用。那么接下来毫无疑问看看RenderObject是干啥的吧。

/// Paint this render object into the given context at the given offset.
  ///
  /// Subclasses should override this method to provide a visual appearance
  /// for themselves. The render object's local coordinate system is
  /// axis-aligned with the coordinate system of the context's canvas and the
  /// render object's local origin (i.e, x=0 and y=0) is placed at the given
  /// offset in the context's canvas.
  ///
  /// Do not call this function directly. If you wish to paint yourself, call
  /// [markNeedsPaint] instead to schedule a call to this function. If you wish
  /// to paint one of your children, call [PaintingContext.paintChild] on the
  /// given `context`.
  ///
  /// When painting one of your children (via a paint child function on the
  /// given context), the current canvas held by the context might change
  /// because draw operations before and after painting children might need to
  /// be recorded on separate compositing layers.
  void paint(PaintingContext context, Offset offset) { }
void layout(Constraints constraints, { bool parentUsesSize = false }) 

在RenderObject中很容易就找到了跟布局相关的layout方法,和跟绘制相关的paint方法,从而我们可以得出一个结论:就是RenderObject其实是真正做绘制布局相关操作的对象。
下面我们总结下widget,element,renderObject三者之间的关系:


image.png
  • widget是面对开发者使用的配置对象,可以通过widget对实际绘制做相关的配置和描述,比价轻量级
  • element 是Widget在UI树具体位置的一个实例化对象,是实际处理生命周期,UI树位置相关的对象
  • RenderObject是实际布局和绘制的对象
    其中widget跟Element是一一对应,但是跟RenderObject并非一一对应,在实际开发中,一般renderObject要少。
    (在这里我们也可以思考下,如果没有Widget直接将Element暴漏出去供大家使用会有什么问题呢,少了一层结构会不会更简单呢,为啥要设计成这种结构呢?)
    知道了三者之间关系那么我们下面我们继续针对flutter这种架构方式是如何做到优化的?
    我们在生命周期篇中有提到在创建或者更新时会执行elemnet的如下方法:
class ComponentElement extends Element{

@override
void performRebuild() {
………
Widget build;

build = build();

………
_child = updateChild(_child, build, slot);
…………
}
}

接下来我们着重看下updateChild方法:

 Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
    if (newWidget == null) { /// 如果新的widget是null
      if (child != null) ///child 不是null,那么就将该elemnt从element tree上移除
        deactivateChild(child);
      return null;
    }

///如果新的widget不是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) { // 两个widget谁同一个的话
        // We don't insert a timeline event here, because otherwise it's
        // confusing that widgets that "don't update" (because they didn't
        // change) get "charged" on the timeline.
        if (child.slot != newSlot) /// 如果位置不一样更新element tree的位置
          updateSlotForChild(child, newSlot);
        newChild = child;
      } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) { 
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        final bool isTimelineTracked = !kReleaseMode && _isProfileBuildsEnabledFor(newWidget);
        if (isTimelineTracked) {
          Map debugTimelineArguments = timelineArgumentsIndicatingLandmarkEvent;
          assert(() {
            if (kDebugMode) {
              debugTimelineArguments = newWidget.toDiagnosticsNode().toTimelineArguments();
            }
            return true;
          }());
          Timeline.startSync(
            '${newWidget.runtimeType}',
            arguments: debugTimelineArguments,
          );
        }
        child.update(newWidget);
        if (isTimelineTracked)
          Timeline.finishSync();
        assert(child.widget == newWidget);
        assert(() {
          child.owner!._debugElementWasRebuilt(child);
          return true;
        }());
        newChild = child;
      } else { // 如果完全不一样
        deactivateChild(child);
        assert(child._parent == null);
        // The [debugProfileBuildsEnabled] code for this branch is inside
        // [inflateWidget], since some [Element]s call [inflateWidget] directly
        // instead of going through [updateChild].
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else { // 之前的widget就是null,那么就新inflate一个
      // The [debugProfileBuildsEnabled] code for this branch is inside
      // [inflateWidget], since some [Element]s call [inflateWidget] directly
      // instead of going through [updateChild].
      newChild = inflateWidget(newWidget, newSlot);
    }

    assert(() {
      if (child != null)
        _debugRemoveGlobalKeyReservation(child);
      final Key? key = newWidget.key;
      if (key is GlobalKey) {
        assert(owner != null);
        owner!._debugReserveGlobalKeyFor(this, newChild, key);
      }
      return true;
    }());

    return newChild;
  }

这里着重看下canUpdate,后面开发对理解widget的更新有帮助

/// Whether the `newWidget` can be used to update an [Element] that currently
  /// has the `oldWidget` as its configuration.
  ///
  /// An element that uses a given widget as its configuration can be updated to
  /// use another widget as its configuration if, and only if, the two widgets
  /// have [runtimeType] and [key] properties that are [operator==].
  ///
  /// If the widgets have no key (their key is null), then they are considered a
  /// match if they have the same type, even if their children are completely
  /// different.
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }

下面我们用张表来总结上面更新的各种情况:


image.png

到这里我们基本把widget,element,renderObject三者的关系,以及如何优化的做了一定的分析。你以为flutter就只做了这些吗,只做了这些就称得上是高效能的UI框架了吗?后面我们会继续分析flutter的layer以及fluttet渲染相关内容。

你可能感兴趣的:(flutter 三棵树 widget,element,renderObject)