Loading...
墨滴

岛上码农@公众号同名

2021/11/03  阅读:24  主题:橙心

源码分析 Flutter 的 setState 过程

前言

上一篇我们对比了 setStateModelBinding这两种状态管理的区别,从结果来看,setState 的方式的性能明显低于 ModelBinding 这种使用 InheritedWidget 的方式。这是因为 setState的时候,不管子组件有没有依赖状态数据,都会蒋全部子组件移除后重建。那么 setState 这个过程做了什么事情,会导致这样的结果呢?本篇我们通过 Flutter 的源码来分析一下 setState 的过程。

setState 的定义

我们先来看 setState 的定义,setState 定义在State<T extends StatefulWidget> with Diagnosticable这个类中,也就是 StatefulWidget或其子类的状态类。方法体代码不多,在执行业务代码做了一些异常处理,具体的代码我们不贴了,主要是做了如下处理:

  • 传给setState 的回调方法不能为空。
  • 生命周期校验:组件已经从组件树移除的时候会被 dispose 掉,因此不能在 dispose 后调用 setState。通常这会发生在定时器、动画或异步回调的过程中。这样的调用可能会导致内存泄露。
  • created 阶段和没有装载阶段(mounted)不可以调用 setState,也就是不能在构造函数里调用 setState。通常应该在 initState 之后调用 setState
  • setState 的回调方法不能返回 Future 对象,也就是不能在 setState中执行异步操作,只能是同步操作。如果要执行异步操作应该咋 setState 之外进行调用。
@protected
void setState(VoidCallback fn) {
  // 省略异常处理代码
  _element!.markNeedsBuild();
}

最为关键的就一行代码:_element!.markNeedsBuild(),从函数名称来看就是标记元素需要构建。那么这个_element 又是从哪来的?继续挖!

Element 是什么?

我们来看_element 的定义,_element 是一个 StatefulElement 对象,实际上,我们还发现,在获取BuildContext的时候,返回的也是_element。在获取 BuildContext 的时候注释是这么说的:

The location in the tree where this widget builds ——widget构建的渲染树的具体位置。

BuildContext 是一个抽象类,因此可以推断出 StatefulElement 实际上是其接口实现类或子类。往上溯源,发现整个的类层级是下面这样的,其中 ElementComponentElement 都是抽象类,而 markNeedsBuild 方法是在 Element 抽象类定义的。而对于 Element,官方的定义为:

An instantiation of a Widget at a particular location in the tree. —— 在渲染树中的 Widget 实例化对象。

可以理解为Element 是将 Widget 配置和渲染树做桥接的对象,也就是实际的渲染过程更多的是由 Element 来控制的。

classDiagram
    BuildContext <|.. Element
    DiagnosticableTree <|-- Element
    Element <|-- ComponentElement
    ComponentElement <|-- StatefulElement
    class Element {
        Element(Widget widget)
        +_sort(Element a, Element b)

        -reassemble()
        -markNeedsBuild()
        -get renderObject
        -updateChild(Element? child, Widget? newWidget, dynamic newSlot)
        -mount(Element? parent, dynamic newSlot)
        -unmount()
        -update(covariant Widget newWidget)
        -detachRenderObject()
        -attachRenderObject(dynamic newSlot)
        -deactivateChild(Element child)
        -activate()
        -didChangeDependencies()
        -markNeedsBuild()
        -rebuild()
        -performRebuild()

        -Element? _parent
        -int _depth
        -Widget _widget
        -BuildOwner? _owner
        _ElementLifecycle _lifecycleState
    }

上面的图我们Element的关键属性和方法列出来的。

  • _depth属性:元素在组件树中的层级,根节点的该值必须大于0。

  • _sort方法:比较两个Element元素a和 b的层级,层级值(_depth)越大,层级越深,显示的层也就越靠前。

  • _parent:父节点元素,可能为空。

  • _widget:配置元素的组件配置(其实是 Widget对象,Widget 本身是渲染元素的配置参数,并不是真正渲染的元素)。

  • _owner:管理元素声明周期的对象。

  • _lifecycleState:生命周期状态属性,默认是 initial 状态。

  • 获取renderObjectget 方法:会递归调用返回元素及其子元素中需要渲染的对象(子元素是 RenderObjectElement对象)。

  • reassemble 方法:重新装配方法,只在 debug 阶段会用到,例如热重载的时候就会调用该方法。该方法处理将元素自身标记为需要build外(调用 markNeedsBuild 方法),还会递归遍历全部子节点,调用子节点的 reassemble 方法。

  • updateChild:这是渲染过程的核心方法,通过新的组件配置来更新指定的子元素。这里存在四种组合:- 如果 child 为空的话而 newWidget 不为空,那么就会创建一个新的元素来渲染:

    • 如果 child 不为空,但是 newWidget 为空,那就表明组件配置中已经没有 child 这个元素了,因此需要移除它。
    • 如果二者都不为空,则需要根据 child 的当前是否可以更新(Widget.canUpdate)来处理,如果可以更新,那么使用新的组件配置更新元素;否则我们需要移除旧的元素,并使用新的组件配置创建一个新的元素。
    • 如果二者都为空,那么什么都不做。

返回的结果也分三种情况:

   1. 如果创建了一个新的元素,则返回新构建的子元素。
   2. 如果旧的元素被更新,返回更新后的子元素。
   3. 如果子元素被移除,而没有新的替换的话,返回null。
  • mount方法:在新元素首次被创建的时候调用该方法,按照给定的插入位置(slot)将元素插入给定的父节点。调用该方法后,元素的状态会从 initial 改为 active。这里还会将子元素的层级(_depth)设置为父元素的层级+1。
  • update 方法:当父节点使用新的配置组件(newWidget)更改元素时,会调用该方法。要求新的配置类型和旧的保持一致。
  • detachRenderObjectattachRenderObject:分别对应从组件树移除renderObject 和添加 RenderObject。
  • deactivateChild方法:将子元素加入到不活跃的元素列表,之后再从渲染树中移除。
  • activate方法:状态从inactive 切换到 active 时会调用,属于生命周期函数。注意组件第一次挂载的时候不会调用这个方法,而是 mount 方法。
  • deactivate 方法:状态从 active 切换到 inactive 时会被调用,也就是元素被移入到不活跃列表的时候会被调用。。
  • unmount 方法:状态从 inactive 切换到defunct(不再存在)状态时调用,此时元素将脱离渲染树,并且再也不会在渲染树存在。
  • didChangeDependencies:当元素的依赖发生改变的时候调用,该方法也会调用 markNeedBuild 方法。
  • markNeedsBuild方法:将元素标记为 dirty 状态,以便在渲染下一帧时重建元素。这个方法的核心是做了下面的事情:
_dirty = true;
owner!.scheduleBuildFor(this)
  • rebuild 方法:当元素的 BuildOwner 对象调用 scheduleBuildFor 方法的时候,会调用 rebuild 方法来重建元素。首次装载的时候是在 mount 方法中触发,配置组件更改时会在 build 方法触发。这个方法调用了 performRebuild方法来重建元素。performRebuild是一个有 Element 的字类实现的方法,也就是每个元素具体怎么重建由子类来决定。

内容看着很多,我们来理一下渲染的状态流转,这是一个元素的生命周期的状态图。组件会被移除出现在 deactivate 方法中,而触发 deactivate方法的是一个元素被移入到不活跃元素列表中。将元素移入到不活跃列表的方法是deactivateChild,也就是父节点上的操作——当一个子元素不再属于父元素构建的渲染树时,就会加入到不活跃的元素列表中。

graph LR
    createElement -->  初始化((initial)) 
    初始化((initial))  --mount-->  已装载((mounted))
    已装载((mounted)) --activate--> 活跃((active)) 
    活跃((active)) --deactivate--> 不活跃((inactive))
    不活跃((inactive))--unmount--> 不再存在((defunct))
    不再存在((defunct))--> dispose

performRebuild方法

现在我们知道在 setState 的时候,实际会调用 performRebuild 方法来重新构建组件树,那么 performRebuild 方法做了什么事情?在 Element 中,performRebuild 方法是个空方法,需要子类去实现。因此我们去 StatefulElement 找找看,代码如下:

@override
void performRebuild() {
  if (_didChangeDependencies) {
    state.didChangeDependencies();
    _didChangeDependencies = false;
  }
  super.performRebuild();
}

还得往上找,那就是 ComponentElement 了,终于找着了!

@override
void performRebuild() {
  // 省略调试的代码
  Widget? built;
  try {
    // ...
    built = build();
    // ...
  } catch (e, stack) {
    // ...
  } finally {
    // We delay marking the element as clean until after calling build() so
    // that attempts to markNeedsBuild() during build() will be ignored.
    _dirty = false;
    // ...
  }
  try {
    _child = updateChild(_child, built, slot);
    assert(_child != null);
  } catch (e, stack) {
    // 省略异常处理
  }
  // 省略调试代码
}

这里的关键在于调用了 build 方法和updateChild 方法。其中 通过 built = build()获取了最新的Widget,由于 build 方法重新构建了组件配置,因此会调用对应的 Widget 的构造函数和 build 方法。然后再调用 updateChild 方法更新子元素。如前所述,updateChild 更新子组件有三种组合。而我们这里_childbuilt肯定不为空,那么关键就在于 builtWidget 对象)的 canUpdate 是否为 true。这个方法在 Widget 类定义:

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

注释说明是如果 Widgetkey 没有设置(一般不推荐给组件设置 key),那么两个组件的 runtimeType 一致就可以更新。因此,实际上大部分情况下返回的都是 true。我们调试更新代码结果也是一样,最终走到的是ElementupdateChild 的这个分支:

// ...
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;
}

由此我们可以推断,setState 方法调用后确实会重新构建整个 Widget,但是并不一定会将 Widget 配置的 Element元素树的每一个元素都移除,然后用新的元素替换来重新渲染一遍。实际上我们调试的时候打开 Flutter 的调试工具也可以看到,实际上的Widget 对应的 Element 在点击按钮后并没有发生改变。

总结

虽然setState的调用并没有像 Widget 层那样,在渲染控制层的 Element 那一层重新构建全部element。但是,这并不代表 setState 的使用没问题,首先,像之前篇章说的那样,它会重新构建整个 Widget 树,这会带来性能损耗;其次,由于整个 Widget 树改变了,意味着整棵树对应的渲染层Element对象都会执行 update方法,虽然不一定会重新渲染,但是这整棵树的遍历的性能开销也很高。因此,从性能上考虑,还是尽量不要使用 setState——除非,这个组件真的很简单,而且下级组件没有或者很少。

关注岛上码农
关注岛上码农

岛上码农@公众号同名

2021/11/03  阅读:24  主题:橙心

作者介绍

岛上码农@公众号同名