在前面的 Flutter 開發起步和 Dart 基礎模塊中,我和你一起學習了 Flutter 框架的整體架構與基本原理,分析了 Flutter 的項目結構和運行機制,并從 Flutter 開發角度介紹了 Dart 語言的基本設計思路,也通過和其他高級語言的類比深入認識了 Dart 的語法特性。
這些內容,是我們接下來系統學習構建 Flutter 應用的基礎,可以幫助我們更好地掌握 Flutter 的核心概念和技術。
在第 4 篇文章“[Flutter 區別于其他方案的關鍵技術是什么?](https://time.geekbang.org/column/article/105703)”中,我和你分享了一張來自 Flutter 官方的架構圖,不難看出 Widget 是整個視圖描述的基礎。這張架構圖很重要,所以我在這里又放了一次。
:-: 
圖 1 Flutter 架構圖
備注:此圖引自[Flutter System Overview](https://flutter.dev/docs/resources/technical-overview)
那么,Widget 到底是什么呢?
Widget 是 Flutter 功能的抽象描述,是視圖的配置信息,同樣也是數據的映射,是 Flutter 開發框架中最基本的概念。前端框架中常見的名詞,比如視圖(View)、視圖控制器(View Controller)、活動(Activity)、應用(Application)、布局(Layout)等,在 Flutter 中都是 Widget。
事實上,**Flutter 的核心設計思想便是“一切皆 Widget”**。所以,我們學習 Flutter,首先得從學會使用 Widget 開始。
那么,在今天的這篇文章中,我會帶著你一起學習 Widget 在 Flutter 中的設計思路和基本原理,以幫助你深入理解 Flutter 的視圖構建過程。
## Widget 渲染過程
在進行 App 開發時,我們往往會關注的一個問題是:如何結構化地組織視圖數據,提供給渲染引擎,最終完成界面顯示。
通常情況下,不同的 UI 框架中會以不同的方式去處理這一問題,但無一例外地都會用到視圖樹(View Tree)的概念。而 Flutter 將視圖樹的概念進行了擴展,把視圖數據的組織和渲染抽象為三部分,即 Widget,Element 和 RenderObject。
這三部分之間的關系,如下所示:
:-: 
圖 2 Widget,Element 與 RenderObject
### Widget
Widget 是 Flutter 世界里對視圖的一種結構化描述,你可以把它看作是前端中的“控件”或“組件”。Widget 是控件實現的基本邏輯單位,里面存儲的是有關視圖渲染的配置信息,包括布局、渲染屬性、事件響應信息等。
在頁面渲染上,**Flutter 將“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。那你可能會問,**既然都是發號施令,那為什么需要增加中間的這層 Element 樹呢?直接由 Widget 命令 RenderObject 去干活兒不好嗎?**
答案是,可以,但這樣做會極大地增加渲染帶來的性能損耗。
因為 Widget 具有不可變性,但 Element 卻是可變的。實際上,Element 樹這一層將 Widget 樹的變化(類似 React 虛擬 DOM diff)做了抽象,可以只將真正需要修改的部分同步到真實的 RenderObject 樹中,最大程度降低對真實渲染視圖的修改,提高渲染效率,而不是銷毀整個渲染視圖樹重建。
這,就是 Element 樹存在的意義。
### RenderObject
從其名字,我們就可以很直觀地知道,RenderObject 是主要負責實現視圖渲染的對象。
在前面的第 4 篇文章“[Flutter 區別于其他方案的關鍵技術是什么?](https://time.geekbang.org/column/article/105703)”中,我們提到,Flutter 通過控件樹(Widget 樹)中的每個控件(Widget)創建不同類型的渲染對象,組成渲染對象樹。
而渲染對象樹在 Flutter 的展示過程分為四個階段,即布局、繪制、合成和渲染。 其中,布局和繪制在 RenderObject 中完成,Flutter 采用深度優先機制遍歷渲染對象樹,確定樹中各個對象的位置和尺寸,并把它們繪制到不同的圖層上。繪制完畢后,合成和渲染的工作則交給 Skia 搞定。
Flutter 通過引入 Widget、Element 與 RenderObject 這三個概念,把原本從視圖數據到視圖渲染的復雜構建過程拆分得更簡單、直接,在易于集中治理的同時,保證了較高的渲染效率。
## RenderObjectWidget 介紹
通過第 5 篇文章“[從標準模板入手,體會 Flutter 代碼是如何運行在原生系統上的](https://time.geekbang.org/column/article/106199)”的介紹,你應該已經知道如何使用 StatelessWidget 和 StatefulWidget 了。
不過,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 樹時,調用 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。這部分內容,我已經在之前的“[Flutter 區別于其他方案的關鍵技術是什么](https://time.geekbang.org/column/article/105703)?”中與你介紹過了,這里就不再贅述了。
接下來,我以下面的界面示例為例,與你說明 Widget、Element 與 RenderObject 在渲染過程中的關系。在下面的例子中,一個 Row 容器放置了 4 個子 Widget,左邊是 Image,而右邊則是一個 Column 容器下排布的兩個 Text。
:-: 
圖 3 界面示例
那么,在 Flutter 遍歷完 Widget 樹,創建了各個子 Widget 對應的 Element 的同時,也創建了與之關聯的、負責實際布局和繪制的 RenderObject。
:-: 
圖 4 示例界面生成的“三棵樹”
## 總結
好了,今天關于 Widget 的設計思路和基本原理的介紹,我們就先進行到這里。接下來,我們一起回顧下今天的主要內容吧。
首先,我與你介紹了 Widget 渲染過程,學習了在 Flutter 中視圖數據的組織和渲染抽象的三個核心概念,即 Widget、 Element 和 RenderObject。
其中,Widget 是 Flutter 世界里對視圖的一種結構化描述,里面存儲的是有關視圖渲染的配置信息;Element 則是 Widget 的一個實例化對象,將 Widget 樹的變化做了抽象,能夠做到只將真正需要修改的部分同步到真實的 Render Object 樹中,最大程度地優化了從結構化的配置信息到完成最終渲染的過程;而 RenderObject,則負責實現視圖的最終呈現,通過布局、繪制完成界面的展示。
最后,在對 Flutter Widget 渲染過程有了一定認識后,我帶你閱讀了 RenderObjectWidget 的代碼,理解 Widget、Element 與 RenderObject 這三個對象之間是如何互相配合,實現圖形渲染工作的。
熟悉了 Widget、Element 與 RenderObject 這三個概念,相信你已經對組件的渲染過程有了一個清晰而完整的認識。這樣,我們后續再學習常用的組件和布局時,就能夠從不同的視角去思考框架設計的合理性了。
不過在日常開發學習中,絕大多數情況下,我們只需要了解各種 Widget 特性及使用方法,而無需關心 Element 及 RenderObject。因為 Flutter 已經幫我們做了大量優化工作,因此我們只需要在上層代碼完成各類 Widget 的組裝配置,其他的事情完全交給 Flutter 就可以了。
## 思考題
你是如何理解 Widget、Element 和 RenderObject 這三個概念的?它們之間是一一對應的嗎?你能否在 Android/iOS/Web 中找到對應的概念呢?
- 前言
- 開篇詞
- 預習篇
- 01丨預習篇 · 從0開始搭建Flutter工程環境
- 02丨預習篇 · Dart語言概覽
- Flutter開發起步
- 03丨深入理解跨平臺方案的歷史發展邏輯
- 04丨Flutter區別于其他方案的關鍵技術是什么?
- 05丨從標準模板入手,體會Flutter代碼是如何運行在原生系統上的
- Dart語言基礎
- 06丨基礎語法與類型變量:Dart是如何表示信息的?
- 07丨函數、類與運算符:Dart是如何處理信息的?
- 08丨綜合案例:掌握Dart核心特性
- Flutter基礎
- 09丨Widget,構建Flutter界面的基石
- 10丨Widget中的State到底是什么?
- 11丨提到生命周期,我們是在說什么?
- 12丨經典控件(一):文本、圖片和按鈕在Flutter中怎么用?
- 13丨ListView在Flutter中是什么?
- 14 丨 經典布局:如何定義子控件在父容器中排版位置?
- 15 丨 組合與自繪,我該選用何種方式自定義Widget?
- 16 丨 從夜間模式說起,如何定制不同風格的App主題?
- 17丨依賴管理(一):圖片、配置和字體在Flutter中怎么用?
- 18丨依賴管理(二):第三方組件庫在Flutter中要如何管理?
- 19丨用戶交互事件該如何響應?
- 20丨關于跨組件傳遞數據,你只需要記住這三招
- 21丨路由與導航,Flutter是這樣實現頁面切換的
- Flutter進階
- 22丨如何構造炫酷的動畫效果?
- 23丨單線程模型怎么保證UI運行流暢?
- 24丨HTTP網絡編程與JSON解析
- 25丨本地存儲與數據庫的使用和優化
- 26丨如何在Dart層兼容Android-iOS平臺特定實現?(一)
- 27丨如何在Dart層兼容Android-iOS平臺特定實現?(二)
- 28丨如何在原生應用中混編Flutter工程?
- 29丨混合開發,該用何種方案管理導航棧?
- 30丨為什么需要做狀態管理,怎么做?
- 31丨如何實現原生推送能力?
- 32丨適配國際化,除了多語言我們還需要注意什么
- 33丨如何適配不同分辨率的手機屏幕?
- 34丨如何理解Flutter的編譯模式?
- 35丨HotReload是怎么做到的?
- 36丨如何通過工具鏈優化開發調試效率?
- 37丨如何檢測并優化FlutterApp的整體性能表現?
- 38丨如何通過自動化測試提高交付質量?
- Flutter綜合應用
- 39丨線上出現問題,該如何做好異常捕獲與信息采集?
- 40丨衡量FlutterApp線上質量,我們需要關注這三個指標
- 41丨組件化和平臺化,該如何組織合理穩定的Flutter工程結構?
- 42丨如何構建高效的FlutterApp打包發布環境?
- 43丨如何構建自己的Flutter混合開發框架(一)?
- 44丨如何構建自己的Flutter混合開發框架(二)?
- 結束語
- 結束語丨勿畏難,勿輕略