1. setState的原理剖析

首先在开头给出答案,setState整个流程到底做了什么事情?

  1. 标记需要重新构建的Element为脏元素并且保存到BuildOwner后向平台申请帧回调,等待重绘
  2. 帧回调回来后,会遍历脏元素重新构建Widget信息,并且递归其子组件重新构建Widget信息
  3. 开始重新测量绘制
1.1 标记

我们先看看setState具体做了什么?

void setState(VoidCallback fn) {
    ///...省略代码
    ///1.调用方法setState中的方法体,执行控件逻辑
    final Object? result = fn() as dynamic;
    ///2.调用State中的Element,markNeedsBuild方法
    _element!.markNeedsBuild();
}

setState里面只做了两件事

  1. 第一个就是调用方法体,执行对应的逻辑
  2. 第二就是调用自身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事情。

  1. 元素标记为脏元素,并且扔到脏元素列表中等待重建。
  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中的基本流程基本就讲述完毕。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐