Flutter 入門與實戰(四十五):萬字長文!一文搞懂InheritedWidget 區域性重新整理機制

島上碼農發表於2021-08-08

這是我參與8月更文挑戰的第8天,活動詳情檢視:8月更文挑戰

前言

上一篇我們從原始碼角度分析了 setState 的過程,從而瞭解到為什麼 setState 方法被呼叫的時候會重新構建整個 Widget 樹。但是,Widget 樹的重新構建並不意味著渲染元素樹也需要重新構建,事實上渲染樹只是做了更新,而不一定是移除後在渲染。

但是,我們的 ModelBinding類也是使用了 setState 進行狀態更新的,為什麼它的子元件沒有重新構建,而只是更新了依賴於狀態的子元件的 build 方法呢?除了使用了內部的 InheritedWidget包裹了子元件外,其他和普通的 StatefulWidget 沒什麼區別。如前面兩篇分析 從InheritedWidget瞭解狀態管理一樣,差別就是在這個 InheritedWidget上。本著技術人刨根問底的精神,本篇就來看一下 InheritedWidget 在呼叫 setState的時候究竟有什麼不同。

image.png

知其然,知其所以然。在閱讀本篇文章前,如果對 Flutter 的狀態管理不是特別清楚的,建議閱讀前幾篇文章瞭解一下背景:

InheritedWidget與 StatefulWidget 的區別

首先,InheritedWidgetStatefulWidget 的繼承鏈不同,對比如下。 渲染過程-Stateful和 Inherited 對比.png InheritedWidget繼承自 ProxyWidget,之後才是 Widget,而 StatefulWidget 直接繼承 Widget。 其二是建立的渲染元素類不同,InheritedWidgetcreateElement 返回的是InheritedElement,而 StatefulWidgetcreateElement 返回的是StatefulElement

我們在上一篇已經知道,實際的渲染控制是有 Element 類來完成的,實際上WidgetcreateElement 方法就是將 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,也就是每次發生變化都會通知子元件。接下來就看 InheritedElementStatefulElement 的區別了。

InheritedElement 與 StatefulElement 的區別

上一篇我們已經分析過 StatefulElement 了,他在 setState 後會呼叫重建方法 performRebuildperformRebuild 方法在父類Component 中實現的。核心是當 Widget 樹發生改變後,根據新的 Widget 樹呼叫 updateChild 方法來更新子元素。

而上一篇的 ModelBinding 呼叫 setState 的時候,因為它自身是一個 StatefulWidget,毫無疑問它也會呼叫到 updateChild來更新子元素。從執行結果來看,由於 ModelBinding 的例子中沒有出現重新構建 Widget 樹的情況,因此應該是在 updateChild 前的處理不同。 在 updateChild 之前會呼叫元件的 build 方法來獲取新的 Widget 樹。是這裡不同嗎?繼續往下看。

渲染過程-InheritedElement 與 StatefulElement.png

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 是如何感知的?比如插入了一個新的元素,或者某個元素的渲染引數變了(顏色,字型,內容等),渲染層是怎麼知道的?繼續繼續! image.png

InheritedElement如何感知元件樹的變化

先看一下 InheritedElement 的類結構。

classDiagram
    Element <-- ComponentElement
    ComponentElement <-- ProxyElement
    ProxyElement <-- InheritedElement

    class Element {
        -dependOnInheritedWidgetOfExactType()
        -dependOnInheritedElement()
    }

    class InheritedElement {
    -Map<Element, Object?> _dependents
        -void _updateInheritance()
        -getDependencies(Element dependent)
        setDependencies(Element dependent, Object? value)
        updateDependencies(Element dependent, Object? aspect)
        notifyDependent(covariant InheritedWidget oldWidget, Element dependent)
        updated(InheritedWidget oldWidget)
        notifyClients(InheritedWidget oldWidget)

    }

    class ProxyElement {
        -build()
        -update(ProxyWidget newWidget)
        -updated(covariant ProxyWidget oldWidget)
        -notifyClients(covariant ProxyWidget oldWidget)
}
    

從類結構上看也不復雜,這是因為大部分渲染的管理已經在父類的 ComponentElementElement 中完成了。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 就和元件對應的渲染元素建立了聯絡。

image.png

接下來就是看 setState 後,怎麼獲取新的元件樹和更新元件了。我們已經知道了setState 的時候會呼叫 performRebuild 方法,在 performRebuild 中會呼叫 ElementupdateChild 方法,現在來看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 方法,我們在這個方法列印出來看一下。 image.png 在元素的 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 的元件渲染的核心就在於如何選擇狀態管理來實現元件的渲染,這個對效能影響很大。接下來我們將以狀態管理外掛的應用方式,講述在實際例子中的應用。


我是島上碼農,微信公眾號同名,這是Flutter 入門與實戰的專欄文章。

??:覺得有收穫請點個贊鼓勵一下!

?:收藏文章,方便回看哦!

?:評論交流,互相進步!

相關文章