Flutter之Widget層級介紹

大逗大人 發表於2019-07-25

flutter中,一切皆Widget。無論是顯示介面的UI元素,如TextImageIcon等;還是功能性元件,如手勢檢測的GestureDetector元件、應用主題資料傳遞的Theme元件、移除系統元件自帶Padding的MediaQuery元件等。可以說,flutter介面就是由一個個粒度非常細的Widget組合起來的。

由於Widget是不可變的,所以當檢視更新時,flutter會建立新的Widget來替換舊的Widget並將舊的Widget銷燬。但這樣就會涉及到大量Widget物件的銷燬和重建,從而對垃圾回收造成壓力。也因此,flutterWidget設計的十分輕量,並將檢視的配置資訊與渲染抽象出來,分別交給ElementRenderObject。從而使得Widget只起一個組織者作用,可以將ElementRenderObject組合起來,構成一個檢視。

1、Widget介紹

前面說過Widget是一種非常輕量且不可變的資料結構,只起一個組織者作用。那麼它是如何輕量的尼?下面我們就從原始碼來一窺究竟。

abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });
  final Key key;

  /// 建立Widget對應的Element物件,Element物件儲存了Widget的配置資訊
  @protected
  Element createElement();

  /// 判斷是否可以更新Widget
  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
        && oldWidget.key == newWidget.key;
  }
}
複製程式碼

Widget是一個抽象類,它只有兩個方法:

  • createElement:該方法是一個抽象方法,需要在子類實現。顧名思義,該方法主要是建立Widget對應的Element物件。
  • canUpdate:該方法主要是判斷Widget是否可更新。根據WidgetruntimeTypekey這兩個欄位來判斷。

由於Widget可以將ElementRenderObject組合成一個檢視,但從上面原始碼我們可以發現,Widget並沒有建立RenderObject物件的方法,那麼它是如何建立RenderObject物件的尼?其實是通過RenderObjectWidgetcreateRenderObject方法來建立的,此Widget是一個非常重要的類,如果不直接或間接繼承該類,Widget就無法顯示在介面上。下面我們對RenderObjectWidget原始碼一窺究竟。

abstract class RenderObjectWidget extends Widget {
  ...
  const RenderObjectWidget({ Key key }) : super(key: key);

  /// RenderObjectWidget對應著RenderObjectElement及其子類
  @override
  RenderObjectElement createElement();

  /// 建立一個RenderObject物件
  @protected
  RenderObject createRenderObject(BuildContext context);

  /// 更新renderObject
  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }

  /// 將renderObject從render樹中移除
  @protected
  void didUnmountRenderObject(covariant RenderObject renderObject) { }
}
複製程式碼

RenderObjectWidget是一個繼承自Widget的子類,但它比Widget多幾個方法。

  • createRenderObject:建立RenderObject物件,在該物件中會將檢視資料繪製到不同的圖層上。筆者認為它對應著Android中ViewManager的addView方法。
  • updateRenderObject:更新Widget所持有的RenderObject物件。筆者認為它對應著Android中ViewManager的updateViewLayout方法。
  • didUnmountRenderObject:將RenderObject物件從Render樹中移除,也就是銷燬RenderObject物件。筆者認為它對應著Android中ViewManager的removeView方法。

由於RenderObject主要是將檢視繪製成不同的圖層,然後再顯示在螢幕上。所以只有當我們的元件直接或間接繼承自RenderObjectWidget時,才會通過RenderObject來進行繪製、渲染,從而顯示在螢幕上,如RichTextRowCenter等。否則只是一個用來組裝元件的容器,如TextListView等。

2、Element介紹

Element是可變的,這裡的可變是指Element擁有自己的生命週期,可以根據生命週期來重用或銷燬Element物件,減少物件的頻繁建立及銷燬。它承載了檢視構建的上下文資料,也是Element在連線WidgetRenderObject的橋樑,ElementWidget是一對多的關係。由於Element是可變的,所以通過ElementWidget樹的變化(類似React虛擬DOM diff)做了抽象,可以將真正需要修改的部分同步到真實的RenderObject樹中,最大程度降低對真實渲染檢視的修改,提高渲染效率,而不是銷燬整個渲染檢視樹重建。下面我們來對Element的原始碼一窺究竟。

/// Element物件中儲存的是widget的配置資訊
abstract class Element extends DiagnosticableTree implements BuildContext {
  
  ...

  /// 是一個非常重要的方法,主要是更新子Element的配置資訊。
  @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {...}

  /// 將當前Element新增到Element樹中指定的位置,該位置由父級指定
  /// 該方法會改變當前Element的狀態,由初始化(initial)狀態改為活動(active)狀態。
  @mustCallSuper
  void mount(Element parent, dynamic newSlot) {...}

  /// 更新當前Element對應的Widget
  /// 該方法僅在Element的活動(active)狀態期間呼叫
  @mustCallSuper
  void update(covariant Widget newWidget) {...}

  /// 改變當前Element在父級中的位置
  /// 在MultiChildRenderObjectElement或其他具有多個子元素的[RenderObjectElement]子類中呼叫。如:Flex及其子類(Column、Row)、Stack等
  @protected
  void updateSlotForChild(Element child, dynamic newSlot) {...}

  void _updateSlot(dynamic newSlot) {...}

  void _updateDepth(int parentDepth) {...}

  /// 從render樹中移除當前Element所對應的RenderObject物件
  void detachRenderObject() {...}

  /// 將RenderObject物件新增到render樹中指定的位置上
  void attachRenderObject(dynamic newSlot) {...}

  ...

  /// 初始化Widget,建立Widget對應的Element物件。該方法會呼叫Widget的createElement方法
  /// 該方法通常由updateChild方法呼叫,但也可以由需要對建立Element進行更細粒度控制的子類直接呼叫
  @protected
  Element inflateWidget(Widget newWidget, dynamic newSlot) {...}

  ...

  /// 將指定Element的狀態由活動狀態改為非活動狀態,並從Render樹中移除該Element持有的RenderObject物件
  @protected
  void deactivateChild(Element child) {...}

  /// 從Element的子列表中移除指定的子Element,以準備在Element樹的其他地方重用子Element
  @protected
  void forgetChild(Element child);

  void _activateWithParent(Element parent, dynamic newSlot) {...}

  static void _activateRecursively(Element element) {...}

  /// 將Element由初始化狀態改為活動狀態的具體實現,在mount方法中會呼叫該方法
  @mustCallSuper
  void activate() {...}

  /// 將Element的狀態由活動轉變為非活動狀態(等待)。處於非活動狀態時,該Element不會被Widget使用,亦不會出現在螢幕上。在當前幀結束之前,該Element會一直存在,如果當前幀結束後,該Element尚未被使用,則該Element狀態將改變為銷燬狀態,從而銷燬該Element。
  @mustCallSuper
  void deactivate() {...}

  ...

  /// 將Element的狀態由非活動(等待)狀態改為銷燬狀態,銷燬當前Element
  @mustCallSuper
  void unmount() {...}

  @override
  RenderObject findRenderObject() => renderObject;

  //計算Widget的size,size是從RenderObject中獲取的的
  @override
  Size get size {...}
  
  ...
  
  /// 當Element的依賴發生變化時會呼叫該方法 
  @mustCallSuper   /// 這個是必須呼叫父類方法的註解吧???
  void didChangeDependencies() {...}
  
  ...
  
  /// 將Element元素標記為dirty並新增到全域性列表中,以便下次在下一幀中重新構建
  void markNeedsBuild() {...}

  /// Called by the [BuildOwner] when [BuildOwner.scheduleBuildFor] has been
  /// called to mark this element dirty, by [mount] when the element is first
  /// built, and by [update] when the widget has changed.
  void rebuild() {...}

  /// 在進行一定的檢查後呼叫rebuild()方法
  @protected
  void performRebuild();
}
複製程式碼

Element原始碼裡東西還是蠻多的,但其實只有以下一些核心的方法。

  • updateChild:更新子Element,它主要有以下幾種情況。
newWidget == null newWidget != null
child == null 返回null 返回一個新的Element物件
child != null 移除child並返回null 如果可能就更新child,返回child或者一個新的Element物件。
  • mount:將當前Element新增到Element樹中指定的位置,該位置由父級指定。該方法會改變當前Element的狀態,由初始化(initial)狀態改為活動(active)狀態。
  • update:更新當前Element對應的Widget。該方法僅在Element的活動(active)狀態期間呼叫。
  • updateSlotForChild:改變當前Element在父級中的位置。在MultiChildRenderObjectElement或其他具有多個子元素的RenderObjectElement子類中呼叫。如:Flex及其子類(ColumnRow)、Stack等。
  • detachRenderObject:從render樹中移除當前Element所對應的RenderObject物件。
  • attachRenderObject:將RenderObject物件新增到render樹中指定的位置上。
  • inflateWidget:初始化Widget,建立Widget對應的Element物件。該方法會呼叫WidgetcreateElement方法來建立Element物件。該方法通常由updateChild方法呼叫,但也可以由需要對建立Element進行更細粒度控制的子類直接呼叫。
  • deactivateChild:將子Element的狀態由活動狀態改為非活動狀態,並從render樹中移除該 Element持有的RenderObject物件
  • activate:將Element由初始化狀態改為活動狀態的具體實現,在mount方法中會呼叫該方法
  • deactivate:將Element的狀態由活動轉變為非活動狀態(等待)。處於非活動狀態時,該Element不會被Widget使用,亦不會出現在螢幕上。在當前幀結束之前,該Element會一直存在,如果當前幀結束後,該Element尚未被使用,則該Element狀態將改變為銷燬狀態,從而銷燬該Element
  • unmount:將Element的狀態由非活動(等待)狀態改為銷燬狀態,銷燬當前Element

從上面程式碼中,我們可以發現Element在每一幀內都會在以下幾種狀態之間轉換。

  • initial:初始化狀態,通過createElement方法建立了一個Elmenet物件。
  • active:活動狀態,通過mount方法將Elmenet物件新增到了Elmenet樹中。
  • inactive:等待狀態,通過deactivate方法將Elmenet物件從Elmenet樹中移除。
  • defunct:銷燬狀態,通過unmount方法將Elmenet物件銷燬。

Element的生命週期流程如下:

3、RenderObject介紹

RenderObjectWidgetElement相比,乾的活是最苦逼的。因為要進行進行檢視的具體渲染,將檢視資料繪製成不同層級。如果沒有它,檢視就無法顯示在螢幕上。下面就從原始碼裡一窺究竟。

/// 顧名思義,主要是來繪製介面
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {

  /// 佈局開始
  
  ...
  
  @override
  void attach(PipelineOwner owner) {...}
  
  /// 將當前RenderObject物件的佈局資訊標記為dirty
  void markNeedsLayout() {...}

  /// 將當前RenderObject物件的父物件的佈局資訊標記為dirty
  @protected
  void markParentNeedsLayout() {...}

  /// 該方法裡僅呼叫了markNeedsLayout與markParentNeedsLayout方法
  void markNeedsLayoutForSizedByParentChange() {...}

  void _cleanRelayoutBoundary() {...}

  void scheduleInitialLayout() {...}

  void _layoutWithoutResize() {...}

  /// 完成當前渲染物件的佈局,也是介面UI真正開始佈局的地方。對應著Android中View的layout方法
  void layout(Constraints constraints, { bool parentUsesSize = false }) {...}

  ...

  /// 僅使用約束更新渲染物件大小。
  /// 僅當[sizesByParent]為true時才呼叫此函式。
  @protected
  void performResize();

  /// 為當前Render物件計算佈局大小,不能直接呼叫,由layout方法呼叫
  @protected
  void performLayout();

  
  @protected
  void invokeLayoutCallback<T extends Constraints>(LayoutCallback<T> callback) {...}

  /// 旋轉當前Render物件(尚未實現)
  void rotate({
    int oldAngle, // 0..3
    int newAngle, // 0..3
    Duration time
  }) { }
  
  /// 佈局結束


  // 繪製開始

  ...
 
  /// 將當前Render物件的合成狀態設為dirty
  void markNeedsCompositingBitsUpdate() {...}

  ...
  
  bool _needsPaint = true;

  /// 將當前Render物件標記為需要重新繪製
  void markNeedsPaint() {...}

  void _skippedPaintingOnLayer() {...}

  void scheduleInitialPaint(ContainerLayer rootLayer) {...}

  /// 圖層替換。
  void replaceRootLayer(OffsetLayer rootLayer) {...}

  void _paintWithContext(PaintingContext context, Offset offset) {...}

  ...

  /// 在該方法中進行真正的繪製,一般由子類重寫,
  void paint(PaintingContext context, Offset offset) { }

  ...
  
  /// 繪製結束
  ...
複製程式碼

別看RenderObject原始碼那麼多。但核心方法其實只有兩個。

  • layout:是一個抽象方法,在其子類中具體實現。它主要是實現介面的佈局,通過該方法就會給Widget指定其在螢幕上的位置。筆者認為它對應著Android中View的layout方法。
  • paint:是一個抽象方法,在其子類中具體實現。它主要是在介面上進行具體的繪製,介面上多姿多彩的介面就是通過該方法繪製的。筆者認為它對應著Android中View的draw方法。

通過RenderObject就可以將一個個Widget繪製成對應的圖層,由於圖層往往會非常多,所以直接向GPU傳遞這些圖層資料會非常低效。因此需要在Engine中將這些圖層進行合併及光柵化,最後在將這些處理後的資料傳遞給GPU。

圖片來自Flutter原理與實踐

關於繪製原理的更多知識可以去YouTube看谷歌推出的講解視訊:Flutter’s renderding pipeline

4、演示案例

接下來用一個示例來說明WidgetElementRenderObject三者之間的關係。

  _myWidget() {
    return Center(
      child: Column(
        children: <Widget>[
          Text("1111"),
          Row(
            children: <Widget>[Text("222"), Text("3333")],
          ),
          Icon(Icons.ac_unit)
        ],
      ),
    );
  }
複製程式碼

在上面例子中,有一個Center來讓Widget居中展示,一個Column來讓Widget按照垂直方向排列,一個Row來讓Widget按照水平方向排列,多個TextIcon來展示。

flutter在對上面的Widget遍歷完成以後,就會建立對應的Widget樹、Element樹及RenderObject樹。如下:

注意:由於Text元件不持有RenderObject物件,所以render樹中的Text只是一個泛指。

可以發現,在上面的三個樹中,WidgetElementRenderObject是一一對應的,在Element物件中會同時持有Widget物件及RenderObject物件。

5、結束語

當然,Widget東西非常多,不可能僅憑一篇文章就能夠描述清楚的,本文也只是從整體結構上來對Widget進行一次描述,方便大家深入的去了解Widget

最後來一個思考題,FlutterWidget粒度為什麼那麼細?一位阿里大佬給了我一種思路。

由於Flutter借鑑了React Native的差分演算法來更新介面。那麼粒度越細,更新介面的效果就越好。

那麼大家以為尼???

【參考資料】

Flutter實戰

高效開發與高效能並存的UI框架——攜程Flutter實踐

Flutter Dart Framework原理簡解

[譯]Flutter中的層級結構

Flutter原理與實踐