Flutter之Widget層級介紹

flutter中,一切皆Widget。不管是顯示界面的UI元素,如TextImageIcon等;仍是功能性組件,如手勢檢測的GestureDetector組件、應用主題數據傳遞的Theme組件、移除系統組件自帶Padding的MediaQuery組件等。能夠說,flutter界面就是由一個個粒度很是細的Widget組合起來的。html

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

一、Widget介紹

前面說過Widget是一種很是輕量且不可變的數據結構,只起一個組織者做用。那麼它是如何輕量的尼?下面咱們就從源碼來一窺究竟。markdown

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多幾個方法。ide

  • createRenderObject:建立RenderObject對象,在該對象中會將視圖數據繪製到不一樣的圖層上。筆者認爲它對應着Android中ViewManager的addView方法。
  • updateRenderObject:更新Widget所持有的RenderObject對象。筆者認爲它對應着Android中ViewManager的updateViewLayout方法。
  • didUnmountRenderObject:將RenderObject對象從Render樹中移除,也就是銷燬RenderObject對象。筆者認爲它對應着Android中ViewManager的removeView方法。

因爲RenderObject主要是將視圖繪製成不一樣的圖層,而後再顯示在屏幕上。因此只有當咱們的組件直接或間接繼承自RenderObjectWidget時,纔會經過RenderObject來進行繪製、渲染,從而顯示在屏幕上,如RichTextRowCenter等。不然只是一個用來組裝組件的容器,如TextListView等。函數

二、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源碼裏東西仍是蠻多的,但其實只有如下一些核心的方法。post

  • 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的生命週期流程以下:

三、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

四、演示案例

接下來用一個示例來講明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對象。

五、結束語

固然,Widget東西很是多,不可能僅憑一篇文章就可以描述清楚的,本文也只是從總體結構上來對Widget進行一次描述,方便你們深刻的去了解Widget

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

因爲Flutter借鑑了React Native的差分算法來更新界面。那麼粒度越細,更新界面的效果就越好。

那麼你們覺得尼???

【參考資料】

Flutter實戰

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

Flutter Dart Framework原理簡解

[譯]Flutter中的層級結構

Flutter原理與實踐

相關文章
相關標籤/搜索