從源碼解讀Flutter構建

關於三棵樹

Flutter 的核心設計思想是一切皆組件 。Flutter 將組件的概念進行了擴展,把組件的組織和渲染抽象爲三部分,即 Widget,Element 和 RenderObject。

Widget

Widget 只是一個配置,裏面存儲的是有關視圖渲染的配置信息,包括佈局、渲染屬性、事件響應信息等。

Widget 是不可變的,無法更新,數據更新是以重建 Widget 樹的方式進行,會涉及對象的銷燬重建和垃圾回收,所以但是因爲只有配置信息,不涉及渲染繪製,所以重建的成本很低。

Element

Element 是 Widget 的一個實例化對象,它承載了視圖構建的上下文數據,是連接結構化的配置信息到完成最終渲染的橋樑。

Element 是可變的。Element 樹這一層將 Widget 樹的變化(類似 React 虛擬 DOM diff)做了抽象,可以只將真正需要修改的部分同步到真實的 RenderObject 樹中,最大程度降低對真實渲染視圖的修改,提高渲染效率,而不是銷燬整個渲染視圖樹重建。

當新的 Widget 替換舊的 Widget,導致 Element 變化,也就是說,多個 Widget 對應一個 Element。

RenderObject

RenderObject 是主要負責實現視圖渲染的對象。

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

BuildContext

BuildContext 對象實際上是 Element 對象。BuildContext 接口用於阻止對 Element 對象的直接操作。

我們日常開發中,一般接觸的都是 Widget,並沒有使用到 Element,其實我們也在一直操作着 Element,BuildContext 對象實際上就是 Element 對象, Element 實現了 BuildContext,告訴了使用者控件在哪裏、可以做什麼。BuildContext 接口設計用於阻止對 Element 對象的直接操作。

我們可以把 Widget 當做菜譜,Element 是配菜,RenderObject 是燒菜和出菜。

流程

RenderObjectWidget 介紹

StatelessWidget 和 StatefulWidget 只是用來組裝控件的容器,並不負責組件最後的佈局和繪製。在 Flutter 中,佈局和繪製工作實際上是在 Widget 的另一個子類 RenderObjectWidget 內完成的。比如 Text:

class Text extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // 返回的是 RichText
    Widget result = RichText(...)
    ...
  }
}

class RichText extends MultiChildRenderObjectWidget{}

abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {}

我們再來看一下 RenderObjectWidget 的源碼,來看看如何使用 Element 和 RenderObject 完成圖形渲染工作。


abstract class RenderObjectWidget extends Widget {
  @override
  RenderObjectElement createElement();
  @protected
  RenderObject createRenderObject(BuildContext context);
  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
  ...
}

對於 Element 的創建,Flutter 會在遍歷 Widget 樹時,調用 createElement 去同步 Widget 自身配置,從而生成對應節點的 Element 對象。而對於 RenderObject 的創建與更新,其實是在 RenderObjectElement 類中完成的。


abstract class RenderObjectElement extends Element {
  RenderObject _renderObject;

  @override
  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _renderObject = widget.createRenderObject(this);
    attachRenderObject(newSlot);
    _dirty = false;
  }
   
  @override
  void update(covariant RenderObjectWidget newWidget) {
    super.update(newWidget);
    widget.updateRenderObject(this, renderObject);
    _dirty = false;
  }
  ...
}

在 Element 創建完畢後,Flutter 會調用 Element 的 mount 方法。在這個方法裏,會完成與之關聯的 RenderObject 對象的創建,以及與渲染樹的插入工作,插入到渲染樹後的 Element 就可以顯示到屏幕中了。

構建

mount被調用,發生第一次構建或者 Widget 改變update被調用,會在rebuild中調用performRebuild,在此方法中,父元素調用updateChild方法。在此構建階段,父元素檢查子元素是否可以更新、刪除或添加到元素樹中。

  @protected
  @pragma('vm:prefer-inline')
  Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {}

通過註釋我們知道,這個是 widgets 系統的核心。每次要根據更新的配置添加、更新或刪除子元素時,都會調用它。

  • child:由其父元素檢查以在當前構建過程中添加、刪除、更新或重用的元素。

  • newWidget:子元素將在當前構建過程中引用的小部件 Widget 。

我們從第一個 case 開始看:

    if (newWidget == null) {
      if (child != null)
        deactivateChild(child);
      return null;
    }

如果新小部件爲null,並且子元素不爲空,則框架從元素樹中刪除子元素。父元素調用deactivateChild將子元素置於非活動狀態的方法,並將子元素的相應渲染對象從渲染樹中分離出來。對應的場景例如:刷新列表後列表數據爲空。

再看第二個 case

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

如果子元素和新小部件都是非空的,並且子元素的舊小部件和新小部件是相同的實例,那麼當前子元素被重用而不需要更新。因此,不會調用相應子小部件的構建方法。就性能而言,這是最理想的情況。也就是負責配置的 Widget 沒有改變,那麼會重用這個元素,直接返回。

再看第三個 case

else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
        if (child.slot != newSlot)
          updateSlotForChild(child, newSlot);
        child.update(newWidget);
        assert(child.widget == newWidget);
        assert(() {
          child.owner!._debugElementWasRebuilt(child);
          return true;
        }());
        newChild = child;
      }

如果子元素和新小部件都是非空的,並且新舊小部件不是同一個實例,但 canUpdate 方法返回 true,通過新的配置更新子元素。對應的場景例如:Text的文本發生改變後引起的構建。

再看第四個 case

else {
        deactivateChild(child);
        assert(child._parent == null);
        newChild = inflateWidget(newWidget, newSlot);
      }

如果子元素和新小部件都是非空的,並且新舊小部件不相同但canUpdate返回false,那麼deactivateChild刪除子元素,相應的渲染對象與渲染樹分離。最後,將新小部件的新元素返回到元素樹。這是在性能方面最昂貴的情況,因爲實例化了新元素節點和渲染對象節點。對應的場景例如:Text變成了Button

再看第五個 case

 else {
      newChild = inflateWidget(newWidget, newSlot);
    }

如果子元素爲 null,並且其新子元素爲非 null,那麼說明構建階段的樹的此位置有一個新的小部件。因此,父元素首先調用inflateWidget,在其中調用新小部件方法的createElement方法,並返回新小部件的新元素。此時,父級也爲創建的元素設置了一個槽。對應的場景例如:在空的row裏添加了一個Button

當然,第一個構建時,都會走到這個 case

更新

StatefulWidget被加載時,StatefulElement會被創建,StatefulElement的構造方法中,通過createState方法創建State,並將StatefulWidgetState對象將永久關聯。

  StatefulElement(StatefulWidget widget)
      : _state = widget.createState(),

mount中,調用_firstBuild,然後調用state.initState() ,初始化State,並且之後調用一次didChangeDependencies

  @override
  void mount(Element? parent, Object? newSlot) {
    super.mount(parent, newSlot);
    ...
    _firstBuild();
        ...
  }
  @override
  void _firstBuild() {
    assert(state._debugLifecycleState == _StateLifecycle.created);
    try {
      _debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
      final Object? debugCheckForReturnedFuture = state.initState() as dynamic;
      ...
      state.didChangeDependencies();
            ...
            }

而在上面第三個case中,child.update(newWidget)會先調用state.didUpdateWidget(oldWidget),再build,所以我們重寫didUpdateWidget方法時,State由於保證該build方法會在didUpdateWidget之後被調用,因此無需在didUpdateWidget中顯式觸發構建。

 @override
  void update(StatefulWidget newWidget) {
    super.update(newWidget);
        ...
    final Object? debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) as dynamic;
        ...
    rebuild();
  }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章