一文搞懂InheritedWidget局部刷新机制

Posted 一叶飘舟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一文搞懂InheritedWidget局部刷新机制相关的知识,希望对你有一定的参考价值。

InheritedWidget与 StatefulWidget 的区别

首先,InheritedWidget 和 StatefulWidget 的继承链不同,对比如下。 

 InheritedWidget继承自 ProxyWidget,之后才是 Widget,而 StatefulWidget 直接继承 Widget。 其二是创建的渲染元素类不同,InheritedWidget 的 createElement 返回的是InheritedElement,而 StatefulWidget 的 createElement 返回的是StatefulElement

我们在上一篇已经知道,实际的渲染控制是有 Element 类来完成的,实际上Widget 的createElement 方法就是将 Widget 对象传给 Element 对象,由 Element 对象根据 Widget 的组件配置来决定如何渲染。

InhretiedWidget 的定义很简单,如下所示:

abstract class InheritedWidget extends ProxyWidget 
  const InheritedWidget(Key? key, required Widget child)
      : super(key: key, child: child);

  @override
  InheritedElement createElement() => InheritedElement(this);

  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);

updateShouldNotify方法用于 InheritedWidget 的子类实现,已决定是否通知其子组件(widget)。例如,如果数据没有发生改变(典型的如下拉刷新没有新的数据),那么就可以返回 false,从而无需更新子组件,减少性能消耗。之前我们的 ModelBinding 例子中是直接返回了 true,也就是每次发生变化都会通知子组件。接下来就看 InheritedElement 和 StatefulElement 的区别了。

InheritedElement 与 StatefulElement 的区别

上一篇我们已经分析过 StatefulElement 了,他在 setState 后会调用重建方法 performRebuildperformRebuild 方法在父类Component 中实现的。核心是当 Widget 树发生改变后,根据新的 Widget 树调用 updateChild 方法来更新子元素。

而上一篇的 ModelBinding 调用 setState 的时候,因为它自身是一个 StatefulWidget,毫无疑问它也会调用到 updateChild来更新子元素。从执行结果来看,由于 ModelBinding 的例子中没有出现重新构建 Widget 树的情况,因此应该是在 updateChild 前的处理不同。 在 updateChild 之前会调用组件的 build 方法来获取新的 Widget 树。是这里不同吗?继续往下看。

与 InheritedWidget 对应,InheritedElement上面还多了一层继承,那就是 ProxyElement。而恰恰在 ProxyElement 我们找到了build 方法。与 StatefulElement不同,这里的 build 方法没有调用对应 Widget 对象的 build 方法,而是直接返回了 widget.child

// ProxyElement的 build 方法
@override
Widget build() => widget.child;

// StatefulElement 的 build 方法
@override
Widget build() => state.build(this);

// StatelessElement 的 build方法
@override
Widget build() => widget.build(this);

由此我们就知道了为什么 InheritedWidget在状态更新的时候为什么没有重新构建其子组件树了,这是因为在ProxyElement中直接就返回了已经构建的子组件树,而不是重建。你是不是以为真相大白了?说好的刨根问底呢?难道我们不应该问问如果子组件树发生了改变,ProxyElement 是如何感知的?比如插入了一个新的元素,或者某个元素的渲染参数变了(颜色,字体,内容等),渲染层是怎么知道的?继续继续! 

InheritedElement如何感知组件树的变化

先看一下 InheritedElement 的类结构。

 

从类结构上看也不复杂,这是因为大部分渲染的管理已经在父类的 ComponentElement 和 Element 中完成了。build 方法我们已经讲过了,重点来看一下在 InheritedWidget 的父组件调用 setState 后的过程。 我们在子组件需要获取状态管理的时候,使用的方法是:

ModelBindingV2.of<FaceEmotion>(context)

这个方法实际调用的是:

_ModelBindingScope<T> scope =
  context.dependOnInheritedWidgetOfExactType(aspect: _ModelBindingScope);

这里的dependOnInheritedWidgetOfExactType方法在 BuildContext定义,但实际上是Element 实现。这里会访问一个HashMap 对象_inheritedWidgets,从数组中找到对应类型的InheritedElement

@override
InheritedWidget dependOnInheritedElement(InheritedElement ancestor,
    Object? aspect) 
  assert(ancestor != null);
  _dependencies ??= HashSet<InheritedElement>();
  _dependencies!.add(ancestor);
  ancestor.updateDependencies(this, aspect);
  return ancestor.widget;


@override
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>(
    Object? aspect) 
  assert(_debugCheckStateIsActiveForAncestorLookup());
  final InheritedElement? ancestor =
      _inheritedWidgets == null ? null : _inheritedWidgets![T];
  if (ancestor != null) 
    assert(ancestor is InheritedElement);
    return dependOnInheritedElement(ancestor, aspect: aspect) as T;
  
  _hadUnsatisfiedDependencies = true;
  return null;

这个数组实际上是在 mount 方法中调用_updateInheritance 中完成初始化的。而在InheritedElement 中重载了 Element 的这个方法。也就是在创建 InheritedWidget 的时候,在 mount 中就将 InheritedElement 与对应的组件运行时类型进行了关联。

@override
void _updateInheritance() 
  assert(_lifecycleState == _ElementLifecycle.active);
  final Map<Type, InheritedElement>? incomingWidgets =
      _parent?._inheritedWidgets;
  if (incomingWidgets != null)
    _inheritedWidgets = HashMap<Type, InheritedElement>.from(incomingWidgets);
  else
    _inheritedWidgets = HashMap<Type, InheritedElement>();
  _inheritedWidgets![widget.runtimeType] = this;

首先这个方法会将父级的全部 InheritedWidgets延续下来,然后在将自己(InheritedElement)存入到这个 HashMap中,以便后续能够找到该元素。

因此,当在子组件中使用dependOnInheritedWidgetOfExactType的时候,实际上执行的是 dependOnInheritedElement 方法,传递的参数是通过类型找到的 InheritedElement 元素和指定的 InheritedWidget 类型参数 aspect,这里就是我们的_ModeBindScope<T>,然后会将当前的渲染元素(Element 子类)与其绑定,告知 InheritedElement对象这个组件会依赖于它的InheritedWidget。我们从调试的结果可以看到,在_dependents 中存在了这么一个对象。就这样,InheritedElement 就和组件对应的渲染元素建立了联系。

接下来就是看 setState 后,怎么获取新的组件树和更新组件了。我们已经知道了setState 的时候会调用 performRebuild 方法,在 performRebuild 中会调用 Element 的 updateChild 方法,现在来看InheritedElementupdateChild 做了什么事情。实际上 updateChild 会调用 child.update(newWidget)方法:

 else if (hasSameSuperclass &&
      Widget.canUpdate(child.widget, newWidget)) 
    if (child.slot != newSlot) updateSlotForChild(child, newSlot);
    child.update(newWidget);
    //...
    newChild = child;
 

// ...

return newChild;

而在 ProxyElement 中,重写了 update 方法。

@override
void update(ProxyWidget newWidget) 
  final ProxyWidget oldWidget = widget;
  assert(widget != null);
  assert(widget != newWidget);
  super.update(newWidget);
  assert(widget == newWidget);
  updated(oldWidget);
  _dirty = true;
  rebuild();

这里的 newWidget 是 setState 的时候构建的新的组件配置,因此和 oldWidget 并不相同。对于 InheritedWidget,它会先调用updated(oldWidget),这个方法实际上就是通知依赖 InheirtedWidget 的组件更新:

@protected
void updated(covariant ProxyWidget oldWidget) 
  notifyClients(oldWidget);


// InheritedElement类
@override
void updated(InheritedWidget oldWidget) 
  if (widget.updateShouldNotify(oldWidget)) super.updated(oldWidget);


@protected
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) 
  dependent.didChangeDependencies();


@override
void notifyClients(InheritedWidget oldWidget) 
  assert(_debugCheckOwnerBuildTargetExists('notifyClients'));
  for (final Element dependent in _dependents.keys) 
      assert(() 
        // check that it really is our descendant
        Element? ancestor = dependent._parent;
        while (ancestor != this && ancestor != null)
          ancestor = ancestor._parent;
        return ancestor == this;
      ());
      // check that it really depends on us
      assert(dependent._dependencies!.contains(this));
      notifyDependent(oldWidget, dependent);
    
  

实际上最终调用了依赖 InheritedWidget 组件渲染元素的 didChangeDependencies 方法,我们在这个方法打印出来看一下。 

 在元素的 didChangeDependencies 中就会调用 markNeedsBuild将元素标记为需要更新,然后后续的过程就和 StatefulElement 的一样了。而对于没有依赖状态的元素,因为没有在_dependent 中,因此不会被更新。 而 ModelBinding 所在的组件是 StatelessWidget,因此最初的这个 Widget 配置树一旦创建就不会改变,而子组件树如果要 改变的话只有两种情况: 1、子组件是 StatefulWidget,通过setState 改变,那这不属于 InheritedWidget 的范畴了,而是通过 StatefulWidget 的更新方式完成——当然,这种做法不推荐。 2、子组件的组件树改变依赖于状态吗,那这个时候自然会在状态改变的时候更新。

由此,我们终于弄明白了InheritedWidget的组件树的感知和通知子组件刷新过程。

总结

从 InheritedWidget 实现组件渲染的过程来看,整个过程分为下面几个步骤:

  • mount 阶段将组件树运行时类型与对应的 InheritedElement绑定,存入到 _inheritedWidgets 这个 HashMap 中;
  • 在子组件添加对状态的依赖的时候,实际上将子组件对应的 Element 元素与InheritedElement(具体的 Element 对象从_inheritedWidgets中获取)进行了绑定,存入到了_dependents 这个 HashMap 中;
  • 当状态更新的时候,InheritedElement 直接使用旧的组件配置通知子元素的依赖发生了改变,这是通过调用Element 的 didChangeDependencies 方法完成的。
  • 在Element的didChangeDependencies将元素标记为需要更新,等待下一帧刷新。
  • 而对于没有依赖状态的子组件,则不会被加入到_dependent 中,因此不会被通知刷新,进而提高性能。

状态管理的原理性文章讲了好几篇了,通过这些文章希望能够达到知其然,知其所以然的目的。实际上,Flutter 的组件渲染的核心就在于如何选择状态管理来实现组件的渲染,这个对性能影响很大。接下来我们将以状态管理插件的应用方式,讲述在实际例子中的应用。

以上是关于一文搞懂InheritedWidget局部刷新机制的主要内容,如果未能解决你的问题,请参考以下文章

一文搞懂JavaScript垃圾回收机制

一文搞懂JavaScript垃圾回收机制

一文搞懂 PyTorch 内部机制

一文搞懂php的垃圾回收机制

一文搞懂php的垃圾回收机制

一文搞懂Java的SPI机制