likes
comments
collection
share

Flutter源码阅读(1)-Widget,Element,RenderObject树的构建和更新流程

作者站长头像
站长
· 阅读数 1

前言

最近在做Flutter项目,写代码的时候一直在想一个问题,为什么Flutter会用这种嵌套的方式去写代码。为什么不像原生开发那样,将一个View定义为一个属性,然后添加到父View上。

带着这个疑问,去看了Widget的源码(定义在framework.dart里),也顺道发现了其他的问题

  1. Widget,Elemnet,RenderObject的三棵树的关系,以及三棵树的构建和更新流程
  2. 为什么通过setState方法可以去更新状态
  3. GlobalKey是如何实现的
  4. ParentData是什么东西
  5. 如何通过InheritedWidget在Widget间传递数据

下面先从Widget的构建,更新流程讲起,顺便也引出一些其他的问题的答案

Tips:

  1. 如果不想看代码,可以直接看下方小节末的流程图,可以了解到树的构建和更新流程
  2. 下方的树如果没有指明是什么树,一般都是指Widget,Elemnet,RenderObject三棵树
  3. 下方代码块中用...代替的一般都是断言和调试代码,因为篇幅限制,这里就省略掉,但是建议在开发的时候可以详细去看一下,因为这有助于了解调试信息

一 概念说明

Widget,State,Elemnet,RenderObject,BuildOwner和BuildContext的概念

Widget 和 State

用于描述UI的配置数据,用于描述Element.同时也是一个生产者,产生Element,RenderObject。

Element

Element是真正的节点。同时也是一个管理者,管理着Widget,State节点,并利用Widget,State构建和更新Element树。同时也管理着RenderObject树的构建和更新

BuildOwner

BuildOwner是一个调度中心。使用Widget管理Element树的构建和更新流程,再由Element树构建Widget和RenderObject树。

在学习Flutter的过程中,我们一般都会接触到。但是我们平时开发的时候一般也只会接触Widget,那么三棵树是怎么构建起来的呢?这里就得说一下BuildOwner.BuildOwner其实是一个调度调度中心,在接收到外部调用时, 会执行下面的流程

  1. 通过Widget去产生一个Element。然后BuildOwner会绑定这个Element.
  2. 在绑定Element过程中,Element会调用Widget的build方法或是State中的build方法去产生一个子Widget
  3. 这个子Widget又会产生一个子Element。并去绑定这个子Element,绑定子Element B的时候,会把子Element中parent指向步骤2中的Element。绑定时又会走到步骤2 通过步骤2,3的不断重复,就会从上往下的去创建Element节点,从而构建Element树。在这个流程里,Widget和Element是一对一的。每产生一个Widget就会对应一个Element.

RenderObject

RenderObject是渲染在屏幕上的对象,类似于iOS开发中的UIView。持有一个用于绘制的Layer。

在绑定Element的时候,如果有需要,会调用widget去创建一个RenderObject,Element会持有这个RenderObject。注意这里并不是每个Element都会拥有一个RenderObject。因为不是每个element都会渲染在屏幕上。有些Element只是用来描述这个RenderObject是大小,位置,有些Element是用来存放依赖关系的。

当沿着Element树往下构建的时候,如果出现了持有RenderObject的子Element,子Element中的RenderObject的parent属性就会指向上一级的RenderObject。所以构建出来的树有可能是下图那样的

Widget树和Element树一一对应,但是Element树和RenderObject不是一一对应。

几个角色关系如下

Flutter源码阅读(1)-Widget,Element,RenderObject树的构建和更新流程

为了理解构建和更新流程,我们从源码一步步地看这个流程

二 树的构建过程

构建流程

从main函数开始,会调用runApp()方法,并传入一个widget,这个runApp(Widget app)的实现如下

  WidgetsFlutterBinding.ensureInitialized()
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();

我们可以看到,其中调用了WidgetsFlutterBinding的scheduleAttachRootWidget方法,WidgetsFlutterBinding是什么我们我们往后再讲,不在这篇文章的范围内,看一下scheduleAttachRootWidget(Widget rootWidget)这个方法,这个方法里调用了attachRootWidget方法,这个attachRootWidget如下

 void attachRootWidget(Widget rootWidget) {
    _readyToProduceFrames = true;
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget,
    ).attachToRenderTree(buildOwner, renderViewElement as RenderObjectToWidgetElement<RenderBox>);
  }

attachRootWidget(Widget rootWidget)里面主要是用传入的rootWidget创建一个RenderObjectToWidgetAdapter对象并调用RenderObjectToWidgetAdapter对象的attachToRenderTree方法 attachToRenderTree方法的实现如下

void updateRenderObject(BuildContext context, RenderObject 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);
      });
      SchedulerBinding.instance.ensureVisualUpdate();
    } else {
      element._newWidget = this;
      element.markNeedsBuild();
    }
    return element;
  }

如果传入的elemnt为空,则去创建一个新的RenderObjectToWidgetElement。如果不为空,则会调用elemnt的markNeedsBuild方法。 _elemnt对象在正常像调用runApp(MyApp())时一般都为null,非null的情况一般是在更新widget的时候才会出现,更新widget的情况在下一小节讨论。这里先讨论一下初始构建的情况

在初始构建过程,会调用owner的下面这个方法

buildScope(Element context, [VoidCallback callback])

前面也提到,BuildOwner是一个调度中心,主要是负责调用Element去处理树的构建和更新流程,其主要是在buildScope这个方法处理,因为buildScope这个方法设计到树的更新流程,也留到下一个小节再讲。 再看下上方调用buildScope方法中传入了一个callback回调,如下

     () {
        element.mount(null, null);
      }

这个callback会在buildScope方法进行具体的处理前执行,也就是说在buildScope进行具体的处理前会先调用element.mount(null, null);

这个mount方法是用来将一个Elemnt绑定到Elemnt树中的。那么是如何绑定的呢?先看下Element类中mount方法的实现

mount方法

void mount(Element parent, dynamic newSlot) {
    ...
    _parent = parent;
    _slot = newSlot;
    _depth = _parent != null ? _parent.depth + 1 : 1;
    _active = true;
    if (parent != null) // Only assign ownership if the parent is non-null
      _owner = parent.owner;
    final Key key = widget.key;
    if (key is GlobalKey) {
      key._register(this);
    }
    _updateInheritance();
   ...
  }

Element类中的mount方法只做了几个简单的事情

  1. 更新_parent值为传入的值
  2. 更新_slot,_depth,_activie。_slot是一个位置标记,_depth是该Elemnt在Elemnt树中的深度,_activit是指该Elemnt的状态
  3. 更新这个Elemnt的owner为父Elemnt的owner。因为管理树只需要一个BuildOwner就可以了,根Elemnt的owner属性会被赋予一个BuildOwner,子Element只要使用这个值就可以了
  4. 注册global key,更新依赖

对于第4点,因为涉及到更新流程,放在第三小节去讲述。

对于第一点,将传入的parent赋值到Elemnt的_parent属性中,代表该Elemnt的父节点是parent对应的Elemnt。如果parent为null,那基本上这个就是一个根节点。

从mount方法可以看出,Elemnt类中在绑定过程更新了父节点的引用,每一个Elemnt都知道父节点是谁。但是Elemnt的子节点呢?怎么去构建子节点呢?

首先得明确一点的是,对于Element,RenderObjct树中,除了根节点,每一个节点都有父节点,所以对于Element,RenderObjct这两个基础类中,都会有一个parent的属性,去指明谁是它的父节点。

但Element,RenderObjct树中不是每一个节点都会有子节点,所以在设计中能看的出来,Widet,Element,RenderObjct这三个基础类都没有子节点的医用。都是交由它们的子类去实现。

Elemnt中有两个大的子类,一个是ComponentElement,对应着不是直接渲染在屏幕上的Elemnt,用于组合其他的Element,一个是RenderObjctElemnt,对应着渲染在屏幕上的Elemnt。

ComponentElemnt类的mount方法

在ComponentElemnt类及其子类中,mount方法的实现如下

void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _firstBuild();
  }

可以看出,额外的调用了_firstBuild方法,_firstBuild里的实现很简单,就是调用了Element类中的rebuild()方法,rebuild()实现如下

 void rebuild() {
    assert(_debugLifecycleState != _ElementLifecycle.initial);
    if (!_active || !_dirty) return; //如果是非活跃状态或是非dirty状态,则直接返回
    ...
    performRebuild(); //调用performRebuild方法
   ...
  }

rebuild()方法里,先判断是否是处于活跃且不为dirty的状态,如果不是,则不执行任何操作。这样可以。如果是,则调用performRebuild()方法,ComponentElemnt中的performRebuild()实现如下

void performRebuild() {
   ...
    Widget built;
    try {
     ...
      built = build(); //执行build方法
      ...
    } catch (e, stack) {
      _debugDoingBuild = false;
      built = ErrorWidget.builder(
        _debugReportException(
          ErrorDescription('building $this'),
          e,
          stack,
          informationCollector: () sync* {
            yield DiagnosticsDebugCreator(DebugCreator(this));
          },
        ),
      );
    } finally {
      // We delay marking the element as clean until after calling build() so
      // that attempts to markNeedsBuild() during build() will be ignored.
      // 为了防止在build方法期间执行markNeedsBuild造成影响,所以这里在build方法以后才恢复状态
      _dirty = false;
		...
    }
    try {
      //第一次_child为空,则创建一个新的element
      //更新时候_child不为空
      _child = updateChild(_child, built, slot);
      ...
    } catch (e, stack) {
      built = ErrorWidget.builder(
        _debugReportException(
          ErrorDescription('building $this'),
          e,
          stack,
          informationCollector: () sync* {
            yield DiagnosticsDebugCreator(DebugCreator(this));
          },
        ),
      );
      _child = updateChild(null, built, slot);
    }
	...
  }

这段代码做了两个事情,第一个是调用build()方法去得到一个Widget,第二个是用得到的Widget去产生一个Elemnt,并更新Element树。 我们先看下build()方法, ComponentElemnt类中的build()是一个空的方法,具体的实现交由子类去做, ComponentElemnt有三个子主要的子类,我们常用的类基本都继承这三个类,我们看下这三个类中build()方法的实现

  1. StatelessElement
Widget build() => widget.build(this);
  1. StatefulElement
Widget build() => _state.build(this);
  1. ProxyElement
 Widget build() => widget.child;

对于StatelessElement和StatefulElement就是调用我们开发中直接接触到的

Widget build(BuildContext context);

对于这三个类,其实都是为了获得一个widget.然后这个拿这个widet去调用Elemnt中的updateChild方法 而这个updateChild方法,就是framework.dart的最重要的方法了,贯穿整个Element树的构建和更新流程. updateChild方法的定义如下

Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    if (newWidget == null) {
    //1 移除Element
      if (child != null) deactivateChild(child);
      return null;
    }
    Element newChild;
    if (child != null) {
    //2 更新Element
      ...
      if (hasSameSuperclass && child.widget == newWidget) {
        //2.1 有相同的widget
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot); //如果插口位置不一致,则更新插口位置
        newChild = child;
      } else if (hasSameSuperclass &&
          Widget.canUpdate(child.widget, newWidget)) {
        //2.2 widget不是同一个,但是canUpdate不是返回yes,重用elemnt,更新element的widget
        if (child.slot != newSlot) updateSlotForChild(child, newSlot);
        child.update(newWidget);
        ...
        newChild = child;
      } else {
        //2.3 element和widget都不是同一个,在将element变为deactivate并增加一个新的element
        deactivateChild(child);
        ...
        newChild = inflateWidget(newWidget, newSlot);
      }
    } else {
     //3 创建或是从GloabalKey中重用一个Element
      //根据newwidget的值判断是否需要返回一个新的element
      newChild = inflateWidget(newWidget, newSlot);
    }
   ...
    return newChild;
  }

因为这里设计到了很多widget树更新的逻辑,我们在讲到更新流程的时候再回过头来去说这个方法。当我们首次构建的时候,我们创建了widget,但是还没创建Element,所以此时widget不为null,child为null。这时候会直接走到inflateWidget(newWidget, newSlot);这个方法里面。inflateWidget的定义如下

Element inflateWidget(Widget newWidget, dynamic newSlot) {
    ...
    final Key key = newWidget.key;
    if (key is GlobalKey) {
      final Element newChild = _retakeInactiveElement(key, newWidget);
      if (newChild != null) {
        //如果存在globalkey而且已经纳入到 inactive elements中,则拿出来重用,否则创建新的
        ...
        newChild._activateWithParent(this, newSlot);
        final Element updatedChild = updateChild(newChild, newWidget, newSlot);
        assert(newChild == updatedChild);
        return updatedChild;
      }
    }
    final Element newChild = newWidget.createElement(); //创建新的element
    ...
    newChild.mount(this, newSlot); //对新的element进行mount
    ...
    return newChild;
  }

这个方法中,首先处理GloabalKey相关的逻辑(第3小节会说到),然后调用widget的createElement()方法去创建一个Elemnt。创建完Element方法后,会对该Element调用mount()方法。

这时候又会调用Elemnt的mount方法,但是注意,这个是子Element的mount方法。当调用这个子Element的mount()方法的时候,又会重新走一遍绑定流程,直到没有子节点为止。调用流程如下

mount -> firstBuild -> rebuild - > performRebuild -> updateChild -> inflateWidget -> mount

如果看到这里,觉得调用流程有点复杂,可以先不用记住流程,后面会给出构建的流程图

RenderObjctElemnt的mount方法

在RenderObjctElemnt中,mount方法的定义如下

@override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    ...
    attachRenderObject(newSlot); //绑定render object
    _dirty = false;
  }

这里调用了RenderObjctElemnt的attachRenderObject方法为该RenderObjctElemnt去绑定一个RenderObjcet,RenderObjctElemnt 中 attachRenderObject的实现如下

 @override
  void attachRenderObject(dynamic newSlot) {
    _slot = newSlot;
    _ancestorRenderObjectElement = _findAncestorRenderObjectElement();
    _ancestorRenderObjectElement?.insertChildRenderObject(
        renderObject, newSlot);
    final ParentDataElement<ParentData> parentDataElement =
        _findAncestorParentDataElement();
    if (parentDataElement != null) _updateParentData(parentDataElement.widget);
  }

attachRenderObject方法中,先调用_findAncestorRenderObjectElement找到是RenderObjctElement类型的Element.赋值给_ancestorRenderObjectElement。再调用insertChildRenderObject把对应的RenderObjct插入到_ancestorRenderObjectElement的子RenderObelct列表中。这样做是因为RenderObjct树不是和Element树一一对应的。RenderObjct是会渲染屏幕上的节点,不是所有的Element都会渲染在屏幕上。所以为了保证renderObjct树正确的构建,需要调用_findAncestorRenderObjectElement找到有RenderObjct的上级Element节点,忽略与渲染无关的节点,找到父RenderObject。

可以看出RenderObjctElemnt中的mount方法中只调用了attachRenderObject去绑定由widget产生的RenderObjct,并没有对子Element节点的处理。那么对于RenderObjctElemnt,是怎么构建子树的呢?

RenderObjctElemnt是一个渲染的节点。但不是每一个渲染的节点都会有子节点,所以对于子节点的构建,交由具体有子节点的子类去实现。RenderObjctElemnt有两个子类是有子节点的,分别是有一个子节点的SingleChildRenderObjectElement和多个子节点的MultiChildRenderObjectElement。

SingleChildRenderObjectElement的mount方法如下(其中的child就是创建widget中的child)

  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _child = updateChild(_child, widget.child, null);
  }

MultiChildRenderObjectElement的mount方法如下(其中的children就是创建widget中的childrenl列表)

void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _children = List<Element>(widget.children.length);
    Element previousChild;
    for (int i = 0; i < _children.length; i += 1) {
      final Element newChild = inflateWidget(
          widget.children[i], IndexedSlot<Element>(i, previousChild));
      _children[i] = newChild;
      previousChild = newChild;
    }
  }

从上面的代码可知其中的updateChild,会调用inflateWidget方法。所以对于带有的子节点的RederObjctElement的子类,构建过程中都会去调用inflateWidget方法。inflateWidget方法中会产生一个子节点,再调用子节点的mount方法。流程如下图

这里总结一下树的构建流程

当调用runAPP()方法开始,调用根Widge的createElement方法,得到一个根Element,调用根Elemnt的mount方法,然后不断的往下去创建子Widget,再调用子Widget的createElement方法创建子Elemnt,再调用子Elemnt的mount方法,一直往下创建节点,直到没有子节点的叶子节点为止。如下图

Flutter源码阅读(1)-Widget,Element,RenderObject树的构建和更新流程

三 树的更新过程

更新流程

当构建完树后,是如何对树进行增加,删除,修改树中节点的操作呢?前面提到过,BuildOwner是负责处理构建,更新树的管理者。它与树的角色大概如图所示。

更新树的流程大概如下:BuildOwner负责一个调度的作用,当我们操作树的时候,通知BuildOwner需要更新。当框架知道需要更新的时候,会调用BuildOwner的buildScope方法。buildScope方法中会对需要更新的Elemnt进行更新。

现在我们先从最熟悉的方法,也就是State的setState方法开始讲起。setState方法定义如下

 void setState(VoidCallback fn) {
    ...
    final dynamic result = fn() as dynamic;
   ...
    _element.markNeedsBuild();
  }

可以看出,这个方法先是我们调用了传入的方法,执行方法后,就调用了State中的_element的markNeedsBuild()方法,markNeedsBuild()方法是将一个Element标记为需要更新。方法的定义如下

void markNeedsBuild() {
    ...
    if (!_active) return;
    ...
    _dirty = true;
    owner.scheduleBuildFor(this); //加入build owner的rebuild计划
  }

markNeedsBuild方法首先判断一个Elemnt是否是_active,如果不是,则不做任何处理,因为一个非活跃的状态不会显示在界面上,所以不需做处理。然后将其_dirty值设置为true,标记这个Element 需要更新。然后调用BuildOwner的scheduleBuildFor方法,并传入需要更新的Element。BuildOwner的scheduleBuildFor定义如下

 void scheduleBuildFor(Element element) {
    ...
    if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
      _scheduledFlushDirtyElements = true;
      onBuildScheduled(); // 通知Engine 在下一帧需要做更新操作;
    }
    _dirtyElements.add(element);
    ...
  }

方法里的_scheduledFlushDirtyElements表明是否是在更新过程中,可以看到,这里先判断_scheduledFlushDirtyElements是否为true,就是不在更新过程中,则调用onBuildScheduled通知框架需要进行更新树。 然后将刚才传入的element添加到BuildOwner的_dirtyElements中。这个_dirtyElements是一个列表,存储着需要所有更新的Elemnt。

当框架收到需要更新树的信息后,就会调用BuildOwner的buildScope()方法,前面在构建过程中提到过这个buildScope方法,但是没有细说,在这里我们看一下buildScope方法的实现

void buildScope(Element context, [VoidCallback callback]) {
    if (callback == null && _dirtyElements.isEmpty) return;
    ...
    //deng 问题 这个timeline是什么
    Timeline.startSync('Build',
        arguments: timelineArgumentsIndicatingLandmarkEvent);
    try {
      _scheduledFlushDirtyElements = true;
      if (callback != null) {
        ...
        
        try {
          callback(); //callback先执行
        } finally {
         ...
        }
      }
      ...
        try {
          _dirtyElements[index].rebuild(); //进行rebuild
        } catch (e, stack) {
          ...
        }
        ...
      }
     ...
    } finally {
      ...
      _dirtyElements.clear(); 
      ...
    }
    ...
  }

buildScope()方法中先是执行了callback方法,对于首次构建的流程,这个callback方法就是调用根Elemnt的mount方法进行构建。

然后这个方法主要就做了一件事,就是从_dirtyElements列表中取出每一个需要更新的Element,然后对Elemnt调用rebuild()方法,再清空_dirtyElements列表,标志着这一轮更新完成。

从前面的构建流程可知, rebuild方法会调用performRebuild方法。performRebuild对于不同的Element子类,有着不一样的额外的实现。

首先对于RenderObjctElemnt类,只会调用updateRenderObject去更新RenderObjct对象,不会涉及任何的子节点的更新.那这里就有疑问了,该怎么对有子节点的RenderObjctElemnt进行更新子树呢?这里先留一下小疑问,等讲完ComponetElemnt然后再解答。

对于ComponetElemnt及其子类,调用performRebuild方法会调用updateChild方法。前面在构建过程中,就贴出了performRebuild和updateChild这两个方法,这里就不再重复去贴代码。在这里重点说一下updateChild这个方法

updateChild

updateChild中传入了(Element child, Widget newWidget, dynamic newSlot) 三个参数。child代表的是该ComponetElemnt的子节点,newWidget是子节点对应的Widget,newSlot是子节点的位置信息

在执行updateChild过程中根据传入的参数做了以下的一些处理。

  1. 如果new widget为空,但是element不为空(也就是原有的widget被删除了)。首先deactivateChild(child),如果child不为空,则解绑child的renderobjce,并添加到build owner中的_inactiveElements列表中,并返回函数。deactivateChild(child会把child添加到BuildOwner_inactiveElements中)

  2. 如果new widget非空,child不为空

  • 2.1 如果传入的widget和原来的widget是同一个,但是slot不一样,则调用updateSlotForChild更新位置

  • 2.2 如果不是同一个widget,则判断widget的canUpdate是否是true,如果是true的话(代表element的可以重用),先判断slot是否和原来的slot相等,不相等,则调用updateSlotForChild更新位置。然后调用elemnt的update方法进行更新。

  • 2.3 如果不符合上面2.1,2.2的情况(则代表element不可重用,newWidget需要用例外一个新的element),则调用deactivateChild(child)方法,并调用inflateWidget(newWidget, newSlot)产生新的child

  1. newWidget为空,child为空。则表明该Wiget还不存在Element,则调用inflateWidget去创建一个新的Element。

上面的1,2,3分别代表着删除,修改,增加 Elemnt子节点的三种情况。当对一个element(ComponetElemnt及其子类的实例)调用markNeedsBuild方法的时候,会调用到updateChild方法去更新该element。

对于步骤1,当一个Widget不再使用的时候,会调用deactivateChild方法,这个方法会把对应的Element放入到BuildlOwner的_inactiveElements列表中,如果Element被再次使用到(如使用了GlobalKey),就会从_inactiveElements列表中移除。如果没有被再次用到,再一次更新树的时候就会被销毁。

对于步骤2.2,elemnt的update方法只会简单的设置widget。具体的实现由各个子类实现。这一步可以buid方法可以沿下更新树

如StatelessElement会调用rebuild方法,RenderObjectElemnt会更新renderobjct。像SingleChildRenderObject和MutilChildRederObjectElemnt等有子elemnt的还会调用updateChild方法(MutilChildRederObjectElemnt 是调用updateChildren,但是updateChildren是对updateChild的一个包装,对传入的列表逐个调用updateChild)更新子elemnt,

更新流程下图所示

Flutter源码阅读(1)-Widget,Element,RenderObject树的构建和更新流程

在说RenderObjctElemnt的时候,留了一个小疑问:如何更新有子节点的RenderObjcetElement的子树。

其实在开发过程中,我们是不会直接调用RenderObjcetElement的markNeedsBuild方法的。就拿Stack这个Wigdet来说。Stack继承自MultiChildRenderObjectWidget。MultiChildRenderObjectWidget对应的是MutilChildRederObjectElemnt。

但会发现,Stack是没有setState方法的,也就说不会直接调用markNeedsBuild方法。Stack一般是被ComponetElemnt及其子类对应的Widget(如StatefuleWidget)所包裹着。当对应的Widget更新的时候,就会调用markNeedsBuild方法。从而进入updateChild更新子树,从而完成对子树的更新。

假设你要在更新流程中,保留Stack,只删除Stack里的一个子节点,会走到updateChild的步骤2.2中,调用Stack对应的MutilChildRederObjectElemnt的update()方法。utilChildRederObjectElemnt的update()如下

  @override
  void update(MultiChildRenderObjectWidget newWidget) {
    super.update(newWidget);
	...
    _children = updateChildren(_children, widget.children,
        forgottenChildren: _forgottenChildren);
    _forgottenChildren.clear();
  }
}

可以看到,update方法调用了updateChildren方法,updateChildren这个方法会遍历子节点,对每一个子节点调用updateChild方法,从而完成子树的更新。

综合上面的流程可以看出,标记一个Element需要更新,其实就是调用markNeedsRebuild方法标记Element需要更新并通知到Flutter框架需要更细。当框架更新该Element的时候,会调用updateChildren方法向下递归更新子树,直到叶子节点为止。

三GlobalKey,ParentData,依赖更新等机制的实现原理

GloabalKey

在开发的过程中,在一个Widget可以通过GloabalKey去找到另外一个Widget,这是怎样实现的呢?

当我们调用GlobalKey的currentWidget去获取一个Widget的时候,调用的是下面的方法

Widget get currentWidget => _currentElement?.widget;

_currentElement方法定义如下

Element get _currentElement => _registry[this];

可这个_registry[this]是什么呢?

看一下GlobalKey的定义,会发现GlobalKey中这个类有一个类属性

static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};

这个_registry以GlobalKey为键,以Element为值。他的作用是存储Widget的key是GloabalKey的Element

前面介绍到,在绑定Element的时候会调用mount方法 mount方法中还会判断对应的widget的key是否是GlobalKey,如果是GlobakKey,就会调用

key._register(this);

_register是GlobakKey中的一个方法

void _register(Element element) {
    _registry[this] = element;
  }

这个方法将GloabakKey和Element作为健值对存放在GlobalKey类中的_registry中。这样就完成一个GlobalKey和Element的绑定

_registry[this] 就是从GlobakKey类中的_registry中取出这一个GlobakKey对应的Element

这样看来,当我们调用GlobalKey的currentWidget去获取一个Widget的时候,实际上就是从GlobalKey这个类中的_registry取出Element,并返回Element的Widget。

只要GlobalKey对应的Element还存在,就可以通过获取到对应的Widget。

流程大概如下

ParentData

在RenderObjct树中,父RenerObjct负责子RenderObjct布局, 每一个RenderObjct中都有一个parentData属性,用于向父RenderObject提供布局信息,父RenderObject根据子RenderObjct的parentData进行布局。但是这个parentData是什么时候提供的呢?

其实RenderObecjElement通常是不会直接拥有一个RenderObecjElement类型的子节点,通常两个RenderObecjElement中间都会有一个ParentDataElemnt类型的中间节点,这是因为需要给子RenderObecjElement设置parentData的属性,而RenderObjct的parentData信息就是在这里设置的。

就拿Stack为列,Stack通常都不会直接嵌套一个的Elemment是RenderObjctElementl类型的子Widget。通常都会嵌套一个Position。而Positio继承自ParentDataWidget,而ParentDataWidget对应的Element就是ParentDataElemnt类型的。而用Stack和Position构建的Widget树对应的Element树如下图所示。

如果上图的Widget D对应的Element是RenderObjctElement类型的,按照树的构建流程,在绑定这个Widget的Element的时候,会调用attachRenderObject方法,attachRenderObject方法的实现在前面已经贴出来了。可以看到,除了构建RenderObjct树外,还做了寻找上级ParentData的操作 如下

final ParentDataElement<ParentData> parentDataElement =
        _findAncestorParentDataElement();
    if (parentDataElement != null) _updateParentData(parentDataElement.widget);

如果存在是ParentDataElement类型的上级Element,则调用_updateParentData方法并传入parentDataElement的widget. _updateParentData的方法如下

void _updateParentData(ParentDataWidget<ParentData> parentDataWidget) {
    bool applyParentData = true;
    ...
    if (applyParentData) parentDataWidget.applyParentData(renderObject);
  }

这个_updateParentData中就做了一个事情,就是调用传入的Elementd的widget的applyParentData方法,并传入Elementd的renderObjct.

回到刚才的例子,在绑定Widget D的Element D得时候,Element D 会向上查找,得到ParentDataElement B,并通过ParentDataElement B找到对应的Positon B,调用Position B的applyParentData方法。Position中的applyParentData方法如下

 @override
  void applyParentData(RenderObject renderObject) {
    assert(renderObject.parentData is StackParentData);
    final StackParentData parentData =
        renderObject.parentData as StackParentData;
    bool needsLayout = false;

    if (parentData.left != left) {
      parentData.left = left;
      needsLayout = true;
    }

    if (parentData.top != top) {
      parentData.top = top;
      needsLayout = true;
    }

    if (parentData.right != right) {
      parentData.right = right;
      needsLayout = true;
    }

    if (parentData.bottom != bottom) {
      parentData.bottom = bottom;
      needsLayout = true;
    }

    if (parentData.width != width) {
      parentData.width = width;
      needsLayout = true;
    }

    if (parentData.height != height) {
      parentData.height = height;
      needsLayout = true;
    }

    if (needsLayout) {
      final AbstractNode targetParent = renderObject.parent;
      if (targetParent is RenderObject) targetParent.markNeedsLayout();
    }
  }

这个方法中,首先会取到renderObject(在这个例子中就是 Element D的renderObjcet)的parentData,并根据Positon B的情况去更新parentData的数据和判断是否需要更新,如果需要更新,则调用renderObject的父节点,并告诉父节点需要重新布局,从而更新这个RenderObjct

流程如下

找到上级的ParentDataElement -> 调用ParentDataElement的widget的applyParentData方法 ->设置parentData ->通知父RenderObjct更新

Element树和RenderObjct树是紧密相连的,随着Element树的构建和更新,RenderObjct树也会随着改变。

这里虽然是只拿了Stack,Position做例子,但是RenderObjct树中的节点基本上都是依靠这一套机制实现布局信息的传递的。

依赖和更新

开发过程中,可以通过Theme.of(context)这样的方法去获取主题相关的设置。当我们使用了主题信息,而主题信息改变的时候,我们的Widget也会重新执行build方法。其实这个也是依赖于Element树的构建和更新去实现的。可以参照InheritedElement的实现。 这个点留个坑位,分析Provider的时候一起分析。

总结

从上面Element的构建和更新的流程中,我们可以看到其实都是通过递归调用,一级一级的往下去对节点进行修改。Flutter代码中会以嵌套的方式去写Widget,可以保证每个Widget都是最新的,从而确保Element树是正确的。

其实有一个地方需要注意的是,每一个Element和RenderParent都会有一个parent属性,用于指明父节点是谁,但是Widget是没有这个父节点的,也就是说Widget是没有上下级关系的。所以严格一点来说,Widget树并不是一个真正的节点树,只是说每一个Element都会对应一个Widget,而且从开发过程看,Widget看起来像是一颗树而已。

Element是核心,整个构建和更新流程是由BuildOwner的buildScope方法触发,由Element实现,Element协调Widget树和RenderObjct树的构建和更新。

结束语

在这一篇文章中主要讲述了Widget,Elemnet,RenderObjct树的构建和更新流程,至于RenderObjct的实现描述不多。 其实RenderObjct中如何去布局,绘制,layer生成,合成对于理解Flutter的界面构建也是一个很重要的部分,篇幅也较大,留给下一篇文章再去讲述