美文网首页
Flutter中关于setState的理解(三)

Flutter中关于setState的理解(三)

作者: 仙人掌__ | 来源:发表于2021-03-10 12:19 被阅读0次

前言

平常在使用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函数的整个流畅就分析清楚了

相关文章

网友评论

      本文标题:Flutter中关于setState的理解(三)

      本文链接:https://www.haomeiwen.com/subject/tqtqqltx.html