Skip to content

如何正确使用setState()?

shenchunxing edited this page Aug 24, 2023 · 1 revision

一、为什么setState()能刷新页面

1、setState() 我们的demo从一个最简单的计数器开始 image

在页面中点击底部的➕号,本地变量加一,之后调用了当前页面的setState(),页面重新构建,显示的数据增加。从现象推断,整个流程必然会经过setState()-···················->当前State的build()-················->页面绘制-············->屏幕刷新。 那么下面我们看看setState()到底做了什么?

State#setState(VoidCallback fn)

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

在去掉所有的断言之后,其实setState只做了两件事儿

1、调用我们传入的VoidCallback fn

2、调用_element.markNeedsBuild()

2、element.markNeedsBuild() Flutter开发中我们一般和Widget打交道,但Widget上有这样一个注释。

Describes the configuration for an [Element].

abstract class Widget extends DiagnosticableTree {
  final Key key;
  Element createElement();
  String toStringShort() {
    return key == null ? '$runtimeType' : '$runtimeType-$key';
 }

Widget只是用于描述Element的一个配置文件,实际在Framework层管理页面的构建,渲染等,都是通过Element完成,Element由Widget创建,并且持有Widget对象,每一种Widget都会对应的一种Element。

image

在上面的demo中,我们在HomePageState调用了setState(),这里的Element有HomePage对象创建。HomePage(Widget) - HomePageState(State) - HomePageElement(StatefulElement) 三者一一对应。

image

Element#markNeedsBuild()

/// The object that manages the lifecycle of this element.
/// 负责管理所有element的构建以及生命周期
@override
BuildOwner get owner => _owner;

void markNeedsBuild() {
  //将自己标记为脏
  _dirty = true;
  owner.scheduleBuildFor(this);
}

调用了BuildOwner.scheduleBuildFor(element),这里的BuildOwner在WidgetsBinding的初始化中完成实例化,负责管理widget框架,每个Element对象在mount到element树中之后都会从父节点获得它的引用。

WidgetsBinding#initInstances()

void initInstances() {
  super.initInstances();
  _instance = this;
  _buildOwner = BuildOwner();
  buildOwner.onBuildScheduled = _handleBuildScheduled;
 /······/
}

BuildOwner#scheduleBuildFor(Element element)

void scheduleBuildFor(Element element) {
  //添加到_dirtyElements集合中
  _dirtyElements.add(element);
  element._inDirtyList = true;
}

最后将自己添加到BuildOwner中维护的一个脏element集合。

总结:1、Element: 持有Widget,存放上下文信息,RenderObjectElement 额外持有 RenderObject。通过它来遍历视图树,支撑UI结构。

​ 2、setState()过程其实只是将当前对应的Element标记为脏(demo中对应HomePageState),并且添加到_dirtyElements合中。

3、Flutter渲染机制

image

开始FrameWork层会通知Engine表示自己可以进行渲染了,在下一个Vsync信号到来之时,Engine层会通过Windows.onDrawFrame回调Framework进行整个页面的构建与绘制。(这里我想为什么要先由Framework发起通知,而不是直接由Vsync驱动。如果一个页面非常卡顿,恰好每一帧绘制的时间大于一个Vsync周期,这样每帧都不能在一个Vsync的时间段内完成绘制。而先由framework保证上完成构建与绘制后,发起通知在下一个Vsync信号再绘制则可以避免这样的情况)。每次收到渲染页面的通知后,Engine调用Windows.onDrawFrame最终交给_handleDrawFrame()方法进行处理。

@protected
void ensureFrameCallbacksRegistered() {
  //构建帧前的处理,主要是进行动画相关的计算
  window.onBeginFrame ??= _handleBeginFrame;
  //Windows.onDrawFrame交给_handleDrawFrame进行处理
  window.onDrawFrame ??= _handleDrawFrame;
}

SchedulerBinding#handleDrawFrame()

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 {
 	/·····························/
  }
}

在Flutter的SchedulerBinding中维护了这样三个队列

  • Transient callbacks,由系统的[Window.onBeginFrame]回调,用于同步应用程序的行为 到系统的展示。例如,[Ticker]s和[AnimationController]s触发器来自与它。
  • Persistent callbacks 由系统的[Window.onDrawFrame]方法触发回调。例如,框架层使用他来驱动渲染管道进行build, layout,paint
  • Post-frame callbacks在下一帧绘制前回调,主要做一些清理和准备工作 Non-rendering tasks 非渲染的任务,可以通过此回调获取一帧的渲染时间进行帧率相关的性能监控。

SchedulerBinding.handleDrawFrame()中对_persistentCallbacks和_postFrameCallbacks集合进行了回调。根据上面的描述可知,_persistentCallbacks中是一些固定流程的回调,例如build,layout,paint。跟踪这个_persistentCallbacks这个集合,发现在RendererBinding.initInstances()初始化中调用了addPersistentFrameCallback(_handlePersistentFrameCallback)方法。这个方法只有一行调用就是drawFrame()。

总结:

SchedulerBinding中维护了这样三个队列TransientCallbacks(动画处理),PersistentCallbacks(页面构建渲染),PostframeCallbacks(每帧绘制完成后),并在合适的时机对其进行回调。 当收到Engine的渲染通知之后通过Windows.onDrawFrame方法回调到Framework层调用handleDrawFrame handleDrawFrame回调PersistentCallbacks(页面构建渲染),最终调用drawFrame()

4、drawFrame()

查看drawFrame()方法一般会直接点击到RendererBinding中

RendererBinding#drawFrame()

void drawFrame() {
  pipelineOwner.flushLayout();//刷新布局
  pipelineOwner.flushCompositingBits();//刷新合成
  pipelineOwner.flushPaint();//刷新绘制
  renderView.compositeFrame(); // this sends the bits to the GPU
  pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}

从这几个方法名能大致看出,这里调用了布局,绘制,渲染帧的。而且看类名,这是负责渲染的Binding,并没有调用Widget的构建。这是因为WidgetsBinding是onRendererBinding的(理解为继承),其中重写了drawFrame(),实际上调用的应该是WidgetsBinding.drawFrame()。

WidgetsBinding#drawFrame()

@override
void drawFrame() {
try {
    if (renderViewElement != null)
      // buildOwner就是前面提到的负责管理widgetbuild的对象
      // 这里的renderViewElement是整个UI树的根节点
      buildOwner.buildScope(renderViewElement);
    super.drawFrame();
  		//将不再活跃的节点从UI树中移除
    buildOwner.finalizeTree();
  } finally {
		/·················/
  }
}

BuildOwner#buildScope(Element context, [ VoidCallback callback ])

void buildScope(Element context, [ VoidCallback callback ]) {
  if (callback == null && _dirtyElements.isEmpty)
    return;
  try {
    _scheduledFlushDirtyElements = true;
    _dirtyElementsNeedsResorting = false;
    _dirtyElements.sort(Element._sort);
    _dirtyElementsNeedsResorting = false;
    int dirtyCount = _dirtyElements.length;
    int index = 0;
    while (index < dirtyCount) {
      try {
        ///关键在这
        _dirtyElements[index].rebuild();
      } catch (e, stack) {
     	/···············/
      }
    }
  } finally {
    for (Element element in _dirtyElements) {
      element._inDirtyList = false;
    }
    _dirtyElements.clear();
  }
}

前面在setState()之后,将homePageState添加到_dirtyElements里面。而这个方法会对集合内的每一个对象调用rebuild()。rebuild()这个方法最终走到performRebuild(),这是一个Element中的一个抽象方法。

二、为什么高位置的setState ()会消耗性能

1、performRebuild()

查看StatelessElement和StatefulElement共同祖先CompantElement中的实现

CompantElement#performRebuild()

void performRebuild() {
  Widget built;
  try {
    built = build();
  } catch (e, stack) {
    built = ErrorWidget.builder();
  } 
  try {
    _child = updateChild(_child, built, slot);
  } catch (e, stack) {
    built = ErrorWidget.builder();
    _child = updateChild(null, built, slot);
  }

}

这个方法直接调用子类的build方法返回了一个Widget,对应调用前面的HomePageState()中的build方法。

将这个新build()出来的widget和之前挂载在Element树上的_child(Element类型)作为参数,传入updateChild(_child, built, slot)中。setState()的核心逻辑就在 updateChild(_child, built, slot)。

2、updateChild(_child, built, slot) StatefulElement#updateChild(Element child, Widget newWidget, dynamic newSlot)

@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  if (newWidget == null) {
    if (child != null)
      //child == null && newWidget == null
      deactivateChild(child);
    //child != null && newWidget == null
    return null;
  }
  if (child != null) {
    if (child.widget == newWidget) {
      //child != null && newWidget == child.widget
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      return child;
    }
    if (Widget.canUpdate(child.widget, newWidget)) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      //child != null && Widget.canUpdate(child.widget, newWidget)
      child.update(newWidget);
      return child;
    }
    deactivateChild(child);
  }
  // child != null && !Widget.canUpdate(child.widget, newWidget)
  return inflateWidget(newWidget, newSlot);
}

这个方法上会根据之前挂载在Element树上的_child以及再次调用build()出来的newWidget对象,共有四种情况

  • 如果之前的位置child为null

    A、如果newWidget为null的话,说明这个位置始终没有子节点,直接返回null即可。

    B、如果newWidget不为null,说明这个位置新增加了子节点调用inflateWidget(newWidget, newSlot)生成一个新的Element返回

  • 如果之前的child不为null

    C、如果newWidget为null的话,说明这个位置需要移除以前的节点,调用 deactivateChild(child)移除并且返回null

    D、如果newWidget不为null的话,先调用Widget.canUpdate(child.widget, newWidget)对比是否能更新。这个方法会对比两个Widget的runtimeType和key,如果一致则说明子Widget没有改变,只是需要根据newWidget(配置清单)更新下当前节点的数据child.update(newWidget);如果不一致说明这个位置发生变化,则deactivateChild(child)后返回inflateWidget(newWidget, newSlot)

而在demo中,观察代码我们可以知道

image

在homePageState中调用setState()后,child和newWidget都不为空都是Scaffold类型,并且由于我们没有显示的指定key,所以会走child.update(newWidget)方法**(注意这里的child已经变成Scaffold)**。

**3、递归更新

update(covariant Widget newWidget)是一个抽象方法,不同element有不同实现,以StatulElement为例

void update(StatefulWidget newWidget) {
  super.update(newWidget);
  assert(widget == newWidget);
  final StatefulWidget oldWidget = _state._widget;
  // Notice that we mark ourselves as dirty before calling didUpdateWidget to
  // let authors call setState from within didUpdateWidget without triggering
  // asserts.
  _dirty = true;
  _state._widget = widget;
  try {
    final dynamic debugCheckForReturnedFuture = _state.didUpdateWidget(oldWidget) as dynamic;
  } finally {
    _debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
  }
  rebuild();
}

这个方法先回调用_state.didUpdateWidget我们可以在State中重写这个方法,走到最后发现最终再次调用了rebuild()。但这里需要注意这次调用rebuild()的已经不是HomePageState了,而是他的第一个子节点Scaffold。所以整个过程又会再次走到performRebuild(),又在再次调用updateChild(_child, built, slot)更新子节点。不断的递归直到页面的最子一级节点。如图 image

build()过程虽然只是调用一个组件的构造方法,不涉及对Element树的挂载操作。但因为我们一个组件往往是N多个Widget的嵌套组合,每个都遍历一遍开销算下来并不小(感兴趣可以数数Scaffold有多少层嵌套)。 回到我们的demo中,其实我们的诉求只是点击+号改变以前显示的数据。

image

但直接在页面节点调用setState()将会重新调用所有Widget(包括他们中的各种嵌套)的build()方法,如果我们的需求是一个较为复杂的页面,这样带来的开销消耗可想而知。

总结

当我们在一个高节点调用setState()的时候会构建再次build所有的Widget,虽然不一定挂载到Element树中,但是平时我们使用的Widget中往往嵌套多个其他类型的Widget,每个build()方法走下来最终也会带来不小的开销,因此通过各种状态管理方案,Stream等方式,只做局部刷新,是我们日常开发中应该养成的良好习惯。