深入淺出 Flutter Framework 之 Element

峰之巔發表於2020-05-17

本文是『 深入淺出 Flutter Framework 』系列文章的第三篇,主要圍繞 Element 相關內容進行分析介紹,包括 Element 分類、Element 與其他幾個核心元素的關係、Element 生命週期以及核心方法解讀等。

本文同時發表於我的個人部落格

本系列文章將深入 Flutter Framework 內部逐步去分析其核心概念和流程,主要包括:

Overview


通過『 深入淺出 Flutter Framework 之 Widget 』的介紹,我們知道 Widget 本質上是 UI 的配置資料 (靜態、不可變),Element 則是通過 Widget 生成的『例項』,兩者間的關係就像是 json 與 object。

同一份配置 (Widget) 可以生成多個例項 (Element),這些例項可能會被安插在樹上不同的位置。

UI 的層級結構在 Element 間形成一棵真實存在的樹「Element Tree」,Element 有 2 個主要職責:

  • 根據 UI (「Widget Tree」) 的變化來維護「Element Tree」,包括:節點的插入、更新、刪除、移動等;
  • Widget 與 RenderObject 間的協調者。

分類


深入淺出 Flutter Framework 之 Element
如圖所示,Element 根據特點可以分為 2 類:

  • 「Component Element」 —— 組合型 Element,「Component Widget」、「Proxy Widget」對應的 Element 都屬於這一型別,其特點是子節點對應的 Widget 需要通過build方法去建立。同時,該型別 Element 都只有一個子節點 (single child);
  • 「Renderer Element」 —— 渲染型 Element,對應「Renderer Widget」,其不同的子型別包含的子節點個數也不一樣,如:LeafRenderObjectElement 沒有子節點,RootRenderObjectElement、SingleChildRenderObjectElement 有一個子節點,MultiChildRenderObjectElement 有多個子節點。

原生型 Element,只有 MultiChildRenderObjectElement 是多子節點的,其他都是單子節點。

同時,可以看到,Element實現了BuildContext介面 —— 我們在 Widget 中遇到的context,其實就是該 Widget 對應的 Element。

關係


在繼續之前有必要先了解一下 Element 與其他幾個核心元素間的關係,以便在全域性上有個認識。

深入淺出 Flutter Framework 之 Element
如圖:

  • Element 通過 parent、child 指標形成「Element Tree」;
  • Element 持有 Widget、「Render Object」;
  • State 是繫結在 Element 上的,而不是綁在「Stateful Widget」上(這點很重要)。

    上述這些關係並不是所有型別的 Element 都有,如:「Render Object」只有「RenderObject Element」才有,State 只有「Stateful Element」才有。

生命週期


Element 作為『例項』,隨著 UI 的變化,有較複雜的生命週期:

  • parent 通過Element.inflateWidget->Widget.createElement建立 child element,觸發場景有:UI 的初次建立、UI 重新整理時新老 Widget 不匹配(old element 被移除,new element 被插入);

  • parent 通過Element.mount將新建立的 child 插入「Element Tree」中指定的插槽處 (slot);

dynamic Element.slot——其含意對子節點透明,父節點用於確定其下子節點的排列順序 (兄弟節點間的排序)。因此,對於單子節點的節點 (single child),child.slot 通常為 null。 另外,slot 的型別是動態的,不同型別的 Element 可能會使用不同型別的 slot,如:Sliver 系列使用的是 int 型的 index,MultiChildRenderObjectElement 用兄弟節點作為後一個節點的 slot。 對於「component element」,mount方法還要負責所有子節點的 build (這是一個遞迴的過程),對於「render element」,mount方法需要負責將「render object」新增到「render tree」上。其過程在介紹到相應型別的 Element 時會詳情分析。

  • 此時,(child) element 處於 active 狀態,其內容隨時可能顯示在螢幕上;

  • 此後,由於狀態更新、UI 結構變化等,element 所在位置對應的 Widget 可能發生了變化,此時 parent 會呼叫Element.update去更新子節點,update 操作會在以當前節點為根節點的子樹上遞迴進行,直到葉子節點;(執行該步驟的前提是新老 Widget.[key && runtimeType] 相等,否則建立新 element,而不是更新現有 element);

  • 狀態更新時,element 也可能會被移除 (如:新老 Widget.[key || runtimeType] 不相等),此時,parent 將呼叫deactivateChild方法,該方法主要做了 3 件事:

    • 從「Element Tree」中移除該 element (將 parent 置為 null);
    • 將相應的「render object」從「render tree」上移除;
    • 將 element 新增到owner._inactiveElements中,在新增過程中會對『以該 element 為根節點的子樹上所有節點』呼叫deactivate方法 (移除的是整棵子樹)。
    void deactivateChild(Element child) {
      child._parent = null;
      child.detachRenderObject();
      owner._inactiveElements.add(child); // this eventually calls child.deactivate()
    }
    複製程式碼
  • 此時,element 處於 "inactive" 狀態,並從螢幕上消失,該狀態一直持續到當前幀動畫結束;

  • 從 element 進入 "inactive" 狀態到當前幀動畫結束期間,其還有被『搶救』的機會,前提是『帶有「global key」&& 被重新插入樹中』,此時:

    • 該 element 將會從owner._inactiveElements中移除;
    • 對該 element subtree 上所有節點呼叫activate方法 (它們又復活了!);
    • 將相應的「render object」重新插入「render tree」中;
    • 該 element subtree 又進入 "active" 狀態,並將再次出現在螢幕上。

    上述過程經歷這幾個方法:Parent Element.inflateWidget-->Parent Element._retakeInactiveElement-->BuildOwner._inactiveElements.remove-->Child Element._activateWithParent...

  • 對於所有在當前幀動畫結束時未能成功『搶救』回來的「Inactive Elements」都將被 unmount;

  • 至此,element 生命週期圓滿結束。

深入淺出 Flutter Framework 之 Element

核心方法


下面對 Element 中的幾個核心方法進行簡單介紹:

updateChild

updateChild是 flutter framework 中的核心方法之一:

Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  if (newWidget == null) {
    if (child != null)
      deactivateChild(child);
    return null;
  }

  if (child != null) {
    if (child.widget == newWidget) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      return child;
    }

    if (Widget.canUpdate(child.widget, newWidget)) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      child.update(newWidget);
      assert(child.widget == newWidget);

      return child;
    }

    deactivateChild(child);
    assert(child._parent == null);
  }

  return inflateWidget(newWidget, newSlot);
}
複製程式碼

在「Element Tree」上,父節點通過該方法來修改子節點對應的 Widget

深入淺出 Flutter Framework 之 Element
根據傳入引數的不同,有以下幾種不同的行為:

  • newWidget == null —— 說明子節點對應的 Widget 已被移除,直接 remove child element (如有);
  • child == null —— 說明 newWidget 是新插入的,建立子節點 (inflateWidget);
  • child != null —— 此時,分為 3 種情況:
    • 若 child.widget == newWidget,說明 child.widget 前後沒有變化,若 child.slot != newSlot 表明子節點在兄弟結點間移動了位置,通過updateSlotForChild修改 child.slot 即可;
    • 通過Widget.canUpdate判斷是否可以用 newWidget 修改 child element,若可以,則呼叫update方法;
    • 否則先將 child element 移除,並通 newWidget 建立新的 element 子節點。

子類一般不需要重寫該方法,該方法有點類似設計模式中的『模板方法』。

update

在更新流程中,若新老 Widget.[runtimeType && key] 相等,則會走到該方法。 子類需要重寫該方法以處理具體的更新邏輯:

Element 基類

@mustCallSuper
void update(covariant Widget newWidget) {
  _widget = newWidget;
}
複製程式碼

基類中的update很簡單,只是對_widget賦值。

子類重寫該方法時必須呼叫 super.

StatelessElement

父類ComponentElement沒有重寫該方法

void update(StatelessWidget newWidget) {
  super.update(newWidget);
  _dirty = true;
  rebuild();
}
複製程式碼

通過rebuild方法觸發重建 child widget (第 4 行),並以此來 update child element,期間會呼叫到StatelessWidget.build方法 (也就是我們寫的 Flutter 程式碼)。

組合型 Element 都會在update方法中觸發rebuild操作,以便重新 build child widget。

StatefulElement

void update(StatefulWidget newWidget) {
  super.update(newWidget);
  final StatefulWidget oldWidget = _state._widget;
  _dirty = true;
  _state._widget = widget;
  try {
    _state.didUpdateWidget(oldWidget) as dynamic;
  }
  finally {
  }
  rebuild();
}
複製程式碼

相比StatelessElementStatefulElement.update稍微複雜一些,需要處理State,如:

  • 修改 State 的 _widget屬性;
  • 呼叫State.didUpdateWidget (熟悉麼)。

最後,同樣會觸發rebuild操作,期間會呼叫到State.build方法。

ProxyElement

void update(ProxyWidget newWidget) {
  final ProxyWidget oldWidget = widget;
  super.update(newWidget);
  updated(oldWidget);
  _dirty = true;
  rebuild();
}

void updated(covariant ProxyWidget oldWidget) {
  notifyClients(oldWidget);
}

Widget build() => widget.child;
複製程式碼

ProxyElement.update方法需要關注的是對updated的呼叫,其主要用於通知關聯物件 Widget 有更新。 具體通知邏輯在子類中處理,如:InheritedElement會觸發所有依賴者 rebuild (對於 StatefulElement 型別的依賴者,會呼叫State.didChangeDependencies)。

ProxyElement 的build操作很簡單:直接返回widget.child

RenderObjectElement

void update(covariant RenderObjectWidget newWidget) {
  super.update(newWidget);
  widget.updateRenderObject(this, renderObject);
  _dirty = false;
}
複製程式碼

RenderObjectElement.update方法呼叫了widget.updateRenderObject來更新「Render Object」(熟悉麼)。

SingleChildRenderObjectElement

SingleChildRenderObjectElementMultiChildRenderObjectElementRenderObjectElement的子類。

void update(SingleChildRenderObjectWidget newWidget) {
  super.update(newWidget);
  _child = updateChild(_child, widget.child, null);
}
複製程式碼

第 3 行,通過newWidget.child呼叫updateChild方法遞迴修改子節點。

MultiChildRenderObjectElement

void update(MultiChildRenderObjectWidget newWidget) {
  super.update(newWidget);
  _children = updateChildren(_children, widget.children, forgottenChildren: _forgottenChildren);
}
複製程式碼

上述實現看似簡單,實則非常複雜,在updateChildren方法中處理了子節點的插入、移動、更新、刪除等所有情況。

inflateWidget

Element inflateWidget(Widget newWidget, dynamic newSlot) {
  final Key key = newWidget.key;
  if (key is GlobalKey) {
    final Element newChild = _retakeInactiveElement(key, newWidget);
    if (newChild != null) {
      newChild._activateWithParent(this, newSlot);
      final Element updatedChild = updateChild(newChild, newWidget, newSlot);
      return updatedChild;
    }
  }

  final Element newChild = newWidget.createElement();
  newChild.mount(this, newSlot);
  return newChild;
}
複製程式碼

inflateWidget 屬於模板方法,故一般情況下子類不用重寫。

該方法的主要職責:通過 Widget 建立對應的 Element,並將其掛載 (mount) 到「Element Tree」上。

如果 Widget 帶有 GlobalKey,首先在 Inactive Elements 列表中查詢是否有處於 inactive 狀態的節點 (即剛從樹上移除),如找到就直接復活該節點。

主要呼叫路徑來自上面介紹的updateChild方法。

mount

當 Element 第一次被插入「Element Tree」上時,呼叫該方法。由於此時 parent 已確定,故在該方法中可以做依賴 parent 的初始化操作。經過該方法後,element 的狀態從 "initial" 轉到了 "active"。

Element

@mustCallSuper
void mount(Element parent, dynamic newSlot) {
  _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;

  if (widget.key is GlobalKey) {
    final GlobalKey key = widget.key;
    key._register(this);
  }

  _updateInheritance();
}
複製程式碼

還記得BuildOwner嗎,正是在該方法中父節點的 owner 傳給了子節點。 如果,對應的 Widget 帶有 GlobalKey,進行相關的註冊。 最後,繼承來自父節點的「Inherited Widgets」。

子類重寫該方法時,必須呼叫 super。 關於「Inherited Widgets」,後文會詳細分析

ComponentElement

void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _firstBuild();
}

void _firstBuild() {
  rebuild();
}
複製程式碼

組合型 Element 在掛載時會執行_firstBuild->rebuild操作。

RenderObjectElement

void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _renderObject = widget.createRenderObject(this);
  attachRenderObject(newSlot);
  _dirty = false;
}
複製程式碼

RenderObjectElement.mount中做的最重要的事就是通過 Widget 建立了「Render Object」(第 3 行),並將其插入到「RenderObject Tree」上 (第 4 行)。

SingleChildRenderObjectElement

@override
void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _child = updateChild(_child, widget.child, null);
}
複製程式碼

SingleChildRenderObjectElement在 super (RenderObjectElement) 的基礎上,呼叫updateChild方法處理子節點,其實此時_childnil,前面介紹過當 child 為nil時,updateChild會呼叫inflateWidget方法建立 Element 例項。

MultiChildRenderObjectElement

void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _children = List<Element>(widget.children.length);
  Element previousChild;
  for (int i = 0; i < _children.length; i += 1) {
    final Element newChild = inflateWidget(widget.children[i], previousChild);
    _children[i] = newChild;
    previousChild = newChild;
  }
}
複製程式碼

MultiChildRenderObjectElement在 super (RenderObjectElement) 的基礎上,對每個子節點直接呼叫inflateWidget方法。

markNeedsBuild

void markNeedsBuild() {
  if (!_active)
    return;

  if (dirty)
    return;

  _dirty = true;
  owner.scheduleBuildFor(this);
}
複製程式碼

markNeedsBuild方法其實在介紹BuildOwer時已經分析過,其作用就是將當前 Element 加入_dirtyElements中,以便在下一幀可以rebuild。 那麼,哪些場景會呼叫markNeedsBuild呢?

  • State.setState —— 這個在介紹 Widget 時已分析過了;
  • Element.reassemble —— debug hot reload;
  • Element.didChangeDependencies —— 前面介紹過當依賴的「Inherited Widget」有變化時會導致依賴者 rebuild,就是從這裡觸發的;
  • StatefulElement.activate —— 還記得activate嗎?前文介紹過當 Element 從 "inactive" 到 "active" 時,會呼叫該方法。為什麼StatefulElement要重寫activate?因為StatefulElement有附帶的 State,需要給它一個activate的機會。

子類一般不必重寫該方法。

rebuild

void rebuild() {
  if (!_active || !_dirty)
    return;

  performRebuild();
}
複製程式碼

該方法邏輯非常簡單,對於活躍的、髒節點呼叫performRebuild,在 3 種場景下被呼叫:

  • 對於 dirty element,在新一幀繪製過程中由BuildOwner.buildScope
  • 在 element 掛載時,由Element.mount呼叫;
  • update方法內被呼叫。

上述第 2、3 點僅「Component Element」需要

performRebuild

Element 基類中該方法是no-op

ComponentElement

void performRebuild() {
  Widget built;
  built = build();

  _child = updateChild(_child, built, slot);
}
複製程式碼

對於組合型 Element,rebuild 過程其實就是呼叫build方法生成「child widget」,再由其更新「child element」。

StatelessElement.build: Widget build() => widget.build(this); StatefulElement.build: Widget build() => state.build(this); ProxyElement.build: Widget build() => widget.child;

RenderObjectElement

void performRebuild() {
  widget.updateRenderObject(this, renderObject);
  _dirty = false;
}
複製程式碼

在渲染型 Element 基類中只是用 Widget 更新了對應的「Render Object」。 在相關子類中可以執行更具體的邏輯。

生命週期視角

至此,Element 的核心方法基本已介紹完,是不是有點暈乎乎的感覺?inflateWidgetupdateChildupdatemountrebuild以及performRebuild等你中有我、我中有你,再加上不同型別的子類對這些方法的重寫。

下面,我們以 Element 生命週期為切入點將這些方法串起來。 對於一個 Element 節點來說在其生命週期內可能會歷經幾次『重大事件』:

  • 被建立 —— 起源於父節點呼叫inflateWidget,隨之被掛載到「Element Tree」上, 此後遞迴建立子節點;
    深入淺出 Flutter Framework 之 Element
  • 被更新 —— 由「Element Tree」上祖先節點遞迴傳遞下來的更新操作,parent.updateChild->child.update
    深入淺出 Flutter Framework 之 Element
  • 被重建 —— 被呼叫rebuild方法(呼叫場景上面已分析);
    深入淺出 Flutter Framework 之 Element
  • 被銷燬 —— element 節點所在的子樹隨著 UI 的變化被移除。
    深入淺出 Flutter Framework 之 Element

依賴 (Dependencies)


在 Element 基類中有這樣兩個成員:

Map<Type, InheritedElement> _inheritedWidgets;
Set<InheritedElement> _dependencies;
複製程式碼

它們是幹嘛用的呢?

  • _inheritedWidgets —— 用於收集從「Element Tree」根節點到當前節點路徑上所有的「Inherited Elements」; 前文提到過在mount方法結束處會呼叫_updateInheritance: 以下是 Element 基類的實現,可以看到子節點直接獲得父節點的_inheritedWidgets
void _updateInheritance() {
  _inheritedWidgets = _parent?._inheritedWidgets;
}
複製程式碼

以下是InheritedElement類的實現,其在父節點的基礎上將自己加入到_inheritedWidgets中,以便其子孫節點的_inheritedWidgets包含它 (第 8 行):

void _updateInheritance() {
  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;
}
複製程式碼
  • _dependencies —— 用於記錄當前節點依賴了哪些「Inherited Elements」,通常我們呼叫context.dependOnInheritedWidgetOfExactType<T>時就會在當前節點與目標 Inherited 節點間形成依賴關係。

在 Element 上提供的便利方法of,一般殾會呼叫dependOnInheritedWidgetOfExactType

同時,在InheritedElement中還有用於記錄所有依賴於它的節點:final Map<Element, Object> _dependents。 最終,在「Inherited Element」發生變化,需要通知依賴者時,會利用依賴者的_dependencies資訊做一下 (debug) check (第 4 行):

void notifyClients(InheritedWidget oldWidget) {
  for (Element dependent in _dependents.keys) {
    // check that it really depends on us
    assert(dependent._dependencies.contains(this));
    notifyDependent(oldWidget, dependent);
  }
}
複製程式碼

小結

至此,Element 相關的內容基本已介紹完。總結提煉一下:

  • Element 與 Widget 一一對應,它們間的關係就像 object 與 json;
  • 只有「Render Element」才有對應的「Render Object」;
  • Element 作為 Widget 與 RenderObejct 間協調者,會根據 UI(「Widget Tree」) 的變化對「Element Tree」作出相應的調整,同時對「RenderObject Tree」進行必要的修改;
  • Widget 是不可變的、無狀態的,而 Element 是有狀態的。

最後,強烈推薦Keys! What are they good for?這篇文章,對於理解本文相關的內容有很大的幫助。

相關文章