深入理解 Widget,構建 Flutter 界面的基石

(一)什么是 Widget

在官方的架構圖中,Widget 是整個視圖描述的基礎。Widget 是 Flutter 功能的抽象描述,是視圖的配置信息,同樣也是數據的映射,是 Flutter 開發框架中最基本的概念。前端框架中常見的名字,比如視圖(View)、視圖控制器(View Controller)、活動(Activity)、應用(Application)、布局(Layout)等,在 Flutter 中都是 Widget。

Flutter 架構圖

事實上,Flutter 的核心設計思想便是“一切皆 Widget”。所以,學習 Flutter,首先得從學會使用 Widget 開始。


(二)Widget 渲染過程

在開發中,我們往往會關注的一個問題:如何結構化地組織視圖數據,提供給渲染引擎,最終完成界面顯示。
通常情況下,無一例外,都會用到視圖樹(View Tree)的概念。而 Flutter 將視圖樹的概念進行了擴展,把視圖數據的組織和渲染抽象為三部分,即 Widget,Element 和 RenderObject。

三者的關系如下:


Widget,Element 和 RenderObject
(1)Widget

Widget 是 Flutter 中對視圖的一種結構化描述,它可以看作是前端中的“控件”或“組件”。Widget 是控件實現的基本邏輯單位,里面存儲的是有關視圖渲染的配置信息,包括布局、渲染屬性、時間響應信息等。

在頁面渲染上,Flutter 將 Widget 設計成不可變的,所以當視圖渲染的配置信息發生變化時,Flutter 會選擇重建 Widget 樹的方式進行數據更新,以數據驅動 UI 構建的方式簡單高效。

缺點就是涉及到大量對象的銷毀和重建,所以會對垃圾回收造成壓力。不過,Widget 本身并不涉及實際渲染位圖,所以它只是一份輕量級的數據結構,重建的成本很低。

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

(2)Element

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

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

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

可以看到,Element 同時持有 Widget 和 RenderObject。最終負責渲染工作的只有 RenderObject。那么,為什么要增加 Element 樹這個中間層呢?而不是由 Widget 直接命令 RenderObject 呢?

答案是,可以,但是這樣做會極大地增加渲染帶來的性能損耗。

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

這就是 Element 樹存在的意義。

(3)RenderObject

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

Flutter 通過控件樹(Widget 樹)中的每個控件(Widget)創建不同類型的渲染對象,組成渲染對象樹。

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


(二)RenderObjectWidget

我們知道,在 Flutter 中有 StatelessWidget 和 StatefulWidget 兩種用來組裝控件的容器,但并不負責組件最后的布局和繪制。在 Flutter 中,布局和繪制工作實際上是在 Widget 的另一個子類 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) { }
  ...
}

RenderObjectWidget 是一個抽象類。我們通過源碼可以看到,這個類中同時擁有創建 Element、RenderObject,以及更新 RenderObject 的方法。

但實際上,RenderObjectWidget 本身并不負責這些對象的創建和更新。

對于 Element 的創建,Flutter 會在遍歷 Widget 樹時,調用 creatElement 去同步 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。


(三)案例

根據下面的案例,了解說明 Widget、Element 與 RenderObject 在渲染過程中的關系。

一個 Row 容器放置了 4 個子 Widget,左邊是 Image,右邊是一個 Column 容器下排布的兩個 Text。


案例界面

那么,在 Flutter 遍歷完 Widget 樹,創建了各個子 Widget 對應的 Element 的同時,也創建了與之關聯的、負責實際布局和繪制的 RenderObject。


案例界面生成的“三棵樹”

總結

主要學習了 Flutter 中視圖數據的組織和渲染抽象的三個核心概念:Widget、Element 和 RenderObject。

Widget 是 Flutter 世界里對視圖的一種結構化描述,里面儲存的是有關視圖渲染的配置信息;
Element 是 Widget 的一個實例化對象,講 Widget 樹的變化做了抽象,能夠做到只將真正需要修改的部分同步到真是的 RenderObject 樹中,最大程度地優化了從結構化的配置信息到完成最終渲染的過程;
RenderObject 是負責實現視圖的最終呈現,通過布局、繪制完成界面的展示。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,739評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,634評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,653評論 0 377
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,063評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,835評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,235評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,315評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,459評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,000評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,819評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,004評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,560評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,257評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,676評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,937評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,717評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,003評論 2 374

推薦閱讀更多精彩內容