写点什么

源码分析 Flutter 的 setState 过程

作者:岛上码农
  • 2022 年 5 月 16 日
  • 本文字数:4556 字

    阅读完需:约 15 分钟

源码分析 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 之外进行调用。


@protectedvoid 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 找找看,代码如下:


@overridevoid performRebuild() {  if (_didChangeDependencies) {    state.didChangeDependencies();    _didChangeDependencies = false;  }  super.performRebuild();}
复制代码


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


@overridevoid 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——除非,这个组件真的很简单,而且下级组件没有或者很少。



发布于: 13 小时前阅读数: 13
用户头像

岛上码农

关注

用代码连接孤岛,公众号@岛上码农 2022.03.03 加入

从南漂到北,从北漂到南的业余码农

评论

发布
暂无评论
源码分析 Flutter 的 setState 过程_flutter_岛上码农_InfoQ写作社区