本文已授权公众号【缦图技术团队】发布
详解flutter刷新流程,让你的应用更流畅
一、概述
Flutter
是谷歌推出的高性能、跨端UI
框架,可以通过一套代码,支持 iOS
、Android
、Windows/MAC/Linux
等多个平台,且能达到原生性能。Flutter
也可以与平台原生代码进行混合开发,其更新迭代速度很快,技术发展也日趋成熟,如今已经有很多公司在使用这种新跨端技术。我们知道在 flutter
中可以使用 setState()
来刷新 StatefulWidget
的 UI
,这会遍历调用子 Widget
的 build()
重构视图。当一个页面内容比较复杂时,会包含多个 widget
,如果直接调用根组件的 setState()
,会遍历所有子 Widget
的 build()
,刷新整个页面,这样会造成很多不必要的开销,刷新的成本相对较大。如果数据很多接口响应又慢的话,还会有界面闪烁的现象。那么 flutter
到底是如何实现界面刷新的,调用 setState({})
后 flutter
的framework
到底做了哪些操作?接下来我们一起来揭开 flutter
刷新界面的神秘面纱。
二、Flutter 渲染中的三棵树
在了解flutter
的刷新机制之前,先来看看flutter
渲染过程中的三棵树。在Flutter
的渲染过程中由Widget
,Element
,RenderObject
这个三个元素组成三棵树。Widget
控件树,Element
元素树,RenderObject
渲染树。Widget
内部调用 createElement()
会创建对应的 Element
,Element
内部调用 createRenderObject()
会创建对应的 RenderObject
,所以我们只需要关心 Widget
就可以快速的构建视图界面了。为什么使用三棵树而不是 Widget
和RenderObject
两棵树呢?这里是为了复用 Element
提升渲染性能,因为 Widget
面向业务它的改变会很频繁,如果根据 Widget
直接生成 RenderObject
会导致渲染性能下降。
RenderObject
渲染树在上屏前会生成一棵 Layer
树去进行屏幕渲染。
三、刷新流程分析
在开始流程分析之前,先上个图来梳理下整个刷新流程,脑海里对整体先有个初步认识,这样再跟着下面的源码一步步往里深入分析,思路会更加清晰一些:
对整体的刷新流程有了大概的认识之后,我们对照着上面这个图的流程来看看调用setState({})
之后,系统具体都做了哪些操作:
注:
setState() 源码位于 flutetr_sdk/packages/flutter/lib/src/widgets/framework.dart 文件中
本文源码基于 Flutter 3.3.8 Dart 2.18.4 • DevTools 2.15.0
@protected
void setState(VoidCallback fn) {
assert(fn != null);
assert(() {
if (_debugLifecycleState == _StateLifecycle.defunct) {
throw FlutterError.fromParts(<DiagnosticsNode>[
// 省略不重要代码
]);
}
if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
throw FlutterError.fromParts(<DiagnosticsNode>[
// 省略不重要代码
]);
}
return true;
}());
final Object? result = fn() as dynamic;
...
// 省略不重要代码
_element!.markNeedsBuild();
}
setState()
中传入的回调函数是立刻同步执行的,不能是异步的。该方法前面主要是 assert
部分的一些校验逻辑,不允许传入的回调函数为null
且不能为异步函数,这里有一个点需要注意:在 widget
构造函数中以及 dispose
调用之后,不允许再调用 setState()
方法去刷新界面,可以在调用前考虑使用 mounted
标志来检测该 widget
是否还挂载在 widget
树上。
最关键的代码在最后一行:_element!.markNeedsBuild();
这里的_element
就是statefullWidget
创建的 StatefulElement
对象,我们看下 Element
类的markNeedsBuild()
方法做了什么,代码依然在 framework.dart
中:
void markNeedsBuild() {
assert(_lifecycleState != _ElementLifecycle.defunct);
if (_lifecycleState != _ElementLifecycle.active) {
return;
}
assert(owner != null);
assert(_lifecycleState == _ElementLifecycle.active);
// ...
if (dirty) {
return;
}
_dirty = true;
owner!.scheduleBuildFor(this);
}
这个方法我们在自定义 flutter
组件中经常见到,前面的 assert
部分是一些生命周期的校验,不在 active
状态,则不进行后续操作。这里要注意,该方法不能在build
期间调用。关键代码还是在最后几行,将当前的 element
标记为 dirty
,然后把它加入到全局的 widget
列表中,然后在下一帧中去进行重绘。这里的owner
指的是 BuildOwner
,它是 widget
的管理类,该类跟踪哪些 widget
需要重建,并处理整体上应用于 widget
树的其他任务,例如管理树的非活动元素列表,以及在调试时的热重载期间在必要时触发“重新组装”命令。BuildOwner
是我们一开始通过 runApp()
初始化时创建的,它内部维护了一个_dirtyElements
列表,用以保存被标记为“脏”的 element
。
再来看下 BuildOwner
类的 scheduleBuildFor()
方法:
void scheduleBuildFor(Element element) {
assert(element != null);
assert(element.owner == this);
// ...
if (element._inDirtyList) {
// ...
_dirtyElementsNeedsResorting = true;
return;
}
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled!();
}
_dirtyElements.add(element);
element._inDirtyList = true;
}
该方法 assert
部分依旧是一些必要的参数校验,关键部分是如果 element
已经在_inDirtyList
列表中,设置 _dirtyElementsNeedsResorting = true
后直接返回。不在的话,如果是新的 dirty element,
且当前这个 dirty element
列表还未注册过 VSync
信号监听,则还需要执行 onBuildScheduled()
,该方法会调用PlatformDispatcher.scheduleFrame()
向平台注册 VSync
监听。最后添加 element
到标记为 dirty element
的列表中,以便当 WidgetsBinding.drawFrame
调用 buildScope
方法时执行重建。
这里的_dirtyElementsNeedsResorting
表示是否需要对 dirt elements
重新排序,因为之前被标“脏”的 element
,它在树中的位置(深度)可能已经变了,需要对 dirty element
重新排序。
来看下 onBuildScheduled()
方法,它在 WidgetBinding
初始化的时候就已经创建了:
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding,
GestureBinding, RendererBinding, SemanticsBinding {
@override
void initInstances() {
super.initInstances();
_instance = this;
_buildOwner = BuildOwner();
buildOwner!.onBuildScheduled = _handleBuildScheduled;
platformDispatcher.onLocaleChanged = handleLocaleChanged;
platformDispatcher.onAccessibilityFeaturesChanged =
handleAccessibilityFeaturesChanged;
SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
platformMenuDelegate = DefaultPlatformMenuDelegate();
}
...
}
可以看到 onBuildScheduled()
回调最终等于回调 _handleBuildScheduled()
void _handleBuildScheduled() {
...
// 省略不重要代码
ensureVisualUpdate();
}
void ensureVisualUpdate() {
switch (schedulerPhase) {
// 没有正在处理的帧,可能正在执行的是 WidgetsBinding.scheduleTask,
// scheduleMicrotask,Timer,事件 handlers,或者其他回调等
case SchedulerPhase.idle:
// 主要是清理和计划执行下一帧的工作
case SchedulerPhase.postFrameCallbacks:
scheduleFrame();
return;
// SchedulerBinding.handleBeginFrame 过程, 处理动画状态更新
case SchedulerPhase.transientCallbacks:
// 处理 transientCallbacks 阶段触发的微任务(Microtasks)
case SchedulerPhase.midFrameMicrotasks:
// WidgetsBinding.drawFrame 和 SchedulerBinding.handleDrawFrame 过程,
// build/layout/paint 流水线工作
case SchedulerPhase.persistentCallbacks:
return;
}
}
上面的 ensureVisualUpdate()
分别处理了 SchedulerPhase
的 5 个枚举值,这里主要看下处理 postFrameCallbacks
分支,它执行了 SchedulerBinding.scheduleFrame()
方法:
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled) {
return;
}
ensureFrameCallbacksRegistered();
platformDispatcher.scheduleFrame();
_hasScheduledFrame = true;
}
看下SchedulerBinding.ensureFrameCallbacksRegistered()
方法:
@protected
void ensureFrameCallbacksRegistered() {
platformDispatcher.onBeginFrame ??= _handleBeginFrame;
platformDispatcher.onDrawFrame ??= _handleDrawFrame;
}
这里可以关注下 _handleBeginFrame()
、_handleDrawFrame()
这两个方法,它确保PlatformDispatcher.onBeginFrame
、PlatformDispatcher.onDrawFrame
回调已注册,然后合适的时机进行相关回调,细节就不展开了。
最后执行 platformDispatcher.scheduleFrame()
,再来看看这个方法:
void scheduleFrame() native 'PlatformConfiguration_scheduleFrame';
@pragma('vm:entry-point')
void _drawFrame() {
PlatformDispatcher.instance._drawFrame();
}
// Called from the engine, via hooks.dart
void _drawFrame() {
_invoke(onDrawFrame, _onDrawFrameZone);
}
它是一个native
方法,会在需要布局绘制下一个帧的适当时机调用SchedulerBinding.handleDrawFrame()
方法去绘制新的一帧:
void handleDrawFrame() {
assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
_frameTimelineTask?.finish();
try {
// PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (final FrameCallback callback in _persistentCallbacks) {
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
}
// POST-FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.of(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (final FrameCallback callback in localPostFrameCallbacks) {
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
}
} finally {
_schedulerPhase = SchedulerPhase.idle;
_frameTimelineTask?.finish();
_currentFrameTimeStamp = null;
}
}
这里去执行了之前注册的回调,最终回调到 WidgetsBinding.drawFrame
方法:
@override
void drawFrame() {
assert(!debugBuildingDirtyElements);
assert(() {
debugBuildingDirtyElements = true;
return true;
}());
TimingsCallback? firstFrameCallback;
if (_needToReportFirstFrame) {
assert(!_firstFrameCompleter.isCompleted);
firstFrameCallback = (List<FrameTiming> timings) {
SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback!);
firstFrameCallback = null;
_firstFrameCompleter.complete();
};
SchedulerBinding.instance.addTimingsCallback(firstFrameCallback!);
}
try {
if (renderViewElement != null) {
buildOwner!.buildScope(renderViewElement!);
}
super.drawFrame();
buildOwner!.finalizeTree();
} finally {
// ...
}
_needToReportFirstFrame = false;
if (firstFrameCallback != null && !sendFramesToEngine) {
_needToReportFirstFrame = true;
SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback!);
}
}
其中 renderViewElement
不为null
的时候则会执行buildOwner!.buildScope(renderViewElement!)
方法,再看下 buildScope()
方法:
@pragma('vm:notify-debugger-on-exception')
void buildScope(Element context, [ VoidCallback? callback ]) {
if (callback == null && _dirtyElements.isEmpty) {
return;
}
// ...
// 省略不重要代码
try {
_scheduledFlushDirtyElements = true;
if (callback != null) {
assert(_debugStateLocked);
Element? debugPreviousBuildTarget;
_dirtyElementsNeedsResorting = false;
try {
callback();
} finally {
// ...
}
}
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
final Element element = _dirtyElements[index];
try {
element.rebuild();
} catch (e, stack) {
// ...
}
if (isTimelineTracked) {
Timeline.finishSync();
}
index += 1;
if (dirtyCount < _dirtyElements.length ||
_dirtyElementsNeedsResorting!) {
_dirtyElements.sort(Element._sort);
_dirtyElementsNeedsResorting = false;
dirtyCount = _dirtyElements.length;
while (index > 0 && _dirtyElements[index - 1].dirty) {
index -= 1;
}
}
}
return true;
}());
} finally {
for (final Element element in _dirtyElements) {
assert(element._inDirtyList);
element._inDirtyList = false;
}
_dirtyElements.clear();
_scheduledFlushDirtyElements = false;
_dirtyElementsNeedsResorting = null;
}
在该方法中对 _dirtyElements
进行了判空,如果非空,则会进行排序_dirtyElements.sort(Element._sort)
,然后循环遍历取出所有“脏” element
,最后通过执行 element.rebuild()
重建标记为 dirty
的 element
。
@pragma('vm:prefer-inline')
void rebuild() {
assert(_lifecycleState != _ElementLifecycle.initial);
if (_lifecycleState != _ElementLifecycle.active || !_dirty) {
return;
}
assert(_lifecycleState == _ElementLifecycle.active);
// ...
performRebuild();
assert(!_dirty);
}
在rebuild()中执行了performRebuild(),它最终调用Element也就是StatefulWidget中的build():
@override
@pragma('vm:notify-debugger-on-exception')
void performRebuild() {
assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(true));
Widget? built;
try {
built = build();
debugWidgetBuilderValue(widget, built);
} catch (e, stack) {
_debugDoingBuild = false;
} finally {
_dirty = false;
assert(_debugSetAllowIgnoredCallsToMarkNeedsBuild(false));
}
try {
_child = updateChild(_child, built, slot);
assert(_child != null);
} catch (e, stack) {
// ...
_child = updateChild(null, built, slot);
}
}
至此,setState()
的工作就完成了,当下一个 VSync
信号到来时,flutter
就会自动帮我们更新那些标记为“脏”的 element
了,重建完成后清理掉“脏”列表中的 element
。
至于注册 VSync
信号监听的时机是在 engine
层,具体注册 VSync
监听的代码体现在PlatformDispatcher
这个类里面,这个类封装于 dart:ui
库中的platform_dispatcher.dart
文件中,它主要负责派发从平台过来的各种从平台配置到屏幕和窗口的创建或销毁的事件,里面定义了许多的 native
方法,负责 flutter
与平台底层的交互。flutter
启动 App
时,从入口方法 runApp()
开始执行,然后会调用ensureInitialized()
方法去初始化 WidgetsBinding
、BuildOwner
等对象,当下一个VSync
信号来临时,通过回调方法最终会调用 PlatformDispatcher.scheduleFrame()
方法,注册VSync
信号监听,然后会调用 PlatformDispatcher.onBeginFrame()
和PlatformDispatcher.onDrawFrame()
,在 onDrawFrame
中会一步步执行 build
、layout
、paint
、composite
等过程。具体就不再细说了,感兴趣的可以去翻一翻这一块源码的详细实现。
四、其他的刷新方式
了解了setState()
的刷新原理,我们在自定义一些flutter
组件的时候,或者实际开发工作中,就可以很清楚的知道何时怎样刷新我们的组件,既增加了定制组件的灵活性,也减小了性能开销。除了setState()
的刷新方式,还有一些其他的轻量一些的局部刷新方式,可以在不同的场景选择合适的刷新方式,来提高刷新渲染效率和用户体验。当然局部刷新最底层的实现都和setState()
是一样的,只是对 setState()
的一层封装,使刷新的开销尽可能的降低。
1、利用GlobalKey进行局部刷新
在一个界面中,通常只需要刷新某个子组件或者某个子组件的部分 UI,这种情况下调用父级State
的 setState
方法会造成不必要的资源浪费,此时使用局部刷新就更合适。这种刷新方式是对上面 setState()
方法的改进,根本的方法还是 setState()
,只不过是通过方法去刷新某个子控件:
GlobalKey<CustomWidgetState> _globalKey = GlobalKey();
int count = 0;
....
CustomWidget(key: _globalKey);
// 更新
_globalKey.currentState.update(count);
2、通过 provider 进行局部刷新
我们也可以使用 provider
来进行局部刷新,provider
是 2019年Google I/O
大会上Flutter
官方新推荐的状态管理方式之一。它内部的 DelegateWidget
是一个 StatefulWidget
,所以具有生命周期状态感知,其状态共享是使用了 InheritedProvider
这个 InheritedWidget
实现的,配合 ChangeNotifier
和notifyListeners()
可以实现跨组件数据的传递以及状态监听界面自动刷新。官方地址:provider | Flutter Package
3、通过 StreamBuilder 进行局部刷新
final StreamController _streamController = StreamController<int>();
StreamBuilder<int> (
stream: _streamController.stream,
initialData: 0,
builder: (BuildContext context, AsyncSnapshot<int> snapshot) {}))
// 触发更新
_streamController.sink.add(--count);
这里通过 sink.add
方法向 streamController.sink
中添加一个事件流,StreamBuilder
接收到这个 stream
流后,触发 builder
方法,接着去重绘页面。StreamBuilder
其实还有很多的其他用处,不仅仅是简单的局部刷新,例如发起一个网络请求后不断的接收 stream
流事件,可以实现不断地从后端获取最新数据实时刷新页面。
类似的组件还有 FutureBuilder
、AnimatedBuilder
,都是官方提供的可以局部刷新的组件。
4、通过 ValueNotifier 进行局部刷新
// 创建
final ValueNotifier<int> _curPageIndex = ValueNotifier(0);
// 更新
_curPageIndex.value = index;
// 使用
ValueListenableBuilder(
valueListenable: _curPageIndex,
builder: (BuildContext context, int value, Widget? child) {
return Text(
'$value',
style: TextStyle(color: Colors.white, fontSize: 11.sp),
);
},
),
这样当 value
改变后,对应监听的组件会自动更新,而不需要再调用 setState()
,其本质也是使用了 ChangeNotifier
配合notifyListeners()
来实现刷新,有点类似观察者模式。
五、总结
以上介绍了在 flutter
中通过 setState()
刷新界面的实现流程,并且介绍了其他几种局部刷新界面的方式。最后我们还需要明确一点,直接使用 setState()
进行更新,只是轻量级的配置信息重新创建,而 Element
、RenderObject
、State
这些对象一般情况下不会重新创建,只是根据配置信息进行了更新,相比较于首次创建 widget
开销要小,但是毕竟涉及到 UI
界面重绘,还是要合理的使用。在实际开发中,直接使用 setState()
时要保持对应的 Widget
尽量小层级尽量低,且回调中的逻辑不能是耗时操作。否则考虑局部刷新的方式,具体可以结合上面几种刷新方式,选择恰当的方式来提升刷新性能和获得更好的用户体验。