(一)什么是 Widget
在官方的架構圖中,Widget 是整個視圖描述的基礎。Widget 是 Flutter 功能的抽象描述,是視圖的配置信息,同樣也是數據的映射,是 Flutter 開發框架中最基本的概念。前端框架中常見的名字,比如視圖(View)、視圖控制器(View Controller)、活動(Activity)、應用(Application)、布局(Layout)等,在 Flutter 中都是 Widget。
事實上,Flutter 的核心設計思想便是“一切皆 Widget”。所以,學習 Flutter,首先得從學會使用 Widget 開始。
(二)Widget 渲染過程
在開發中,我們往往會關注的一個問題:如何結構化地組織視圖數據,提供給渲染引擎,最終完成界面顯示。
通常情況下,無一例外,都會用到視圖樹(View Tree)的概念。而 Flutter 將視圖樹的概念進行了擴展,把視圖數據的組織和渲染抽象為三部分,即 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 是負責實現視圖的最終呈現,通過布局、繪制完成界面的展示。