Flutter框架分析(三)-- Element

1. 前言

上一篇文章講到,Widget是描述一個UI元素的配置數據,Element才真正表明屏幕顯示元素,是某個位置的Widget生成的實例。本篇文章則主要介紹Element的主要功能。html

經過上篇文章介紹的Widget TreeFlutter Framework會生成一系列Element,這些Element構成了Element Tree,其主要功能以下:node

  • 維護這棵Element Tree,根據Widget Tree的變化來更新Element Tree,包括:節點的插入、更新、刪除、移動等;
  • WidgetRenderObject關聯到Element Tree上。

2. Element分類

image.png 如上圖所示,Element從功能上看,能夠分爲兩大類:markdown

  • ComponentElement

組合類Element。這類Element主要用來組合其餘更基礎的Element,獲得功能更加複雜的Element。開發時經常使用到的StatelessWidgetStatefulWidget相對應的ElementStatelessElementStatefulElement,即屬於ComponentElement架構

  • RenderObjectElement

渲染類Element,對應Renderer Widget,是框架最核心的ElementRenderObjectElement主要包括LeafRenderObjectElementSingleChildRenderObjectElement,和MultiChildRenderObjectElement。其中,LeafRenderObjectElement對應的WidgetLeafRenderObjectWidget,沒有子節點;SingleChildRenderObjectElement對應的WidgetSingleChildRenderObjectWidget,有一個子節點;MultiChildRenderObjectElement對應的WidgetMultiChildRenderObjecWidget ,有多個子節點。框架

3. Element生命週期

Element有4種狀態:initial,active,inactive,defunct。其對應的意義以下:less

  • initial:初始狀態,Element剛建立時就是該狀態。
  • active:激活狀態。此時ElementParent已經經過mount將該Element插入Element Tree的指定的插槽處(Slot),Element此時隨時可能顯示在屏幕上。
  • inactive:未激活狀態。當Widget Tree發生變化,Element對應的Widget發生變化,同時因爲新舊WidgetKey或者的RunTimeType不匹配等緣由致使該Element也被移除,所以該Element的狀態變爲未激活狀態,被從屏幕上移除。並將該ElementElement Tree中移除,若是該Element有對應的RenderObject,還會將對應的RenderObjectRender Tree移除。可是,此Element仍是有被複用的機會,例如經過GlobalKey進行復用。
  • defunct:失效狀態。若是一個處於未激活狀態的Element在當前幀動畫結束時仍是未被複用,此時會調用該Element的unmount函數,將Element的狀態改成defunct,並對其中的資源進行清理。

Element4種狀態間的轉換關係以下圖所示: image.pngide

4. ComponentElement

4.1 與核心元素關係

如上文所述,ComponentElement分爲StatelessElementStatefulElement,這兩種Element同核心元素Widget以及State之間的關係以下圖所示。 image.png 如圖:函數

  • ComponentElement持有Parent ElementChild Element,由此構成Element Tree.
  • ComponentElement持有其對應的Widget,對於StatefulElement,其還持有對應的State,以此實現ElementWidget之間的綁定。
  • State是被StatefulElement持有,而不是被StatefulWidget持有,便於State的複用。事實上,StateStatefulElement是一一對應的,只有在初始化StatefulElement時,纔會初始化對應的State並將其綁定到StatefulElement上。

4.2 核心流程

一個Element的核心操做流程有,建立、更新、銷燬三種,下面將分別介紹這三個流程。oop

  • 建立

ComponentElement的建立起源與父Widget調用inflateWidget,而後經過mount將該Element掛載至Element Tree,並遞歸建立子節點。post

image.png

  • 更新

由父Element執行更新子節點的操做(updateChild),因爲新舊Widget的類型和Key均未發生變化,所以觸發了Element的更新操做,並經過performRebuild將更新操做傳遞下去。其核心函數updateChild以後會詳細介紹。

image.png

  • 銷燬

由父Element或更上級的節點執行更新子節點的操做(updateChild),因爲新舊Widget的類型或者Key發生變化,或者新Widget被移除,所以致使該Element被轉爲未激活狀態,並被加入未激活列表,並在下一幀被失效。

image.png

4.3 核心函數

下面對ComponentElement中的核心方法進行介紹。

  • inflateWidget
Element inflateWidget(Widget newWidget, dynamic newSlot) {
  final Key key = newWidget.key;
//複用GlobalKey對應的Element
  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;
    }
  }
//建立Element,並掛載至Element Tree
  final Element newChild = newWidget.createElement();
  newChild.mount(this, newSlot);
  return newChild;
}
複製代碼

inflateWidget的主要職責以下:

  1. 判斷新Widget是否有GlobalKey,若是有GlobalKey,則從Inactive Elements列表中找到對應的Element並進行復用。
  2. 無可複用Element,則根據新Widget建立對應的Element,並將其掛載至Element Tree
  • mount
void mount(Element parent, dynamic newSlot) {
//更新_parent等屬性,將元素加入Element Tree
  _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;
//註冊GlobalKey
  final Key key = widget.key;
  if (key is GlobalKey) {
    key._register(this);
  }
  _updateInheritance();
}
複製代碼

Element第一次被插入Element Tree的時候,該方法被調用。其主要職責以下:

  1. 將給Element加入Element Tree,更新_parent,_slot等樹相關的屬性。
  2. 若是新WidgetGlobalKey,將該Element註冊進GlobalKey中,其做用下文會詳細分析。
  3. ComponentElement的mount函數會調用_firstBuild函數,觸發子Widget的建立和更新。
  • performRebuild
@override
void performRebuild() {
//調用build函數,生成子Widget
  Widget built;
  built = build();
//根據新的子Widget更新子Element
  _child = updateChild(_child, built, slot);
}
複製代碼

performRebuild的主要職責以下:

  1. 調用build函數,生成子Widget
  2. 根據新的子Widget更新子Element
  • update
@mustCallSuper
void update(covariant Widget newWidget) {
  _widget = newWidget;
}
複製代碼

此函數主要職責爲:

  1. 將對應的Widget更新爲新的Widget
  2. ComponentElement的各類子類中,還會調用rebuild函數觸發對子Widget的重建。
  • updateChild
updateChild
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  if (newWidget == null) {
//新的Child Widget爲null,則返回null;若是舊Child Widget,使其未激活
    if (child != null)
      deactivateChild(child);
    return null;
  }
  Element newChild;
  if (child != null) {
//新的Child Widget不爲null,舊的Child Widget也不爲null
    bool hasSameSuperclass = true;
    if (hasSameSuperclass && child.widget == newWidget) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      newChild = child;
    } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)){
//Key和RuntimeType相同,使用update更新
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      child.update(newWidget);
      newChild = child;
    } else {
//Key或RuntimeType不相同,使舊的Child Widget未激活,並對新的Child Widget使用inflateWidget
      deactivateChild(child);
      newChild = inflateWidget(newWidget, newSlot);
    }
  } else {
//新的Child Widget不爲null,舊的Child Widget爲null,對新的Child Widget使用inflateWidget
    newChild = inflateWidget(newWidget, newSlot);
  }

  return newChild;
}
複製代碼

該方法的主要職責爲: 根據新的子Widget,更新舊的子Element,或者獲得新的子Element。其核心邏輯能夠用表格表示:

newWidget == null newWidget != null
Child == null 返回null 返回新Element
Child != null 移除舊的子Element,返回null 若是Widget能更新,更新舊的子Element,並返回之;不然建立新的子Element並返回。

該邏輯歸納以下:

  • 若是newWidget爲null,則返回null,同時若是有舊的子Element則移除之。
  • 若是newWidget不爲null,舊Child爲null,則建立新的子Element,並返回之。
  • 若是newWidget不爲null,舊Child不爲null,新舊子WidgetKeyRuntimeType等都相同,則調用update方法更新子Element並返回之。
  • 若是newWidget不爲null,舊Child不爲null,新舊子WidgetKeyRuntimeType等不徹底相同,則說明Widget Tree有變更,此時移除舊的子Element,並建立新的子Element,並返回之。

5. RenderObjectElement

5.1 與核心元素關係

RenderObjectElement同核心元素WidgetRenderObject之間的關係以下圖所示:

image.png

如圖:

  • RenderObjectElement持有Parent Element,可是不必定持有Child Element,有可能無Child Element,有可能持有一個Child ElementChild),有可能持有多個Child Element(Children)。
  • RenderObjectElement持有對應的WidgetRenderObject,將WidgetRenderObject串聯起來,實現了WidgetElementRenderObject之間的綁定。

5.2 核心流程

ComponentElement同樣,RenderObjectElement的核心操做流程有,建立、更新、銷燬三種,接下來會詳細介紹這三種流程。

  • 建立

RenderObjectElement的建立流程和ComponentElement的建立流程基本一致,其最大的區別是ComponentElement在mount後,會調用build來建立子Widget,而RenderObjectElement則是create和attach其RenderObject

image.png

  • 更新

RenderObjectElement的更新流程和ComponentElement的更新流程也基本一致,其最大的區別是ComponentElement的update函數會調用build函數,從新觸發子Widget的構建,而RenderObjectElement則是調用updateRenderObject對綁定的RenderObject進行更新。

image.png

  • 銷燬

RenderObjectElement的銷燬流程和ComponentElement的銷燬流程也基本一致。也是由父Element或更上級的節點執行更新子節點的操做(updateChild),致使該Element被停用,並被加入未激活列表,並在下一幀被失效。其不同的地方是在unmount Element的時候,會調用didUnmountRenderObject失效對應的RenderObject

image.png

5.3 核心函數

下面對RenderObjectElement中的核心方法進行介紹。

  • inflateWidget

該函數和ComponentElement的inflateWidget函數徹底一致,此處再也不復述。

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

該函數的調用時機和ComponentElement的一致,當Element第一次被插入Element Tree的時候,該方法被調用。其主要職責也和ComponentElement的一致,此處只列舉不同的職責,職責以下:

  1. 調用createRenderObject建立RenderObject,並使用attachRenderObject將RenderObject關聯到Element上。
  2. SingleChildRenderObjectElement會調用updateChild更新子節點,MultiChildRenderObjectElement會調用每一個子節點的inflateWidget重建全部子Widget
  • performRebuild
@override
void performRebuild() {
//更新renderObject
  widget.updateRenderObject(this, renderObject);
  _dirty = false;
}
複製代碼

performRebuild的主要職責以下:

調用updateRenderObject更新對應的RenderObject

  • update
@override
void update(covariant RenderObjectWidget newWidget) {
  super.update(newWidget);
  widget.updateRenderObject(this, renderObject);
  _dirty = false;
}
複製代碼

update的主要職責以下:

  1. 將對應的Widget更新爲新的Widget
  2. 調用updateRenderObject更新對應的RenderObject
  • updateChild

該函數和ComponentElement的inflateWidget函數徹底一致,此處再也不復述。

  • updateChildren
@protected
List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element> forgottenChildren }) {
  int newChildrenTop = 0;
  int oldChildrenTop = 0;
  int newChildrenBottom = newWidgets.length - 1;
  int oldChildrenBottom = oldChildren.length - 1;

  final List<Element> newChildren = oldChildren.length == newWidgets.length ?
      oldChildren : List<Element>(newWidgets.length);

  Element previousChild;

// 從頂部向下更新子Element
  // Update the top of the list.
  while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
    final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
    final Widget newWidget = newWidgets[newChildrenTop];
    if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
      break;
    final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
    newChildren[newChildrenTop] = newChild;
    previousChild = newChild;
    newChildrenTop += 1;
    oldChildrenTop += 1;
  }

// 從底部向上掃描子Element
  // Scan the bottom of the list.
  while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
    final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
    final Widget newWidget = newWidgets[newChildrenBottom];
    if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
      break;
    oldChildrenBottom -= 1;
    newChildrenBottom -= 1;
  }

// 掃描舊的子Element列表裏面中間的子Element,保存Widget有Key的Element到oldKeyChildren,其餘的失效
  // Scan the old children in the middle of the list.
  final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
  Map<Key, Element> oldKeyedChildren;
  if (haveOldChildren) {
    oldKeyedChildren = <Key, Element>{};
    while (oldChildrenTop <= oldChildrenBottom) {
      final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
      if (oldChild != null) {
        if (oldChild.widget.key != null)
          oldKeyedChildren[oldChild.widget.key] = oldChild;
        else
          deactivateChild(oldChild);
      }
      oldChildrenTop += 1;
    }
  }

// 根據Widget的Key更新oldKeyChildren中的Element。
  // Update the middle of the list.
  while (newChildrenTop <= newChildrenBottom) {
    Element oldChild;
    final Widget newWidget = newWidgets[newChildrenTop];
    if (haveOldChildren) {
      final Key key = newWidget.key;
      if (key != null) {
        oldChild = oldKeyedChildren[key];
        if (oldChild != null) {
          if (Widget.canUpdate(oldChild.widget, newWidget)) {
            // we found a match!
            // remove it from oldKeyedChildren so we don't unsync it later
            oldKeyedChildren.remove(key);
          } else {
            // Not a match, let's pretend we didn't see it for now.
            oldChild = null;
          }
        }
      }
    }

    final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
    newChildren[newChildrenTop] = newChild;
    previousChild = newChild;
    newChildrenTop += 1;
  }

  newChildrenBottom = newWidgets.length - 1;
  oldChildrenBottom = oldChildren.length - 1;

  // 從下到上更新底部的Element。.
  while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
    final Element oldChild = oldChildren[oldChildrenTop];
    final Widget newWidget = newWidgets[newChildrenTop];
    final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
    newChildren[newChildrenTop] = newChild;
    previousChild = newChild;
    newChildrenTop += 1;
    oldChildrenTop += 1;
  }

// 清除舊子Element列表中其餘全部剩餘Element
  // Clean up any of the remaining middle nodes from the old list.
  if (haveOldChildren && oldKeyedChildren.isNotEmpty) {
    for (final Element oldChild in oldKeyedChildren.values) {
      if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
        deactivateChild(oldChild);
    }
  }

  return newChildren;
}
複製代碼

該函數的主要職責以下:

  1. 複用能複用的子節點,並調用updateChild對子節點進行更新。
  2. 對不能更新的子節點,調用deactivateChild對該子節點進行失效。

其步驟以下:

  1. 從頂部向下更新子Element
  2. 從底部向上掃描子Element
  3. 掃描舊的子Element列表裏面中間的子Element,保存WidgetKeyElement到oldKeyChildren,其餘的失效。
  4. 對於新的子Element列表,若是其對應的WidgetKey和oldKeyChildren中的Key相同,更新oldKeyChildren中的Element
  5. 從下到上更新底部的Element
  6. 清除舊子Element列表中其餘全部剩餘Element

6. 小結

本文主要介紹了Element相關知識,重點介紹了其分類,生命週期,和核心函數。重點以下:

  • 維護Element Tree,根據Widget Tree的變化來更新Element Tree,包括:節點的插入、更新、刪除、移動等;並起到紐帶的做用,將Widget以及RenderObject關聯到Element Tree上。
  • Element分爲ComponentElementRenderObjectElement,前者負責組合子Element,後者負責渲染。
  • Element的主要複用和更新邏輯由其核心函數updateChild實現,具體邏輯見上文。

7. 參考文檔

深刻淺出 Flutter Framework 之 Element
Flutter實戰

8. 相關文章

Flutter框架分析(一)--架構總覽
Flutter框架分析(二)-- Widget
Flutter框架分析(四)-RenderObject
Flutter框架分析(五)-Widget,Element,RenderObject樹
Flutter框架分析(六)-Constraint
Flutter框架分析(七)-relayoutBoundary
Flutter框架分析(八)-Platform Channel
Flutter框架分析- Parent Data
Flutter框架分析 -InheritedWidget

相關文章
相關標籤/搜索