Flutter-探索setState原理流程
上面的代码,我们可以关注asyncWaitForVsync这个方法,当Flutter调用scheduleFrame()方法后,只最后会调用到这个方法,而这个方法中Flutter这边会向Android的Choreographer对象申请一个帧回调,简单来说就是借用了平台自身的帧回调,当Vsync信号来临的时候,就会调用FlutterJNI的nativeOnVsync逻辑通知Flutter层中。至此,
1. setState的原理剖析
首先在开头给出答案,setState整个流程到底做了什么事情?
- 标记需要重新构建的Element为脏元素并且保存到BuildOwner后向平台申请帧回调,等待重绘
- 帧回调回来后,会遍历脏元素重新构建Widget信息,并且递归其子组件重新构建Widget信息
- 开始重新测量绘制
1.1 标记
我们先看看setState具体做了什么?
void setState(VoidCallback fn) {
///...省略代码
///1.调用方法setState中的方法体,执行控件逻辑
final Object? result = fn() as dynamic;
///2.调用State中的Element,markNeedsBuild方法
_element!.markNeedsBuild();
}
setState里面只做了两件事
- 第一个就是调用方法体,执行对应的逻辑
- 第二就是调用自身Element对象的markNeedsBuild方法,告诉Element我们需要重新构建。
那么我们接着跟一下Element的markNeedsBuild方法
class Element implements BuildContext {
BuildOwner? get owner => _owner;
BuildOwner? _owner;
void markNeedsBuild() {
//...忽略非重点代码
//1.判断当前是否为活跃元素,如果非活跃元素则不需要构建
if (_lifecycleState != _ElementLifecycle.active) {
return;
}
//2.判断当前是否脏元素,如果已经标记为脏元素则意味着之前调用过一次该方法,无须重复调用
if (dirty) {
return;
}
//3.标记脏元素
_dirty = true;
//4.调用owner的scheduleBuildFor方法
owner!.scheduleBuildFor(this);
}
}
在Element中,如果还在活跃状态,则将自身标记为脏元素,并且调用owner对象的scheduleBuildFor方法。
在接着走流程之前,我们首先简单了解一下这个BuildOwner对象。
BuildOwner是一个负责管理Element的类,全局唯一且主要负责dirtyElement、inactiveElement、globalkey关联的element的管理。
class BuildOwner {
///...省略其他代码
//存储inactiveElement。
final _InactiveElements _inactiveElements = _InactiveElements();
//存储dirtyElement,就是那些需要重建的element。
final List<Element> _dirtyElements = <Element>[];
//存储所有有globalKey的
final Map<GlobalKey, Element> _globalKeyRegistry = <GlobalKey, Element>{};
}
当我们了解了BuildOwner的职责后,我们继续深入BuildOwner的scheduleBuildFor方法中看看
class BuildOwner {
//..省略部分构造参数
BuildOwner({ this.onBuildScheduled})
//调度构建用的,从构造器中传进来,本质上指向了了WidgetsBinding中的_handleBuildScheduled方法
VoidCallback? onBuildScheduled;
void scheduleBuildFor(Element element) {
//判断Element是否已经在
if (element._inDirtyList) {
//该标志用于DirtyList中在重建过程中时,判断重建时是否重新发生了新的变化,需要重置重建的深度
_dirtyElementsNeedsResorting = true;
return;
}
//判断是否重建中,如果不在重建中则执行onBuildScheduled()方法体
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled!();
}
//添加到脏列表中
_dirtyElements.add(element);
//标记存在于脏列表中的元素
element._inDirtyList = true;
}
}
我们可以通过注释看到,onBuildScheduled方法体,本质上是指向了WidgetsBinding类中_handleBuildScheduled方法,我们接着看
class WidgetsBinding {
//1.该函数无实际作用
void _handleBuildScheduled() {
//...省略非关键代码
ensureVisualUpdate();
}
//2.判断当前帧阶段是否可以请求帧调度,我们通过注释简单了解每个阶段,就不深入讲解了
void ensureVisualUpdate() {
switch (schedulerPhase) {
//空闲阶段 - 这个阶段没有帧任务执行
case SchedulerPhase.idle:
//帧结束阶段
case SchedulerPhase.postFrameCallbacks:
//关键! - 请求一帧的绘制
scheduleFrame();
return;
// 瞬时任务阶段 - 这个阶段执行 SchedulerBinding.scheduleFrameCallback 添加的回调,一般情况下,这些回调执行动画有关的计算
case SchedulerPhase.transientCallbacks:
//帧中微任务处理阶段 - transientCallbacks 也会产生一些微任务,这些微任务会在这个阶段执行。
case SchedulerPhase.midFrameMicrotasks:
//持续任务处理阶段 -这个阶段主要处理 SchedulerBinding.addPersistentFrameCallback 添加的回调,与 transientCallbacks 阶段相对应,这个阶段处理 build/layout/paint。
case SchedulerPhase.persistentCallbacks:
return;
}
}
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled) {
return;
}
//提前注册好每一帧来临时的回调,包括了帧开始和结束
ensureFrameCallbacksRegistered();
//向当前平台请求帧回调
platformDispatcher.scheduleFrame();
//标记已经申请了帧回调
_hasScheduledFrame = true;
}
void ensureFrameCallbacksRegistered() {
//向platformDispatcher中注册开始帧和结束帧的回调
platformDispatcher.onBeginFrame ??= _handleBeginFrame;
platformDispatcher.onDrawFrame ??= _handleDrawFrame;
}
}
至此,setState的标记流程已经结束,剩下的流程会流转到平台方面,我们可以总结一下,在setState这个流程中核心具体做了2事情。
- 元素标记为脏元素,并且扔到脏元素列表中等待重建。
- 向平台注册帧回调,等待绘制
至此,方法开始退栈,setState标记流程完成,接下来就等待平台触发帧回调,开始后半步,重绘的过程。
1.2 重绘
1.2.1 重绘是如何从平台往下分发的?
下面的例子我以Android为例,跟进一下Flutter是如何向Android申请帧绘制回调的,我们在标记流程中的最后Flutter调用了platformDispatcher.scheduleFrame方法后,会来到Android原生端的VsyncWaiter类中
//进程单例实例类
public class VsyncWaiter {
//......
//一个来自engine触发的回调
private final FlutterJNI.AsyncWaitForVsyncDelegate asyncWaitForVsyncDelegate =
new FlutterJNI.AsyncWaitForVsyncDelegate() {
@Override//当Flutter调用scheduleFrame()方法后,只最后会调用到这个方法
public void asyncWaitForVsync(long cookie) {
//每逢回调回来就向Choreographer post一个绘制VSYNC请求。
Choreographer.getInstance()
.postFrameCallback(
new Choreographer.FrameCallback() {
@Override
public void doFrame(long frameTimeNanos) {
float fps = windowManager.getDefaultDisplay().getRefreshRate();
long refreshPeriodNanos = (long) (1000000000.0 / fps);
//调用FlutterJNI的nativeOnVsync逻辑通知VSYNC
FlutterJNI.nativeOnVsync(
frameTimeNanos, frameTimeNanos + refreshPeriodNanos, cookie);
}
});
}
};
}
上面的代码,我们可以关注asyncWaitForVsync这个方法,当Flutter调用scheduleFrame()方法后,只最后会调用到这个方法,而这个方法中Flutter这边会向Android的Choreographer对象申请一个帧回调,简单来说就是借用了平台自身的帧回调,当Vsync信号来临的时候,就会调用FlutterJNI的nativeOnVsync逻辑通知Flutter层中。
大伙还记得我们在标记流程中向PlatformDispatcher注册的两个回调吗?此时流程最终会流转到platformDispatcher.onDrawFrame中开始绘制流程,最终会走到WidgetsBinding类的drawFrame方法中
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
void drawFrame() {
try {
if (renderViewElement != null) {
//1.重新构建脏元素列表中每个Element的Widget对象
buildOwner!.buildScope(renderViewElement!);
}
//2.执行绘制
super.drawFrame();
} finally {
}
}
}
1.2.2 重新构建Element中的Widget
该流程流程是从BuildOwner.buildScope开始的,因为这个类就是处理脏元素的管理类,所以很好理解为什么从这里开始。
但是我们刚刚也说到onDrawFrame中开始绘制流程,那么我们把调用栈放出来,让大家了解一下是如何从platformDispatcher.onDrawFrame流转到BuildOwner.buildScope的,至少有个概念路径,不至于说丢了流程。
了解了流转流程后,绘制就从buildScope中开始了,我们看看这个方法做了什么?
void buildScope(Element context, [ VoidCallback? callback ]) {
try {
//对元素进行一次排序,排序规则是根据深度来进行排序的
_dirtyElements.sort(Element._sort);
int dirtyCount = _dirtyElements.length;
int index = 0;
//开始对脏元素列表遍历重建
while (index < dirtyCount) {
//取出脏元素
final Element element = _dirtyElements[index];
try {
//对元素进行重建(关键)
element.rebuild();
}
//移动角标
index += 1;
//判断脏元素列表是否在重建过程中发生变化,如果产生变化则进行重新排序,与回退角标
if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting!) {
//元素列表重新排序
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
//重新赋值总数
dirtyCount = _dirtyElements.length;
//往回退寻找dirty标志到没有被成功rebuild的元素下标
while (index > 0 && _dirtyElements[index - 1].dirty) {
index -= 1;
}
}
}
} finally {
//对Element的_inDirtyList标志重置
for (final Element element in _dirtyElements) {
element._inDirtyList = false;
}
//清除脏元素列表
_dirtyElements.clear();
}
}
在这个方法里面,重点做的就是执行每个Dirty Element的rebuild方法,促进重建,接下来我们深入的rebuild方法中看看做了什么?
class Element{
void rebuild({bool force = false}) {
if (_lifecycleState != _ElementLifecycle.active || (!_dirty && !force)) {
return;
}
Element? debugPreviousBuildTarget;
performRebuild();
}
}
这个方法就是判断了一下状态,接着调用了StatefulElement的performRebuild方法
class StatefulElement extends ComponentElement {
void performRebuild() {
//判断依赖项有没有变化,如果有的话则调用其回调
if (_didChangeDependencies) {
state.didChangeDependencies();
_didChangeDependencies = false;
}
//调用父类的rebuild方法
super.performRebuild();
}
}
接着我们继续深入ComponentElement的方法到底做了什么
abstract class ComponentElement extends Element {
void performRebuild() {
Widget? built;
try {
//这里就是我们State中build方法,调用这里重新构建并且获取了一个全新的Widget对象
built = build();
} catch (e, stack) {
//...忽略代码,我们会看到一些界面构建错误的红色提示,一般从这里产生
} finally {
//清除该Element的dirty标志
super.performRebuild();
}
try {
_child = updateChild(_child, built, slot);
} catch (e, stack) {
//...忽略代码,我们会看到一些界面构建错误的红色提示,一般从这里产生
_child = updateChild(null, built, slot);
}
}
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
//...省略其他代码
final Element newChild;
//判断是否能更新
if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
//复用Element,并且更新Element中的Widget,并且递归子组件更新Widget信息
child.update(newWidget);
newChild = child;
}
//...省略其他代码
}
return newChild;
}
//更新Element中的Widget信息
void update(covariant Widget newWidget) {
_widget = newWidget;
}
}
至此,dirtyELement中每个Element的Widget基本重建了,但是只是Widget重建了,你可以理解成Widget只是下一帧的绘制信息,但是还没真正的触发绘制。
另外,在child.update方法中,如果当前child是StatefulElement,则会继续递归子组件,调用其Element的performRebuild,递归重建子组件的Widget实例,所以为什么不推荐在高层次调用setState,因为会从当前Element重新build全部子组件
放一张图以便理解
接下来退栈到调用buildScope处,接着开始真正的布局测量绘制过程。
1.2.3 重绘流程
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
void drawFrame() {
try {
if (renderViewElement != null) {
// 1.2.2 中解释完这个流程了
buildOwner!.buildScope(renderViewElement!);
}
// 该节点中开始布局测量绘制
super.drawFrame();
} finally {
}
}
}
void drawFrame() {
//开始布局
pipelineOwner.flushLayout();
//开始绘制
pipelineOwner.flushPaint();
}
基于篇幅问题,布局绘制我们就不在这一篇文章中做对应的介绍了,但是setState中的基本流程基本就讲述完毕。
更多推荐



所有评论(0)