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 会经过 scheduleAttachRootWidget 、 attachRootWidget 、attachToRenderTree 调用到 RenderObjectToWidgetElement 的 mount 方法。在过程中会涉及相当多的源码函数,具体的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(() {})里面的回调函数。然后就是调用了自己的_element 的markNeedsBuild()方法,字面意思就是标记为需要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()后页面的渲染流程,我在MyWidget的build方法里面加了个断点:
点击按钮执行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(),确保生成第一帧的时候只会访问一次。接着调用SchedulerBinding 的scheduleFrame 方法里面的 ensureFrameCallbacksRegistered方法,确保window.onBeginFrame 和 window.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();
}
}