前言
平常在使用flutter的控件时我们都知道,要刷新页面那么只需要调用setState()方法即可,当调用此方法后,该页面会重新build,然后重新构建Tree。那么使用setState()方法时有哪些注意事项呢?调用当前页面的setState()方法会将当前页面的所有子控件都刷新,如何实现局部刷新呢?最后一个疑问就是setState()的刷新机制和刷新原理到底是怎样的呢?今天,带着这些疑问,逐步学习和了解。
使用setState()的注意事项
了解一个方法的注意事项,最好的方式就是看方法说明文档和源码了,这里截取最重要的setState()方法的源码及注释
.....省略的注释
/// The provided callback is immediately called synchronously. It must not
/// return a future (the callback cannot be `async`), since then it would be
/// unclear when the state was actually being set.
.....省略的注释
/// Generally it is recommended that the `setState` method only be used to
/// wrap the actual changes to the state, not any computation that might be
/// associated with the change.
void setState(VoidCallback fn) {
.....省略的源码
final dynamic result = fn() as dynamic;
....省略的源码
_element.markNeedsBuild();
}
这里说明如下:
1、回调方法不能是async修饰的,从源码可以看到,这里的回调方法fn是同步执行的,所以如果换成async可能会导致不同步了,实际上仔细查看源码也可以发现如果是async的方法assert()会抛出异常。
2、回调方法内最好只做跟build相关的改变的代码,为什么呢?因为从setState()函数中可以看到它先同步执行回调函数,最后一句将State标记为可以build的状态,等到下一个Vsync信号到来时才进行build和重新渲染,如果这个回调方法里面有很长的耗时任务,那么最后一句迟迟不能执行,所以就会导致build延迟了。为此,官方也给出了使用示例,如下:
Future<void> _incrementCounter() async {
setState(() {
_counter++;
});
Directory directory = await getApplicationDocumentsDirectory();
final String dirName = directory.path;
await File('$dir/counter.txt').writeAsString('$_counter');
}
这里就将刷新和保存数据的工作分开进行了
3、官方推荐的写法为:setState(() { _myState = newValue; });通过前面分析可以知道_myState = newValue;setState(() {});效果是一样的,但是官方的写法明显更加优雅,所以推荐。
实现局部刷新
首先通过一张流程图了解一下setState()的工作机制
image.png
总结起来就是,调用一个Widget的setState()方法,它会将当前Widget及其所有子Widget都标记要重新刷新的状态,等待下一个Vsync刷新信号到来时重新刷新所有这些标记为刷新状态的控件。也就是调用他们的build方法,这里通过如下段代码验证一下:
import 'package:flutter/material.dart';
void main(){
runApp(StudyApp());
}
class StudyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'flutter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: new HomePage(),
);
}
}
class MyText extends StatefulWidget {
final String text;
MyText(this.text)
@override
State<StatefulWidget> createState() => _MyTextState();
}
class _MyTextState extends State<MyText> {
@override
Widget build(BuildContext context) {
print("刷新了呀2");
return Text(widget.text);
}
}
class HomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => _HomePageState();
}
class _HomePageState extends State<StatefulWidget> {
int title = 0;
@override
Widget build(BuildContext context) {
print("刷新了呀1");
return Scaffold(
appBar:AppBar(title:Text("flutter学习")),
body:Center(child:Column(crossAxisAlignment:CrossAxisAlignment.center,children: [
FlatButton(onPressed:(){
setState(() {
title += 1;
});
},child:Text("dart语法"),color:Colors.grey),
MyText("$title")
])
));
}
}
点击按钮后,控制台输出结果为:
flutter: 刷新了呀1
flutter: 刷新了呀2
实际项目中可能会遇到这样的场景,比如一个新闻列表页,每一个列表项为一个新闻的摘要,列表项有点赞按钮,用户点击按钮后,点赞按钮的状态发生改变。再比如一个商品详情页,里面包括大量的图片介绍信息,以及其它说明信息,里面有一个收藏按钮,用户点击收藏后改变收藏按钮的状态。等等很多类似需求,那么还按照上面类似的做法吗?我觉得不能,可能会造成不必要的性能问题,那么有没有一种办法做局部刷新呢,就是我只是调用点赞按钮或者收藏按钮的setState()方法,答案是有的。还是以上面代码为例,经过改造后如下:
import 'package:flutter/material.dart';
void main(){
runApp(StudyApp());
}
class StudyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'flutter',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: new HomePage(),
);
}
}
class MyText extends StatefulWidget {
final String text;
MyText(Key theKey,this.text):super(key:theKey);
@override
State<StatefulWidget> createState() => _MyTextState();
}
class _MyTextState extends State<MyText> {
@override
Widget build(BuildContext context) {
print("刷新了呀2");
return Text(widget.text);
}
}
class HomePage extends StatefulWidget {
@override
State<StatefulWidget> createState() => _HomePageState();
}
class _HomePageState extends State<StatefulWidget> {
int title = 0;
GlobalKey<_MyTextState> _txtKey = GlobalKey();
@override
Widget build(BuildContext context) {
print("刷新了呀1");
return Scaffold(
appBar:AppBar(title:Text("flutter学习")),
body:Center(child:Column(crossAxisAlignment:CrossAxisAlignment.center,children: [
FlatButton(onPressed:(){
_txtKey.currentState.setState(() {
title += 1;
});
},child:Text("dart语法"),color:Colors.grey),
MyText(_txtKey,"$title")
])
));
}
}
再次点击按钮,发现输出为:
flutter: 刷新了呀2
对,没错,只有指定的控件MyText的build执行的刷新,这里是通过保存GlobalKey这样一个对象来实现的。那么这里就有疑问了,GlobalKey是什么?这个东西和要刷新的Widget又是如何绑定的呢?为了了解其机制,这里仍然是去阅读源码了。
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
.....省略注释
final Key key;
...省略源码
}
可以看到Key作为Widget的成员变量存在,GlobalKey是Key的一个子类。接下来看一下GlobalKey的源码
abstract class GlobalKey<T extends State<StatefulWidget>> extends Key {
/// Creates a [LabeledGlobalKey], which is a [GlobalKey] with a label used for
/// debugging.
///
/// The label is purely for debugging and not used for comparing the identity
/// of the key.
factory GlobalKey({ String debugLabel }) => LabeledGlobalKey<T>(debugLabel);
/// Creates a global key without a label.
///
/// Used by subclasses because the factory constructor shadows the implicit
/// constructor.
const GlobalKey.constructor() : super.empty();
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
static final Set<Element> _debugIllFatedElements = HashSet<Element>();
// This map keeps track which child reserves the global key with the parent.
// Parent, child -> global key.
// This provides us a way to remove old reservation while parent rebuilds the
// child in the same slot.
static final Map<Element, Map<Element, GlobalKey>> _debugReservations = <Element, Map<Element, GlobalKey>>{};
static void _debugRemoveReservationFor(Element parent, Element child) {
assert(() {
assert(parent != null);
assert(child != null);
_debugReservations[parent]?.remove(child);
return true;
}());
}
void _register(Element element) {
assert(() {
if (_registry.containsKey(this)) {
assert(element.widget != null);
assert(_registry[this].widget != null);
assert(element.widget.runtimeType != _registry[this].widget.runtimeType);
_debugIllFatedElements.add(_registry[this]);
}
return true;
}());
_registry[this] = element;
}
void _unregister(Element element) {
assert(() {
if (_registry.containsKey(this) && _registry[this] != element) {
assert(element.widget != null);
assert(_registry[this].widget != null);
assert(element.widget.runtimeType != _registry[this].widget.runtimeType);
}
return true;
}());
if (_registry[this] == element)
_registry.remove(this);
}
.....省略源码
T get currentState {
final Element element = _currentElement;
if (element is StatefulElement) {
final StatefulElement statefulElement = element;
final State state = statefulElement.state;
if (state is T)
return state;
}
return null;
}
GlobaKey里面有一个_registry静态成员变量,它是一个Map类型,保存着key和它关联的Element,为什么是Element呢?这就是巧妙之处了,因为Element才是flutter的最终渲染所需的实例对象,只有这个实例对象有改变了才会重新去渲染,而且通过Element也可以拿到它绑定的State以及Widget。而且这里_registry为静态变量也避免了重复创建对象。那GlobalKey和Element是什么时候关联和解除关联的呢?请看如下源码:
void mount(Element parent, dynamic newSlot) {
assert(_debugLifecycleState == _ElementLifecycle.initial);
assert(widget != null);
assert(_parent == null);
assert(parent == null || parent._debugLifecycleState == _ElementLifecycle.active);
assert(slot == null);
assert(depth == null);
assert(!_active);
_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;
final Key key = widget.key;
if (key is GlobalKey) {
key._register(this);
}
_updateInheritance();
assert(() {
_debugLifecycleState = _ElementLifecycle.active;
return true;
}());
}
void unmount() {
assert(_debugLifecycleState == _ElementLifecycle.inactive);
assert(_widget != null); // Use the private property to avoid a CastError during hot reload.
assert(depth != null);
assert(!_active);
// Use the private property to avoid a CastError during hot reload.
final Key key = _widget.key;
if (key is GlobalKey) {
key._unregister(this);
}
assert(() {
_debugLifecycleState = _ElementLifecycle.defunct;
return true;
}());
}
即当Element首次被挂载到渲染树中时通过GlobalKey的_register()方法关联了起来,当Element被解除挂载时候通过GlobalKey的_unregister()解除关联
setState()的刷新机制及原理
为什么调用setState()方法之后,State的build()方法就会被执行,这整个流程是怎样的呢?通过阅读源码一步一步来了解(为了简洁,下面只有关键代码)
1、setState()方法源码
void setState(VoidCallback fn) {
_element.markNeedsBuild();
}
- 该方法调用State对应的Element对象(前面了解到这才是真正的渲染对象)的markNeedsBuild()方法
2、markNeedsBuild()
void markNeedsBuild() {
if (!_active)
return;
if (dirty)
return;
_dirty = true;
owner.scheduleBuildFor(this);
}
- 将Element的_dirty变量置为true,只有该变量为true那么当下一次Vsync信号来到时Element才会被重新build。同时将该Elemnt加入到owner中,owner代表该Element的父Element(一个Element可能有多个子控件组成)
3、scheduleBuildFor()
void scheduleBuildFor(Element element) {
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled();
}
_dirtyElements.add(element);
element._inDirtyList = true;
}
- 在父Element中将该Element标记为dirty,因为build的调用是从父控件到子控件逐层调用的,每一个父控件通过BuildOwner对象来管理它的子控件,接下来再调用onBuildScheduled();方法,注意这里的onBuildScheduled是一个回调方法,它在flutter启动时通过WidgetsBinding被初始化,具体的回调函数为_handleBuildScheduled
4、_handleBuildScheduled
mixin WidgetsBinding on BindingBase, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
void _handleBuildScheduled() {
ensureVisualUpdate(); //[见小节2.5]
}
}
5、ensureVisualUpdate()
void ensureVisualUpdate() {
switch (schedulerPhase) {
case SchedulerPhase.idle:
case SchedulerPhase.postFrameCallbacks:
scheduleFrame(); //[见小节2.6]
return;
case SchedulerPhase.transientCallbacks:
case SchedulerPhase.midFrameMicrotasks:
case SchedulerPhase.persistentCallbacks:
return;
}
}
6、接下来是scheduleFrame()
void scheduleFrame() {
if (_hasScheduledFrame || !framesEnabled)
return;
assert(() {
if (debugPrintScheduleFrameStacks)
debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');
return true;
}());
ensureFrameCallbacksRegistered();
window.scheduleFrame();
_hasScheduledFrame = true;
}
void scheduleFrame() native 'PlatformConfiguration_scheduleFrame';
这里会调用native方法scheduleFrame(),此方法内部最终会注册Vsync信号回调函数,当下一个Vsync信号来临时,这个回调函数最终又调用dart层的drawFrame()方法,如下:
7、handleBeginFrame()方法
mixin WidgetsBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, RendererBinding, SemanticsBinding {
/// Pump the build and rendering pipeline to generate a frame.
///
/// This method is called by [handleDrawFrame], which itself is called
/// automatically by the engine when it is time to lay out and paint a
/// frame.
///
/// Each frame consists of the following phases:
///
/// 1. The animation phase: The [handleBeginFrame] method, which is registered
/// with [Window.onBeginFrame], invokes all the transient frame callbacks
/// registered with [scheduleFrameCallback], in
/// registration order. This includes all the [Ticker] instances that are
/// driving [AnimationController] objects, which means all of the active
/// [Animation] objects tick at this point.
///
/// 2. Microtasks: After [handleBeginFrame] returns, any microtasks that got
/// scheduled by transient frame callbacks get to run. This typically includes
/// callbacks for futures from [Ticker]s and [AnimationController]s that
/// completed this frame.
///
/// After [handleBeginFrame], [handleDrawFrame], which is registered with
/// [Window.onDrawFrame], is called, which invokes all the persistent frame
/// callbacks, of which the most notable is this method, [drawFrame], which
/// proceeds as follows:
///
/// 3. The build phase: All the dirty [Element]s in the widget tree are
/// rebuilt (see [State.build]). See [State.setState] for further details on
/// marking a widget dirty for building. See [BuildOwner] for more information
/// on this step.
///
/// 4. The layout phase: All the dirty [RenderObject]s in the system are laid
/// out (see [RenderObject.performLayout]). See [RenderObject.markNeedsLayout]
/// for further details on marking an object dirty for layout.
///
/// 5. The compositing bits phase: The compositing bits on any dirty
/// [RenderObject] objects are updated. See
/// [RenderObject.markNeedsCompositingBitsUpdate].
///
/// 6. The paint phase: All the dirty [RenderObject]s in the system are
/// repainted (see [RenderObject.paint]). This generates the [Layer] tree. See
/// [RenderObject.markNeedsPaint] for further details on marking an object
/// dirty for paint.
///
/// 7. The compositing phase: The layer tree is turned into a [Scene] and
/// sent to the GPU.
///
/// 8. The semantics phase: All the dirty [RenderObject]s in the system have
/// their semantics updated (see [RenderObject.assembleSemanticsNode]). This
/// generates the [SemanticsNode] tree. See
/// [RenderObject.markNeedsSemanticsUpdate] for further details on marking an
/// object dirty for semantics.
///
/// For more details on steps 4-8, see [PipelineOwner].
///
/// 9. The finalization phase in the widgets layer: The widgets tree is
/// finalized. This causes [State.dispose] to be invoked on any objects that
/// were removed from the widgets tree this frame. See
/// [BuildOwner.finalizeTree] for more details.
///
/// 10. The finalization phase in the scheduler layer: After [drawFrame]
/// returns, [handleDrawFrame] then invokes post-frame callbacks (registered
/// with [addPostFrameCallback]).
//
// When editing the above, also update rendering/binding.dart's copy.
@override
void drawFrame() {
assert(!debugBuildingDirtyElements);
assert(() {
debugBuildingDirtyElements = true;
return true;
}());
TimingsCallback firstFrameCallback;
if (_needToReportFirstFrame) {
assert(!_firstFrameCompleter.isCompleted);
firstFrameCallback = (List<FrameTiming> timings) {
assert(sendFramesToEngine);
if (!kReleaseMode) {
developer.Timeline.instantSync('Rasterized first useful frame');
developer.postEvent('Flutter.FirstFrame', <String, dynamic>{});
}
SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback);
firstFrameCallback = null;
_firstFrameCompleter.complete();
};
// Callback is only invoked when [Window.render] is called. When
// [sendFramesToEngine] is set to false during the frame, it will not
// be called and we need to remove the callback (see below).
SchedulerBinding.instance.addTimingsCallback(firstFrameCallback);
}
try {
if (renderViewElement != null)
buildOwner.buildScope(renderViewElement);
super.drawFrame();
buildOwner.finalizeTree();
} finally {
assert(() {
debugBuildingDirtyElements = false;
return true;
}());
}
if (!kReleaseMode) {
if (_needToReportFirstFrame && sendFramesToEngine) {
developer.Timeline.instantSync('Widgets built first useful frame');
}
}
_needToReportFirstFrame = false;
if (firstFrameCallback != null && !sendFramesToEngine) {
// This frame is deferred and not the first frame sent to the engine that
// should be reported.
_needToReportFirstFrame = true;
SchedulerBinding.instance.removeTimingsCallback(firstFrameCallback);
}
}
}
这个函数的注释解释了每一渲染的步骤,可以看到在执行build函数之前会优先执行动画相关代码,和microtask中的相关任务,所以如果microtask中任务过重,那有可能影响页面流畅度
这个函数内部通过buildOwner.buildScope(renderViewElement);调用进入Widgets的build函数流程,点进去这个函数看看
void buildScope(Element context, [ VoidCallback callback ]) {
....省略
Timeline.startSync('Build', arguments: timelineArgumentsIndicatingLandmarkEvent);
....省略
}
可以看到它是通过Timeline调用了Build,然后native又从c++层回调到dart层的build方法的
至此,setState()调用后再如何回调到State的build函数的整个流畅就分析清楚了
网友评论