Flutter Provider原理深入浅出(下)

Flutter Provider原理深入浅出(上) 从Provider的用法入手,深入分析了Provider原理相关的源码,Flutter Provider原理深入浅出(中)结合启动原理分析了Widget tree和Element tree是如何建立关系的,并解释了遗留的一些疑问。本文将重点分析Provider是如何结合Flutter的渲染原理来实现状态改变影响UI的效果。

原理上第四节Consumer源码分析中,我们分析到buildWithChild是系统调用的,那深入源码,我们看看具体是怎么调用的,直接上堆栈

Consumer.buildWithChild (consumer.dart:177)
SingleChildStatelessWidget.build (nested.dart:260)
StatelessElement.build (framework.dart:4701)
SingleChildStatelessElement.build (nested.dart:280)
ComponentElement.performRebuild (framework.dart:4627)
Element.rebuild (framework.dart:4343)
BuildOwner.buildScope (framework.dart:2730)
----------------------------------------------------------------------------------------------------------
WidgetsBinding.drawFrame (binding.dart:913)
RendererBinding._handlePersistentFrameCallback (binding.dart:302)
SchedulerBinding._invokeFrameCallback (binding.dart:1117)
SchedulerBinding.handleDrawFrame (binding.dart:1055)
SchedulerBinding._handleDrawFrame (binding.dart:971)
----------------------------------------------------------------------------------------------------------
_rootRun (zone.dart:1190)
_CustomZone.run (zone.dart:1093)
_CustomZone.runGuarded (zone.dart:997)
_invoke (hooks.dart:251)
_drawFrame (hooks.dart:209)

从下往上_drawFrame是dart虚拟机调用的,里面涉及到UI的渲染原理,具体可以移步这里,1.22.6关于Consumer的源码已经做了改动,但原理基本相似,先看下改动后的源码。

class Consumer<T> extends StatelessWidget
    implements SingleChildCloneableWidget {
  /// {@template provider.consumer.constructor}
  /// Consumes a [Provider<T>]
  /// {@endtemplate}
  Consumer({
    Key key,
    @required this.builder,
    this.child,
  })  : assert(builder != null),
        super(key: key);

  /// The child widget to pass to [builder].
  final Widget child;

  /// {@template provider.consumer.builder}
  /// Build a widget tree based on the value from a [Provider<T>].
  ///
  /// Must not be `null`.
  /// {@endtemplate}
  final Widget Function(BuildContext context, T value, Widget child) builder;

  @override
  Widget build(BuildContext context) {
    return builder(
      context,
      Provider.of<T>(context),
      child,
    );
  }

  @override
  Consumer<T> cloneWithChild(Widget child) {
    return Consumer(
      key: key,
      builder: builder,
      child: child,
    );
  }
}

对比1.21.x版本,已经不存在buildWithChild抽象方法了,取而代之的是cloneWithChildbuild,这里build是没有回传child参数的,这里似乎回应了之前我们有疑问的回传了child但并没使用的问题,但具体原因有待考察,这里build确实没有拿到之前的child,但是回调业务层,是把外面传给Consumerchild又回传出去了。

新版本(1.22.6)的堆栈

ProviderDebugPage1.build.<anonymous closure> (test_provider_page1.dart:37)
Consumer.build (consumer.dart:180)
StatelessElement.build (framework.dart:4701)
ComponentElement.performRebuild (framework.dart:4627)
Element.rebuild (framework.dart:4343)
ComponentElement._firstBuild (framework.dart:4606)
ComponentElement.mount (framework.dart:4601)
这里省略若干层

Element.rebuild (framework.dart:4343)
StatelessElement.update (framework.dart:4708)
Element.updateChild (framework.dart:3314)
ComponentElement.performRebuild (framework.dart:4652)
Element.rebuild (framework.dart:4343)
ProxyElement.update (framework.dart:4987)
Element.updateChild (framework.dart:3314)
ComponentElement.performRebuild (framework.dart:4652)
StatefulElement.performRebuild (framework.dart:4800)
Element.rebuild (framework.dart:4343)
BuildOwner.buildScope (framework.dart:2730)
-------------------------------------------------------------------------------------------------------
WidgetsBinding.drawFrame (binding.dart:913)
RendererBinding._handlePersistentFrameCallback (binding.dart:302)
SchedulerBinding._invokeFrameCallback (binding.dart:1117)
SchedulerBinding.handleDrawFrame (binding.dart:1055)
SchedulerBinding._handleDrawFrame (binding.dart:971)
-------------------------------------------------------------------------------------------------------
_rootRun (zone.dart:1190)
_CustomZone.run (zone.dart:1093)
_CustomZone.runGuarded (zone.dart:997)
_invoke (hooks.dart:251)
_drawFrame (hooks.dart:209)

SchedulerBinding._handleDrawFrame还是分界线,这里标识dart层渲染的起点,_handleDrawFrame是C++层onDrawFrame在dart的回调方法。而WidgetsBinding.drawFrameRendererBinding初始化的时候注册进去的,初始化方法如下:

mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
  @override
  void initInstances() {
    super.initInstances();
    _instance = this;
    _pipelineOwner = PipelineOwner(
      onNeedVisualUpdate: ensureVisualUpdate,
      onSemanticsOwnerCreated: _handleSemanticsOwnerCreated,
      onSemanticsOwnerDisposed: _handleSemanticsOwnerDisposed,
    );
    window
      ..onMetricsChanged = handleMetricsChanged
      ..onTextScaleFactorChanged = handleTextScaleFactorChanged
      ..onPlatformBrightnessChanged = handlePlatformBrightnessChanged
      ..onSemanticsEnabledChanged = _handleSemanticsEnabledChanged
      ..onSemanticsAction = _handleSemanticsAction;
    initRenderView();
    _handleSemanticsEnabledChanged();
    assert(renderView != null);
    addPersistentFrameCallback(_handlePersistentFrameCallback);
    initMouseTracker();
  }
}
  void _handlePersistentFrameCallback(Duration timeStamp) {
    drawFrame();
    _scheduleMouseTrackerUpdate();
  }

其中WidgetsBinding.drawFrame调用了buildOwner.buildScope(renderViewElement);,这里的renderViewElement只有一个地方进行赋值,就是attachRootWidget

void attachRootWidget(Widget rootWidget) {
    _readyToProduceFrames = true;
    _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
      container: renderView,
      debugShortDescription: '[root]',
      child: rootWidget,
    ).attachToRenderTree(buildOwner, renderViewElement as RenderObjectToWidgetElement<RenderBox>);
  }

根据原理中介绍,attachRootWidget是启动的时候调用的,这里的_renderViewElement就是runApp时候传入的widget构造的对应的element

image.png

buildScope调用了callback()

  void buildScope(Element context, [ VoidCallback callback ]) {
    if (callback == null && _dirtyElements.isEmpty)
      return;
    assert(context != null);
    assert(_debugStateLockLevel >= 0);
    assert(!_debugBuilding);
    assert(() {
      if (debugPrintBuildScope)
        debugPrint('buildScope called with context $context; dirty list is: $_dirtyElements');
      _debugStateLockLevel += 1;
      _debugBuilding = true;
      return true;
    }());
    Timeline.startSync('Build', arguments: timelineArgumentsIndicatingLandmarkEvent);
    try {
      _scheduledFlushDirtyElements = true;
      if (callback != null) {
        assert(_debugStateLocked);
        Element debugPreviousBuildTarget;
        assert(() {
          context._debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
          debugPreviousBuildTarget = _debugCurrentBuildTarget;
          _debugCurrentBuildTarget = context;
          return true;
        }());
        _dirtyElementsNeedsResorting = false;
        try {
          callback();
        } finally {
          assert(() {
            context._debugSetAllowIgnoredCallsToMarkNeedsBuild(false);
            assert(_debugCurrentBuildTarget == context);
            _debugCurrentBuildTarget = debugPreviousBuildTarget;
            _debugElementWasRebuilt(context);
            return true;
          }());
        }
      }
      _dirtyElements.sort(Element._sort);
      _dirtyElementsNeedsResorting = false;
      int dirtyCount = _dirtyElements.length;
      int index = 0;
      while (index < dirtyCount) {
        assert(_dirtyElements[index] != null);
        assert(_dirtyElements[index]._inDirtyList);
        assert(() {
          if (_dirtyElements[index]._active && !_dirtyElements[index]._debugIsInScope(context)) {
            throw FlutterError.fromParts(<DiagnosticsNode>[
              ErrorSummary('Tried to build dirty widget in the wrong build scope.'),
              ErrorDescription(
                'A widget which was marked as dirty and is still active was scheduled to be built, '
                'but the current build scope unexpectedly does not contain that widget.',
              ),
              ErrorHint(
                'Sometimes this is detected when an element is removed from the widget tree, but the '
                'element somehow did not get marked as inactive. In that case, it might be caused by '
                'an ancestor element failing to implement visitChildren correctly, thus preventing '
                'some or all of its descendants from being correctly deactivated.',
              ),
              DiagnosticsProperty<Element>(
                'The root of the build scope was',
                context,
                style: DiagnosticsTreeStyle.errorProperty,
              ),
              DiagnosticsProperty<Element>(
                'The offending element (which does not appear to be a descendant of the root of the build scope) was',
                _dirtyElements[index],
                style: DiagnosticsTreeStyle.errorProperty,
              ),
            ]);
          }
          return true;
        }());
        try {
          _dirtyElements[index].rebuild();
        } catch (e, stack) {
          _debugReportException(
            ErrorDescription('while rebuilding dirty elements'),
            e,
            stack,
            informationCollector: () sync* {
              if (index < _dirtyElements.length) {
                yield DiagnosticsDebugCreator(DebugCreator(_dirtyElements[index]));
                yield _dirtyElements[index].describeElement('The element being rebuilt at the time was index $index of $dirtyCount');
              } else {
                yield ErrorHint('The element being rebuilt at the time was index $index of $dirtyCount, but _dirtyElements only had ${_dirtyElements.length} entries. This suggests some confusion in the framework internals.');
              }
            },
          );
        }
        index += 1;
        if (dirtyCount < _dirtyElements.length || _dirtyElementsNeedsResorting) {
          _dirtyElements.sort(Element._sort);
          _dirtyElementsNeedsResorting = false;
          dirtyCount = _dirtyElements.length;
          while (index > 0 && _dirtyElements[index - 1].dirty) {
            // It is possible for previously dirty but inactive widgets to move right in the list.
            // We therefore have to move the index left in the list to account for this.
            // We don't know how many could have moved. However, we do know that the only possible
            // change to the list is that nodes that were previously to the left of the index have
            // now moved to be to the right of the right-most cleaned node, and we do know that
            // all the clean nodes were to the left of the index. So we move the index left
            // until just after the right-most clean node.
            index -= 1;
          }
        }
      }
      assert(() {
        if (_dirtyElements.any((Element element) => element._active && element.dirty)) {
          throw FlutterError.fromParts(<DiagnosticsNode>[
            ErrorSummary('buildScope missed some dirty elements.'),
            ErrorHint('This probably indicates that the dirty list should have been resorted but was not.'),
            Element.describeElements('The list of dirty elements at the end of the buildScope call was', _dirtyElements),
          ]);
        }
        return true;
      }());
    } finally {
      for (final Element element in _dirtyElements) {
        assert(element._inDirtyList);
        element._inDirtyList = false;
      }
      _dirtyElements.clear();
      _scheduledFlushDirtyElements = false;
      _dirtyElementsNeedsResorting = null;
      Timeline.finishSync();
      assert(_debugBuilding);
      assert(() {
        _debugBuilding = false;
        _debugStateLockLevel -= 1;
        if (debugPrintBuildScope)
          debugPrint('buildScope finished');
        return true;
      }());
    }
    assert(_debugStateLockLevel >= 0);
  }

这里的callbackattachRootWidget初始化的时候attachToRenderTree中传了回调element.mount(null, null); 所以,当Widget Tree被渲染为Element Tree后会调用mount,后续当视图树发生变化后,_dirtyElements将存放这些子视图,并遍历进行rebuild操作,如果有子类就继续进行子类的rebuildStatelessElement调用rebuild后执行了widget.build(this),也就触发了Consumer.build,也就和新版本(1.22.6)的堆栈对应上了,这里其实也是一个视图树的构建过程,通过Consumerbuilder参数来完成这个页面对应Widget的构建。那讲到这里我们明白了,初始化的时候系统触发了UI渲染机制,一步步的完成了UI的初始化工作(Widget Tree 转化成 Element Tree),接下来就是当Model的数据发生变化时,是如果改变UI的。
我们尝试修改修改Model中的数据,得到下面的堆栈

ProviderDebugPage1.build.<anonymous closure> (test_provider_page1.dart:37)
Consumer.build (consumer.dart:180)
StatelessElement.build (framework.dart:4701)
ComponentElement.performRebuild (framework.dart:4627)
Element.rebuild (framework.dart:4343)
BuildOwner.buildScope (framework.dart:2730)
WidgetsBinding.drawFrame (binding.dart:913)
RendererBinding._handlePersistentFrameCallback (binding.dart:302)
SchedulerBinding._invokeFrameCallback (binding.dart:1117)
SchedulerBinding.handleDrawFrame (binding.dart:1055)
SchedulerBinding._handleDrawFrame (binding.dart:971)
_rootRun (zone.dart:1190)
_CustomZone.run (zone.dart:1093)
_CustomZone.runGuarded (zone.dart:997)
_invoke (hooks.dart:251)
_drawFrame (hooks.dart:209)

对应的我们发现,_dirtyElements是这样的

image.png

这里,我们发现,当前页面,用到Model1的是_dirtyElements[1]和_dirtyElements[2],所以这也符合我们的预期,我们只剩下最后一个环节,Model1中的数据改变是怎样影响_dirtyElements的,又是如何触发重新渲染的。
我们看到,触发数据改变的时候,产生如下的堆栈

BuildOwner.scheduleBuildFor (framework.dart:2546)
Element.markNeedsBuild (framework.dart:4311)
State.setState (framework.dart:1264)
_ListenableDelegateMixin.startListening.<anonymous closure> (listenable_provider.dart:190)
ChangeNotifier.notifyListeners (change_notifier.dart:226)
MyModel1.incrementCounter (test_provider_model1.dart:13)
MyModel1.incrementCounter (test_provider_model1.dart:1)
_InkResponseState._handleTap (ink_well.dart:993)
_InkResponseState.build.<anonymous closure> (ink_well.dart:1111)
GestureRecognizer.invokeCallback (recognizer.dart:183)
TapGestureRecognizer.handleTapUp (tap.dart:598)
BaseTapGestureRecognizer._checkUp (tap.dart:287)
BaseTapGestureRecognizer.handlePrimaryPointer (tap.dart:222)
PrimaryPointerGestureRecognizer.handleEvent (recognizer.dart:476)
PrimaryPointerGestureRecognizer.handleEvent (recognizer.dart:1)
PointerRouter._dispatch (pointer_router.dart:77)
PointerRouter._dispatchEventToRoutes.<anonymous closure> (pointer_router.dart:122)
_LinkedHashMapMixin.forEach (compact_hash.dart:377)
PointerRouter._dispatchEventToRoutes (pointer_router.dart:120)
PointerRouter.route (pointer_router.dart:106)
GestureBinding.handleEvent (binding.dart:358)
GestureBinding.dispatchEvent (binding.dart:338)
RendererBinding.dispatchEvent (binding.dart:267)
GestureBinding._handlePointerEvent (binding.dart:295)
GestureBinding._flushPointerEventQueue (binding.dart:240)
GestureBinding._handlePointerDataPacket (binding.dart:213)
GestureBinding._handlePointerDataPacket (binding.dart:1)
_rootRunUnary (zone.dart:1206)
_rootRunUnary (zone.dart:1)
_CustomZone.runUnary (zone.dart:1100)
_CustomZone.runUnaryGuarded (zone.dart:1005)
_invoke1 (hooks.dart:265)
_dispatchPointerDataPacket (hooks.dart:174)

当Model数据发生变化时,会调用到setState,最终触发BuildOwner.scheduleBuildFor执行,里面调用了_dirtyElements.add(element)

BuildOwner.scheduleBuildFor (framework.dart:2590)
Element.markNeedsBuild (framework.dart:4311)
Element.didChangeDependencies (framework.dart:4130)
InheritedElement.notifyDependent (framework.dart:5205)
InheritedElement.notifyClients (framework.dart:5244)
ProxyElement.updated (framework.dart:4997)
InheritedElement.updated (framework.dart:5217)
ProxyElement.update (framework.dart:4985)
Element.updateChild (framework.dart:3314)
ComponentElement.performRebuild (framework.dart:4652)
StatefulElement.performRebuild (framework.dart:4800)
Element.rebuild (framework.dart:4343)
BuildOwner.buildScope (framework.dart:2730)
WidgetsBinding.drawFrame (binding.dart:913)
RendererBinding._handlePersistentFrameCallback (binding.dart:302)
SchedulerBinding._invokeFrameCallback (binding.dart:1117)
SchedulerBinding.handleDrawFrame (binding.dart:1055)
SchedulerBinding._handleDrawFrame (binding.dart:971)
_rootRun (zone.dart:1190)
_CustomZone.run (zone.dart:1093)
_CustomZone.runGuarded (zone.dart:997)
_invoke (hooks.dart:251)
_drawFrame (hooks.dart:209)

同样didChangeDependencies也会触发UI刷新,上面notifyListeners触发markNeedsBuild是因为,在使用前调用了addListener,demo中,Model1addListener是在attachRootWidget中调用的,也可以任何时机,只要在触发Model1数据变化前进行就可以,所以当后面Model1中的数据发生变化的时候才能触发setState

最后,谁来触发_dirtyElements,简单说,还是系统,引用中的一段描述,当调用到引擎Engine的ScheduleFrame()方法过程则会注册VSYNC信号回调,一旦Vsync信号达到,则会调用到doFrame()方法。所以VSYNC会周期性的回调,来调用drawFrame,当发现有_dirtyElements,就rebuild

总结:
  • 数据的改变一定要调用markNeedsBuild才能让UI刷新到最新的数据,而Provider内部实现了这样的机制,当我们的Model with 或者 extends ChangeNotifier的时候就具备了这样的潜力,只要在Model实例数据发生变化的时候调用notifyListeners即可。
  • 使用Provider.of监听数据变化的原理类似,都是在想办法让数据发生变化时,Element出现在_dirtyElements中,作为Widget在构建Element Tree的时候,就已经将widget和element建立了关系,当数据改变触发rebuild的时候,element可以找到对应的widget进行rebuild,所以我们可以看到最新的数据呈现在UI上。
  • Consumer原理也是一样的,因为Consumer底层也是调用Provider.of,可以说ConsumerProvider.of的包装,对用户更加友好。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容