Widget 基礎

宋魚000發表於2019-09-25

一切皆Widget

Widget 渲染過程

Flutter把檢視資料的組織和渲染抽象為三部分,即 Widget、Element 和 RenderObject。

Widget

Widget 是空間實現的基本邏輯單位,裡面儲存的是有關檢視渲染的配置資訊,包括佈局、渲染屬性、事件響應資訊等。

頁面渲染遵循“Simple is best”(簡單是最好的)理念。Flutter 將 Widget 設計成不可變的,所以當檢視渲染的配置資訊發生變化時,Flutter 會選擇重建 Widget 樹的方式進行資料更新,以資料驅動 UI 構建的方式簡單高效。

缺點是,因為涉及到大量物件的銷燬和重建,所以會對垃圾回收造成壓力。但是,Widget 本身並不涉及實際渲染點陣圖,所以它只是一份輕量級的資料結構,重建成本很低。

另外,由於 Widget 的不可變行,可以以較低成本進行渲染節點複用,因此在一個真實的渲染樹中可能存在不同的 Widget 對應同一個渲染節點的情況,這無疑又降低了重建 UI 的成本。

Element

Element 是 Widget 的一個例項化物件,它承載了檢視構建的上下文資料,是連線結構化的配置資訊到完成最終渲染的橋樑。

Flutter 渲染過程,可以分三步:

  • 首先,通過 Widget 樹生成對應的 Element樹;
  • 然後,建立相應的 RenderObject 並關聯到 Element.renderObject 屬性上;
  • 最後,構建成 RenderObject 樹,以完成最終的渲染。

Element 同時持有 Widget 和 RenderObject,無論是 Widget 還是 Element,其實都不負責最後的渲染,只負責發號施令,真正幹活兒的只有 RenderObject。

為什麼不直由 Widget 命令 RenderObject 去幹活兒,而是增加 Element 樹?

Widget 直接命令,會極大地增加渲染帶來的效能損耗。

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

RenderObject

RenderObject 是主要負責實現檢視渲染的物件。

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

Flutter 通過引入 Widget、Element 與 RenderObject 這三個概念,把原本從檢視資料到檢視渲染的複雜構建過程拆分得更簡單、直接,在易於集中治理的同時,保證了較高的渲染效率。

RenderObjectWidget 介紹

在 Flutter 中,佈局和繪製工作實際上是在 Widget 的另一個子類 RenderObjectWidget 內完成的。RenderObjectWidget 原始碼如下:

abstract class RenderObjectWidget extends Widget {
  @override
  RenderObjectElement createElement();
  @protected
  RenderObject createRenderObject(BuildContext context);
  @protected
  void updateRenderObjct(BuildContext context, covariant RenderObject renderObjct);
  ...
}
複製程式碼
RenderObjectWidget 是一個抽象類。這個類中同時擁有建立 Element、RenderObject,以及更新 RenderObject 的方法。但實際上,RenderObjectWidget 本身並不負責這些物件的建立與更新。

對於 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 就可以顯示到螢幕中了。

如果 Widget 的配置資料發生了改變,那麼持有該 Widget 的 Element 節點會被標記為 dirty。在下一個週期的繪製時,Flutter 就會觸發 Element 樹的更新,並使用最新的 Widget 資料更新自身以及關聯的 RenderObject 物件,接下來便會進入 Layout 和 Paint 的流程。而真正的繪製和佈局過程,則完全交由 RenderObject 完成:

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
  ...
  void layout(Constraints constraints, {bool parentUsesSize = false}){...}
  void paint(PaintingContext context, Offset offset){}
}
複製程式碼
佈局和繪製完成後,接下來的事情就交給 Skia 了。在 VSync 訊號同步時直接從渲染樹合成 Bitmap,然後提交給 GPU。

相關文章