美文网首页
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