Flutter渲染之Widget、Element 和 RenderObject

郭巨集偉發表於2020-07-03

提出問題

用Flutter寫介面寫了一段時間了,感覺很爽,尤其是熱載入功能,節省了大把時間,宣告式的程式設計方式也是以後的趨勢。現在基本熟練以後一些簡單的效果能很快寫出來,即使沒見過的也可以在網上搜一下找到答案,但是感覺沒有深入底層瞭解,有些問題還是一知半解,這些問題比如以下幾個:

  1. createState 方法在什麼時候呼叫?state 裡面為啥可以直接獲取到 widget 物件?
  2. build 方法是在什麼時候呼叫的?
  3. BuildContext 是什麼?
  4. Widget 頻繁更改建立是否會影響效能?複用和更新機制是什麼樣的?
  5. 建立 Widget 裡面的 Key 到底是什麼作用?

後面抽時間看了一些關於 Flutter 渲染的文章,重點了解了 Widget、Element 和 RenderObject 方面的內容,終於有了一些瞭解,對上面幾個問題也有了清晰的答案,因此通過這篇文章記錄一下。

三棵樹

首先先了解三棵樹,這是我們的核心,需要首先建立一個概念。

Widget 樹

我們平時用 Widget 使用宣告式的形式寫出來的介面,可以理解為 Widget 樹,這是要介紹的第一棵樹。

RenderObject 樹

Flutter 引擎需要把我們寫的 Widget 樹的資訊都渲染到介面上,這樣人眼才能看到,跟渲染有關的當然有一顆渲染樹 RenderObject tree,這是第二顆樹,渲染樹節點叫做 RenderObject,這個節點裡面處理佈局、繪製相關的事情。這兩個樹的節點並不是一一對應的關係,有些 Widget是要顯示的,有些 Widget ,比如那些繼承自 StatelessWidget & StatefulWidget 的 Widget 只是將其他 Widget 做一個組合,這些 Widget 本身並不需要顯示,因此在 RenderObject 樹上並沒有相對應的節點。

Element 樹

Widget 樹是非常不穩定的,動不動就執行 build方法,一旦呼叫 build 方法意味著這個 Widget 依賴的所有其他 Widget 都會重新建立,如果 Flutter 直接解析 Widget樹,將其轉化為 RenderObject 樹來直接進行渲染,那麼將會是一個非常消耗效能的過程,那對應的肯定有一個東西來消化這些變化中的不便,來做cache。因此,這裡就有另外一棵樹 Element 樹。Element 樹這一層將 Widget 樹的變化(類似 React 虛擬 DOM diff)做了抽象,可以只將真正需要修改的部分同步到真實的 RenderObject 樹中,最大程度降低對真實渲染檢視的修改,提高渲染效率,而不是銷燬整個渲染檢視樹重建。

這三棵樹如下圖所示,是我們討論的核心內容。

Flutter渲染之Widget、Element 和 RenderObject

從上圖可以看出,widget 樹和 Element 樹節點是一一對應關係,每一個 Widget 都會有其對應的 Element,但是 RenderObject 樹則不然,只有需要渲染的 Widget 才會有對應的節點。Element 樹相當於一箇中間層,大管家,它對 Widget 和 RenderObject 都有引用。當 Widget 不斷變化的時候,將新 Widget 拿到 Element 來進行對比,看一下和之前保留的 Widget 型別和 Key 是否相同,如果都一樣,那完全沒有必要重新建立 Element 和 RenderObject,只需要更新裡面的一些屬性即可,這樣可以以最小的開銷更新 RenderObject,引擎在解析 RenderObject 的時候,發現只有屬性修改了,那麼也可以以最小的開銷來做渲染。

以上只是引出了非常重要的三棵樹和他們之間的關係,簡而言之,Widget 樹就是配置資訊的樹,我們平時寫程式碼寫的就是這棵樹,RenderObject 樹是渲染樹,負責計算佈局,繪製,Flutter 引擎就是根據這棵樹來進行渲染的,Element 樹作為中間者,管理著將 Widget 生成 RenderObject和一些更新操作。

前面只是從概念角度粗略來介紹,下面我們從原始碼層面來看一看。

從原始碼來了解 Widget、Element 和 RenderObject

Widget

下面對 Widget 的概述截圖來自官網

Flutter渲染之Widget、Element 和 RenderObject

翻譯一下就是,Widget 描述 Element 的配置資訊,是 Flutter 框架裡的核心類層次結構,一個 Widget 是使用者介面某一部分的不可變描述。Widgets 可以轉為 Elements,Elements 管理著底層的渲染樹。

有這麼多 Widget,我們來簡單分各類吧,前面已經提到 Widget 有可渲染和不可渲染的分別了。可渲染裡面分為多孩子和單孩子,也就是屬性為 child 或 children,在不可渲染的 Widgets 裡面又分為有狀態和無狀態,也就是 StatefullWidget 和 StatelessWidget。我們選擇四個典型的Widgets來看看吧,如 Padding、RichText、Container、TextField。通過查閱原始碼,我們看到這幾個類的繼承關係如下圖所示。

Flutter渲染之Widget、Element 和 RenderObject

來到Widget 類裡面可以看到有以下方法

  @protected
  Element createElement();
複製程式碼

Widget 是個抽象類,所有的 Widgets 都是它的子類,其抽象方法 createElement 需要子類實現,這裡體現了之前我們說的 Widget 和 Element 的一一對應關係。來到 StatelessWidget、StatefulWidget、MultiChildRenderObjectWidget、SingleChildRenderObjectWidget 裡面我們可以找到 createElement 的實現。

SingleChildRenderObjectWidget

@override
 SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
複製程式碼

MultiChildRenderObjectWidget

@override
MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
複製程式碼

StatefulWidget

@override
StatefulElement createElement() => StatefulElement(this);
複製程式碼

StatelessWidget

@override
StatelessElement createElement() => StatelessElement(this);
複製程式碼

可以發現規律,建立 Element 都會傳入 this,也就是當前 Widget,然後返回對應的 Element,這些 Element 都是繼承自 Element,Element 會有引用指向當前 Widget。

我們繼續來到 RichText 和 Padding 類定義裡面,他們都是繼承自 RenderObjectWidget,可以看到他們都有 createRenderObject 方法,如下

Padding

  @override
  RenderPadding createRenderObject(BuildContext context) {
    return RenderPadding(
      padding: padding,
      textDirection: Directionality.of(context),
    );
  }
複製程式碼

RichText

@override
  RenderParagraph createRenderObject(BuildContext context) {
    assert(textDirection != null || debugCheckHasDirectionality(context));
    return RenderParagraph(text,
      textAlign: textAlign,
      textDirection: textDirection ?? Directionality.of(context),
      softWrap: softWrap,
      overflow: overflow,
      textScaleFactor: textScaleFactor,
      maxLines: maxLines,
      strutStyle: strutStyle,
      textWidthBasis: textWidthBasis,
      locale: locale ?? Localizations.localeOf(context, nullOk: true),
    );
  }
複製程式碼

RenderPadding 和 RenderParagraph 最終都是繼承自 RenderObject。通過以上原始碼分析,我們可以看出來 Widget 裡面有生成 Element 和 RenderObject 的方法,所以我們平時只需要埋頭寫好 Widget 就行,Flutter 框架會幫我們生成對應的 Element 和 RenderObject。但是在什麼時候呼叫 createElement 和 createRenderObject呢, 後面繼續分析。

Element

以下對 Element 描述來自官網

Flutter渲染之Widget、Element 和 RenderObject

直接翻譯過來就是,Element 是 樹中特定位置 Widget 的一個例項化物件。這句話有兩層意思:1. 表示 Widget 是一個配置,Element 才是最終的物件;2. Element 是通過遍歷 Widget 樹時,呼叫 Widget 的方法建立的。Element 承載了檢視構建的上下文資料,是連線結構化的配置資訊到完成最終渲染的橋樑。

上面從原始碼裡面介紹 Widget 都會生成對應的 Element,這裡我們也對 Element 簡單做一個分類,和 Widget 相對應,如下圖所示。

Flutter渲染之Widget、Element 和 RenderObject

首先還是進入 Element 類裡面看看,這是個抽象類,可以看到一些關鍵的方法和屬性。

  /// Typically called by an override of [Widget.createElement].
  Element(Widget widget)
    : assert(widget != null),
      _widget = widget;
複製程式碼

上面介紹Widget 裡面 createElement 方法的時候可以看到會傳入 this,這裡從 Element 的構造方法中可以看到,this 最後傳給了 Element 裡面的 _widget。也就是說每個 Element 裡面都會有一個 Widget 的引用。_widget 在 Element 裡面定義如下

  /// The configuration for this element.
  @override
  Widget get widget => _widget;
  Widget _widget;
複製程式碼

從原始碼裡面知道 Element 裡面的 widget 是一個 get 方法,直接返回 _widget。從上面的註釋資訊也再一次提到 Widget 和 Element 的關係,Widget 是 Element 的配置。

對於 Element 的構造方法,StatelessfulElement 有一些特殊的地方,如下

class StatefulElement extends ComponentElement {
  /// Creates an element that uses the given widget as its configuration.
  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),
        super(widget) {
    ... 省略斷言 ...
    assert(_state._element == null);
    _state._element = this;
     ... 省略斷言 ...
    _state._widget = widget;
    assert(_state._debugLifecycleState == _StateLifecycle.created);
  }
  
  /// The [State] instance associated with this location in the tree.
  ///
  /// There is a one-to-one relationship between [State] objects and the
  /// [StatefulElement] objects that hold them. The [State] objects are created
  /// by [StatefulElement] in [mount].
  State<StatefulWidget> get state => _state;
  State<StatefulWidget> _state;
}
複製程式碼

StatefulElement 的構造方法中還呼叫了對應 Widget 的 createState 方法,並賦值給 _state,這也解答了我們在文章開頭提出的問題(createState 方法在什麼時候呼叫?)。StatefulElement 裡面不僅有對 Widget 的引用,也有對 StatefulWidget 的 State 的引用。並且在建構函式裡面還將 widget 賦值給了 _state 裡面的 _widget。所以我們在 State 裡面可以直接使用 widget 就可以拿到 State 對應的 Widget。原來是在 StatefulElement 建構函式的時候賦值的。解釋了開頭提到的問題(state 裡面為啥可以直接獲取到 widget 物件?)。

Element 還有一個關鍵的方法 mount,如下

  @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();
    ... 省略斷言 ...
  }
複製程式碼

Flutter 框架會根據 Widget 建立對應的 Element,Element 生成以後會呼叫 Element 的 mount 方法,將生成的 Element 掛載到 Element 樹上。這裡的 createElement 和 mount 都是 Flutter 框架自動呼叫的,不需要開發者手動呼叫。因此我們平時可能沒關注這些過程。Element 裡面的 mount 方法需要子類實現,我們來看看ComponentElement 裡的 mount 方法。

  @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    assert(_child == null);
    assert(_active);
    _firstBuild();
    assert(_child != null);
  }
複製程式碼

這裡一步一步看原始碼,發現執行鏈路如下: _firstBuild()【ComponentElement】 -> rebuild() 【Element】-> performRebuild()【ComponentElement】 -> build()【StatelessElement】 看一看最後 StatelessElement build() 的原始碼

  @override
  StatelessWidget get widget => super.widget;

  @override
  Widget build() => widget.build(this);
複製程式碼

StatefulElement 的 build() 的原始碼如下

  @override
  Widget build() => state.build(this);  
複製程式碼

可以看出ComponentElement 的 mount 最後執行的是 build 方法。不過 StatelessElement 和 StatefulElement 是有區別的,StatelessElement 執行的是 Widget 裡的 build 方法,而 StatefulElement 裡面執行的是 state 的 build 方法。因此,這裡也解決了文章開始提到的一個問題(build 方法是在什麼時候呼叫的?)。也知道了 StatefulWidget 和 它的 State 是如何聯絡起來的。

另外,我們看到上面執行執行build 方法傳遞的引數 this,也就是當前 Element,而我們在寫程式碼的時候 build 方法是這樣的

  @override
  Widget build(BuildContext context) {
  }
複製程式碼

因此我們知道了,這個 BuildContext 其實就是這個 Widget 所對應的 Element。看看 Element 的定義就更清楚了。這也解釋了開始提到的問題(BuildContext 是什麼?)。

abstract class Element extends DiagnosticableTree implements BuildContext {
}
複製程式碼

再來看看 RenderObjectElement 裡的 mount 方法

  @override
  void mount(Element parent, dynamic newSlot) {
    ... 省略斷言 ...
    _renderObject = widget.createRenderObject(this);
    ... 省略斷言 ...
    attachRenderObject(newSlot);
    _dirty = false;
  }
複製程式碼

對比一下 ComponentElement 和 RenderObjectElement 裡面的 mount 方法,前面介紹過,ComponentElement 是非渲染 Widget 對應的 Element,而 RenderObjectElement 是渲染 Widget 對應的 Element,前者的mount 方法主要是負責執行 build 方法,而後者的 mount 方法主要是呼叫 Widget 裡面的 createRenderObject 方法生成 RenderObject,然後賦值給自己的 _renderObject。

因此可以總結,ComponentElement 的 mount 方法主要作用是執行 build,而 RenderObjectElement 的 mount 方法主要作用是生成 RenderObject。

Widget 類裡面有一個很重要的靜態方法,本來可以放到上面講 Widget 的時候說,但是還是放到 Element 裡面吧。就是這個

  /// Whether the `newWidget` can be used to update an [Element] that currently
  /// has the `oldWidget` as its configuration.
  ///
  /// An element that uses a given widget as its configuration can be updated to
  /// use another widget as its configuration if, and only if, the two widgets
  /// have [runtimeType] and [key] properties that are [operator==].
  ///
  /// If the widgets have no key (their key is null), then they are considered a
  /// match if they have the same type, even if their children are completely
  /// different.
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
複製程式碼

Element 裡面有一個 _widget 作為其配置資訊,當widget變化或重新生成以後,Element 要不要銷燬重建呢,還是直接將新生成的 Widget 替換舊的 Widget。答案就是通過這個方法判斷的,上面的註釋可以翻譯如下

判斷新 Widget 是否可以用來取代 Element 當前的配置資訊 _widget。 Element 使用特定的 widget 作為其配置資訊,如果 runtimeType 和 key 和之前的 widget 相同,那麼可以使用一個新的 widget 更新 Element 裡面舊的 widget。 如果這兩個widget 都沒有賦值 key,那麼只要 runtimeType 相同也可以更新,即使這兩個 widget 的孩子 widget 都完全不一樣。

因此可以看出,即使外面的 widget 樹經常變換重建,我們的 Element 可以維持相對穩定,不會重複建立,當然也就不會重複 mount, 生成 RenderObject,只需要以最小代價更新相關屬性即可,最大可能減小了效能消耗。Widget 本身只是一些配置資訊,簡單的物件,它的變更重建不直接影響渲染,對效能影響很小。這就解決了上文提到的另外一個問題(Widget 頻繁更改建立是否會影響效能?複用和更新機制是什麼樣的?)。

RenderObject

從 RenderObject 的名字,我們就能很直觀地知道,RendreObject 是主要負責實現檢視渲染的物件。從上文中我們知道了一下幾點

  1. RenderObject 和 widget 並不是一一對應的,只有繼承自 RenderObjectWidget 的 widget 才有對應的 RenderObject;
  2. 生成 RenderObject 的方法 createRenderObject 是在 Widget 裡面定義的;
  3. 在 RenderObjectElement 執行 mount 方法的時候呼叫的 widget 裡面的 createRenderObject 方法的;
  4. RenderObjectElement 裡面既有對 Widget 的引用也有對 RenderObject 的引用,它作為中間者,管理著雙方。

RenderObject 在 Flutter 的展示分為四個階段,即佈局、繪製、合成和渲染。其中,佈局和繪製在 RenderObject 中完成,Flutter 採用深度優先機制遍歷渲染物件樹,確定樹中各個物件的位置和尺寸,並把它們繪製在不同的圖層上。繪製完畢後,合成和渲染的工作則交給 Skia 搞定。

總結

上面通過原始碼講解了一下 Widget、Element、RenderObject 的聯絡。下面簡單來個總結。

我們寫好 Widget 樹後,Flutter 會在遍歷 Widget 樹時呼叫 Widget 裡面的 createElement 方法去生成對應節點的 Element 物件,同時 Element 裡面也有了對 Widget 的引用。特別的是當 StatefulElement 建立的時候也執行 StatefulWidget 裡面的 createState 方法建立 state,並且賦值給 Element 裡的 _state 屬性,當前 widget 也同時賦值給了 state 裡的_widget。Element 建立好以後 Flutter 框架會執行 mount 方法,對於非渲染的 ComponentElement 來說 mount 主要執行 widget 裡的 build 方法,而對於渲染的 RenderObjectElement 來說 mount 裡面會呼叫 widget 裡面的 createRenderObject 方法 生成 RenderObject,並賦值給 RenderObjectElement 裡的相應屬性。StatefulElement 執行 build 方法的時候是執行的 state 裡面的 build 方法,並且將自身傳入,也就是 常見的 BuildContext。

如果 Widget 的配置資料發生了改變,那麼持有該 Widget 的 Element 節點也會被標記為 dirty。在下一個週期的繪製時,Flutter 就會觸發該 Element 樹的更新,通過 canUpdate 方法來判斷是否可以使用新的 Widget 來更新 Element 裡面的配置,還是重新生成 Element。並使用最新的 Widget 資料更新自身以及關聯的 RenderObject物件。佈局和繪製完成後,接下來的事情交給 Skia 了。在 VSync 訊號同步時直接從渲染樹合成 Bitmap,然後提交給 GPU。

回答開頭提出的問題

  1. createState 方法在什麼時候呼叫?state 裡面為啥可以直接獲取到 widget 物件?

答:Flutter 會在遍歷 Widget 樹時呼叫 Widget 裡面的 createElement 方法去生成對應節點的 Element 物件,同時執行 StatefulWidget 裡面的 createState 方法建立 state,並且賦值給 Element 裡的 _state 屬性,當前 widget 也同時賦值給了 state 裡的_widget,state 裡面有個 widget 的get 方法可以獲取到 _widget 物件。

  1. build 方法是在什麼時候呼叫的?

答:Element 建立好以後 Flutter 框架會執行 mount 方法,對於非渲染的 ComponentElement 來說 mount 主要執行 widget 裡的 build 方法,StatefulElement 執行 build 方法的時候是執行的 state 裡面的 build 方法,並且將自身傳入,也就是常見的 BuildContext

  1. BuildContext 是什麼?

答:StatefulElement 執行 build 方法的時候是執行的 state 裡面的 build 方法,並且將自身傳入,也就是 常見的 BuildContext。簡而言之 BuidContext 就是 Element。

  1. Widget 頻繁更改建立是否會影響效能?複用和更新機制是什麼樣的?

答:不會影響效能,widget 只是簡單的配置資訊,並不直接涉及佈局渲染相關。Element 層通過判斷新舊 widget 的runtimeType 和 key 是否相同決定是否可以直接更新之前的配置資訊,也就是替換之前的 widget,而不必每次都重新建立新的 Element。

  1. 建立 Widget 裡面的 Key 到底是什麼作用?

答:Key 作為 Widget 的標誌,在widget 變更的時候通過判斷 Element 裡面之前的 widget 的 runtimeType 和 key來決定是否能夠直接更新。需要了解更多 Key 的作用可以閱讀這邊文章 Flutter渲染之通過demo瞭解Key的作用

相關文章