setState 更新原理和流程

980 阅读5分钟

setState 更新原理和流程

在学习Flutter核心原理和state的生命周期的时候,通过翻看源码和教程等,把自己的一些理解和心得整理了,分享一下setState的更新原理和流程,以及setState后带来的一些性能问题怎么避免。

准备

我这里准备了一个简短的代码示例。根组件MyHomePage,自定义组件MyWidget展示一个Text,FloatingActionButton按钮负责触发setState:

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key, this.title}) : super(key: key);
  final String title;
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: MyWidget(),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() {}),
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

自定义组件MyWidget,测试是否会触发build

class MyWidget extends StatelessWidget {
  // const MyWidget();

  @override
  Widget build(BuildContext context) {
    print("MyWidget build");
    return Center(child: Text('MyWidget'));
  }
}

首次 build

首次启动的时候Flutter 会经过 scheduleAttachRootWidgetattachRootWidgetattachToRenderTree 调用到 RenderObjectToWidgetElementmount 方法。在过程中会涉及相当多的源码函数,具体的Flutter显示流程和所调用的函数得解释,我们可以参考《Flutter实战》

我用debug 模式,打断点得到这份渲染流程信息:

当我们首次加载一个页面组件的时候,由于所有节点都是不存在的,因此这时候的流程大部分情况下都是创建新的节点。

setState

点击FloatingActionButton调用setState(() {}),日志输出

flutter: MyWidget build

MyWidget调用了build,发生了重绘,实际上MyHomePage循环了所有子组件,都进行了刷新。 我们来看看Flutter做了什么,调用了那些函数。 查看setState方法

1.State 类的 setState

State#setState(VoidCallback fn)

@protected
  void setState(VoidCallback fn) {
    final dynamic result = fn() as dynamic;
    _element.markNeedsBuild();
  }

除了一些判断条件 和 assert 代码,执行了我们传进来的fn,即setState(() {})里面的回调函数。然后就是调用了自己的_elementmarkNeedsBuild()方法,字面意思就是标记为需要build了。继续来看下markNeedsBuild:

2. Element 类 markNeedsBuild方法

void markNeedsBuild() {
  ...
  // 由于一帧做两次更新有点低效,所以在如果`_active=false` 的时候直接返回。
  if (!_active)
    return;//返回
   ...
  if (dirty)
    return;
   // 设置`_dirty = true `
  _dirty = true;
  //调用scheduleBuildFor方法
  owner.scheduleBuildFor(this);
}

_dirty = true 将 element 元素标记为“脏” 。调用owner.scheduleBuildFor(this),把element传到scheduleBuildFor方法,实际在scheduleBuildFor方法里,会把它添加到全局的“脏”链表里,以便在下一帧更新信号时更新。 可以看到owner是BuildOwner类型,但是那它是从哪里来的呢?在看scheduleBuildFor方法之前,我们先来探究下owner的来龙去脉。

3.BuildOwner

WidgetsBinding类有一个成员BuildOwner _buildOwner,在应用启动时WidgetsBinding实例化的时候进行了初始化:

WidgetsBinding#initInstances

mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    ...
    _buildOwner = BuildOwner();
    buildOwner.onBuildScheduled = _handleBuildScheduled;
    ...
  }

实际上每个Element对象在mount到element树中之后都会从父节点获得WidgetsBinding._buildOwner的引用。

稍微跟踪启动流程来看看: 从runApp()方法开始进入找到WidgetsBinding.attachRootWidget,创建根节点,里面调用了attachToRenderTree,并且传入了buildOwner:

RenderObjectToWidgetElement#attachToRenderTree

 RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
    if (element == null) {
      owner.lockState(() {
        element = createElement();
        assert(element != null);
        element.assignOwner(owner);//赋值给了element的_owner。
      });
      owner.buildScope(element, () {
        element.mount(null, null);//会循环创建子节点,并在创建的过程中将需要更新的数据标记为 dirty
      });
      SchedulerBinding.instance.ensureVisualUpdate();
    } else {
      element._newWidget = this;
      element.markNeedsBuild();
    }
    return element;
  }

attachToRenderTree里面创建了根element,调用了element.assignOwner(owner),即把buildOwner赋值给了根element的 _owner。 而element 的 mount会循环创建子节点,在mount里面,子element又会得到父element的owner。查看Element类的mount方法:

Element#mount

@mustCallSuper
  void mount(Element parent, dynamic newSlot) {
   ...
    _parent = parent;
    _slot = newSlot;
    _depth = _parent != null ? _parent.depth + 1 : 1;
    _active = true;
    if (parent != null) // Only assign ownership if the parent is non-null
      _owner = parent.owner;
   ...
  }

_owner = parent.owner,至此每个Element对象获得了WidgetsBinding._buildOwner的引用。

其实BuildOwner是widget framework的管理类,它跟踪哪些widget需要重新构建。

下面回到BuildOwner的scheduleBuildFor方法。

4.BuildOwner 类 scheduleBuildFor方法

BuildOwner#scheduleBuildFor

void scheduleBuildFor(Element element) {
    ...
    _dirtyElements.add(element);//把element添加到脏元素链表
    element._inDirtyList = true;
    ...
  }

把一个 element 添加到 _dirtyElements 集合,注意_dirtyElements也是BuildOwner的成员。

至此setState()的结果就是将当前对应的element标记为脏,添加到buildOwner 的 _dirtyElements集合中。 那么我们的组件是怎么刷新渲染的呢?下面我们再看下setState()后页面是怎么渲染的。

渲染

为了更新显示画面,显示器是以固定的频率刷新(从GPU取数据),当一帧图像绘制完毕后准备绘制下一帧时,显示器会发出一个垂直同步信号VSync,Flutter Engine收到通知会通过Windows.onDrawFrame回调Framework进行整个页面的构建与绘制。

window.onDrawFrame

为了探究setState()后页面的渲染流程,我在MyWidgetbuild方法里面加了个断点:

点击按钮执行setState()后,发现断点:

可以看到首先执行了hooks.dart里面的_drawFrame方法:

@pragma('vm:entry-point')
// ignore: unused_element
void _drawFrame() {
  _invoke(window.onDrawFrame, window._onDrawFrameZone);
}

调用了_invoke方法_invoke(void callback()?, Zone zone),看下断点发现是回调参数是_handleDrawFrame()方法:

其实Windows.onDrawFrame 是绑定到了SchedulerBinding 的 _handleDrawFrame(),在第一次启动app的时候就已经注册了:在attachToRenderTree方法里面调用了 SchedulerBinding.instance.ensureVisualUpdate(),确保生成第一帧的时候只会访问一次。接着调用SchedulerBindingscheduleFrame 方法里面的 ensureFrameCallbacksRegistered方法,确保window.onBeginFramewindow.onDrawFrame回调被注册:

/// Ensures callbacks for `window.onBeginFrame` and `window.onDrawFrame`
  /// are registered.
  @protected
  void ensureFrameCallbacksRegistered() {
    window.onBeginFrame ??= _handleBeginFrame;
    window.onDrawFrame ??= _handleDrawFrame;
  }

接下来我们重点来看下SchedulerBinding 的 _handleDrawFrame

SchedulerBinding 类的 _handleDrawFrame

当收到Engine的渲染通知之后通过Windows.onDrawFrame方法回调到SchedulerBinding 的handleDrawFrame,最终调用SchedulerBinding 的 drawFrame()。下面看下如何调用到drawFrame()的。

void handleDrawFrame() {
  try {
    // PERSISTENT FRAME CALLBACKS
    // 关键回调
    for (FrameCallback callback in _persistentCallbacks)
      _invokeFrameCallback(callback, _currentFrameTimeStamp);
    // POST-FRAME CALLBACKS
    final List<FrameCallback> localPostFrameCallbacks =
        List<FrameCallback>.from(_postFrameCallbacks);
    _postFrameCallbacks.clear();
   for (FrameCallback callback in localPostFrameCallbacks)
        _invokeFrameCallback(callback, _currentFrameTimeStamp);
  } finally {
 	/·····························/
  }
}

其中_persistentCallbacks用于存放一些持久的回调,遍历_persistentCallbacks集合,执行相应的回调方法。通过SchedulerBinding 类的addPersistentFrameCallback方法注册添加回调函数:

 void addPersistentFrameCallback(FrameCallback callback) {
    _persistentCallbacks.add(callback);
  }

addPersistentFrameCallback是在RendererBinding 实例化的时候调用的:

mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    _...
    window
      ..onMetricsChanged = handleMetricsChanged
      ..onTextScaleFactorChanged = handleTextScaleFactorChanged
      ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
      ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
      ..onSemanticsAction = _handleSemanticsAction;
    ...
    addPersistentFrameCallback(_handlePersistentFrameCallback);
    ...
  }

注册添加了_handlePersistentFrameCallback回调,这个方法里面调用了drawFrame()

void _handlePersistentFrameCallback(Duration timeStamp) {
    drawFrame();
    _scheduleMouseTrackerUpdate();
  }

RendererBinding 类的 drawFrame

查看RendererBinding 类的 drawFrame:

void drawFrame() {
  assert(renderView != null);
  pipelineOwner.flushLayout(); //布局
  pipelineOwner.flushCompositingBits(); //重绘之前的预处理操作,检查RenderObject是否需要重绘
  pipelineOwner.flushPaint(); // 重绘
  renderView.compositeFrame(); // 将需要绘制的比特数据发给GPU
  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}

由于RendererBinding只是一个mixin,而with它的是WidgetsBinding,所以我们需要看看WidgetsBinding中是否重写该方法,查看WidgetsBinding的drawFrame()方法源码:

@override
void drawFrame() {
 ...//省略无关代码
  try {
    if (renderViewElement != null)
      buildOwner.buildScope(renderViewElement); 
    super.drawFrame(); //调用RendererBinding的drawFrame()方法
    buildOwner.finalizeTree();
  } 
}