Flutter从源码分析setState 的时候到底发生了什么?
Posted 一叶飘舟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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
实际上是其接口实现类或子类。往上溯源,发现整个的类层级是下面这样的,其中 Element
、ComponentElement
都是抽象类,而 markNeedsBuild
方法是在 Element
抽象类定义的。而对于 Element,官方的定义为:
An instantiation of a Widget at a particular location in the tree. —— 在渲染树中的 Widget 实例化对象。
可以理解为Element
是将 Widget
配置和渲染树做桥接的对象,也就是实际的渲染过程更多的是由 Element
来控制的。
上面的图我们Element的关键属性和方法列出来的。
-
_depth
属性:元素在组件树中的层级,根节点的该值必须大于0。 -
_sort
方法:比较两个Element
元素a和 b的层级,层级值(_depth
)越大,层级越深,显示的层也就越靠前。 -
_parent
:父节点元素,可能为空。 -
_widget
:配置元素的组件配置(其实是Widget
对象,Widget
本身是渲染元素的配置参数,并不是真正渲染的元素)。 -
_owner
:管理元素声明周期的对象。 -
_lifecycleState
:生命周期状态属性,默认是initial
状态。 -
获取
renderObject
的get
方法:会递归调用返回元素及其子元素中需要渲染的对象(子元素是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
)更改元素时,会调用该方法。要求新的配置类型和旧的保持一致。detachRenderObject
和attachRenderObject
:分别对应从组件树移除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
,也就是父节点上的操作——当一个子元素不再属于父元素构建的渲染树时,就会加入到不活跃的元素列表中。
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
更新子组件有三种组合。而我们这里_child
和 built
肯定不为空,那么关键就在于 built
(Widget
对象)的 canUpdate
是否为 true
。这个方法在 Widget 类定义:
static bool canUpdate(Widget oldWidget, Widget newWidget)
return oldWidget.runtimeType == newWidget.runtimeType &&
oldWidget.key == newWidget.key;
注释说明是如果 Widget
的 key
没有设置(一般不推荐给组件设置 key),那么两个组件的 runtimeType
一致就可以更新。因此,实际上大部分情况下返回的都是 true
。我们调试更新代码结果也是一样,最终走到的是Element
的 updateChild
的这个分支:
// ...
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
——除非,这个组件真的很简单,而且下级组件没有或者很少。
以上是关于Flutter从源码分析setState 的时候到底发生了什么?的主要内容,如果未能解决你的问题,请参考以下文章
Flutter:调用 setState() 方法后不显示 SnackBar
Flutter - 从 websocket 消息动态创建小部件 - 未处理的异常:在构造函数中调用 setState()