Flutter 的核心設(shè)計思想便是“一切皆 Widget”
Flutter 將視圖樹的概念進行了擴展,把視圖數(shù)據(jù)的組織和渲染抽象為三部分,即 Widget,Element 和 RenderObject。Widget 是 Flutter 世界里對視圖的一種結(jié)構(gòu)化描述,里面存儲的是有關(guān)視圖渲染的配置信息;Element 則是 Widget 的一個實例化對象,將 Widget 樹的變化做了抽象,能夠做到只將真正需要修改的部分同步到真實的 Render Object 樹中,最大程度地優(yōu)化了從結(jié)構(gòu)化的配置信息到完成最終渲染的過程;而 RenderObject,則負責實現(xiàn)視圖的最終呈現(xiàn),通過布局、繪制完成界面的展示。

Widget
是控件實現(xiàn)的基本邏輯單位,里面存儲的是有關(guān)視圖渲染的配置信息,包括布局、渲染屬性、事件響應(yīng)信息等。
Flutter 將 Widget 設(shè)計成不可變的,所以當視圖渲染的配置信息發(fā)生變化時,F(xiàn)lutter 會選擇重建 Widget 樹的方式進行數(shù)據(jù)更新,以數(shù)據(jù)驅(qū)動 UI 構(gòu)建的方式簡單高效。
這樣做的缺點是,因為涉及到大量對象的銷毀和重建,所以會對垃圾回收造成壓力。不過,Widget 本身并不涉及實際渲染位圖,所以它只是一份輕量級的數(shù)據(jù)結(jié)構(gòu),重建的成本很低。
由于 Widget 的不可變性,可以以較低成本進行渲染節(jié)點復(fù)用,因此在一個真實的渲染樹中可能存在不同的 Widget 對應(yīng)同一個渲染節(jié)點的情況,這無疑又降低了重建 UI 的成本。
Element
Element 是 Widget 的一個實例化對象,它承載了視圖構(gòu)建的上下文數(shù)據(jù),是連接結(jié)構(gòu)化的配置信息到完成最終渲染的橋梁。
Flutter 渲染過程,可以分為三步:
通過 Widget 樹生成對應(yīng)的 Element 樹;
創(chuàng)建相應(yīng)的 RenderObject 并關(guān)聯(lián)到 Element.renderObject 屬性上
構(gòu)建成 RenderObject 樹,以完成最終的渲染
而無論是 Widget 還是 Element,其實都不負責最后的渲染,只負責發(fā)號施令,真正去干活兒的只有 RenderObject。如果跨過Element直接由Widget樹命令RenderObject渲染將有巨大的性能開銷。而Element 樹這一層將 Widget 樹的變化(類似 React 虛擬 DOM diff)做了抽象,可以只將真正需要修改的部分同步到真實的 RenderObject 樹中,最大程度降低對真實渲染視圖的修改,提高渲染效率,而不是銷毀整個渲染視圖樹重建。
RenderObject
主要負責實現(xiàn)視圖渲染的對象。渲染對象樹在 Flutter 的展示過程分為四個階段,即布局、繪制、合成和渲染。
布局和繪制在 RenderObject 中完成,F(xiàn)lutter 采用深度優(yōu)先機制遍歷渲染對象樹,確定樹中各個對象的位置和尺寸,并把它們繪制到不同的圖層上。繪制完畢后,合成和渲染的工作則交給 Skia 搞定。
RenderObjectWidget
StatelessWidget 和 StatefulWidget 只是用來組裝控件的容器,并不負責組件最后的布局和繪制。在 Flutter 中,布局和繪制工作實際上是在 Widget 的另一個子類 RenderObjectWidget 內(nèi)完成的。
abstract class RenderObjectWidget extends Widget {
@override
RenderObjectElement createElement();
@protected
RenderObject createRenderObject(BuildContext context);
@protected
void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }
...
}
RenderObjectWidget 是一個抽象類。我們通過源碼可以看到,這個類中同時擁有創(chuàng)建 Element、RenderObject,以及更新 RenderObject 的方法。
對于 Element 的創(chuàng)建,F(xiàn)lutter 會在遍歷 Widget 樹時,調(diào)用 createElement 去同步 Widget 自身配置,從而生成對應(yīng)節(jié)點的 Element 對象。而對于 RenderObject 的創(chuàng)建與更新,其實是在 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 創(chuàng)建完畢后,F(xiàn)lutter 會調(diào)用 Element 的 mount 方法。在這個方法里,會完成與之關(guān)聯(lián)的 RenderObject 對象的創(chuàng)建,以及與渲染樹的插入工作,插入到渲染樹后的 Element 就可以顯示到屏幕中了。
如果 Widget 的配置數(shù)據(jù)發(fā)生了改變,那么持有該 Widget 的 Element 節(jié)點也會被標記為 dirty。在下一個周期的繪制時,F(xiàn)lutter 就會觸發(fā) Element 樹的更新,并使用最新的 Widget 數(shù)據(jù)更新自身以及關(guān)聯(lián)的 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 在底層做了大量的渲染優(yōu)化工作,使得我們只需要通過組合、嵌套不同類型的 Widget,就可以構(gòu)建出任意功能、任意復(fù)雜度的界面。
UI 編程范式
Flutter 的視圖開發(fā)是聲明式的,其核心設(shè)計思想就是將視圖和數(shù)據(jù)分離
除了設(shè)計好 Widget 布局方案之外,還需要提前維護一套文案數(shù)據(jù)集,并為需要變化的 Widget 綁定數(shù)據(jù)集中的數(shù)據(jù),使 Widget 根據(jù)這個數(shù)據(jù)集完成渲染。
命令式編程強調(diào)精確控制過程細節(jié);而聲明式編程強調(diào)通過意圖輸出結(jié)果整體。對應(yīng)到 Flutter 中,意圖是綁定了組件狀態(tài)的 State,結(jié)果則是重新渲染后的組件。在 Widget 的生命周期內(nèi),應(yīng)用到 State 中的任何更改都將強制 Widget 重新構(gòu)建。
當你所要構(gòu)建的用戶界面不隨任何狀態(tài)信息的變化而變化時,需要選擇使用 StatelessWidget,反之則選用 StatefulWidget。前者一般用于靜態(tài)內(nèi)容的展示,而后者則用于存在交互反饋的內(nèi)容呈現(xiàn)中。
什么場景下應(yīng)該使用 StatelessWidget 呢?
一個簡單的判斷規(guī)則是:父 Widget 是否能通過初始化參數(shù)完全控制其 UI 展示效果?如果能,那么我們就可以使用 StatelessWidget 來設(shè)計構(gòu)造函數(shù)接口了。
正確評估你的視圖展示需求,避免無謂的 StatefulWidget 使用,是提高 Flutter 應(yīng)用渲染性能最簡單也是最直接的手段。
除了我們主動地通過 State 刷新 UI 之外,在一些特殊場景下,Widget 的 build 方法有可能會執(zhí)行多次。
State 生命周期

State 的生命周期可以分為 3 個階段:創(chuàng)建(插入視圖樹)、更新(在視圖樹中存在)、銷毀(從視圖樹中移除)
State 初始化時會依次執(zhí)行 :構(gòu)造方法 -> initState -> didChangeDependencies -> build,隨后完成頁面渲染。
構(gòu)造方法是 State 生命周期的起點,F(xiàn)lutter 會通過調(diào)用 StatefulWidget.createState() 來創(chuàng)建一個 State。我們可以通過構(gòu)造方法,來接收父 Widget 傳遞的初始化 UI 配置數(shù)據(jù)。這些配置數(shù)據(jù),決定了 Widget 最初的呈現(xiàn)效果。
initState,會在 State 對象被插入視圖樹的時候調(diào)用。這個函數(shù)在 State 的生命周期中只會被調(diào)用一次,所以我們可以在這里做一些初始化工作,比如為狀態(tài)變量設(shè)定默認值。
didChangeDependencies 則用來專門處理 State 對象依賴關(guān)系變化,會在 initState() 調(diào)用結(jié)束后,被 Flutter 調(diào)用。
build,作用是構(gòu)建視圖。經(jīng)過以上步驟,F(xiàn)ramework 認為 State 已經(jīng)準備好了,于是調(diào)用 build。我們需要在這個函數(shù)中,根據(jù)父 Widget 傳遞過來的初始化配置數(shù)據(jù),以及 State 的當前狀態(tài),創(chuàng)建一個 Widget 然后返回。
Widget 的狀態(tài)更新,主要由 3 個方法觸發(fā):setState、didchangeDependencies 與 didUpdateWidget。
setState:我們最熟悉的方法之一。當狀態(tài)數(shù)據(jù)發(fā)生變化時,我們總是通過調(diào)用這個方法告訴 Flutter更新UI。
didChangeDependencies:State 對象的依賴關(guān)系發(fā)生變化后,F(xiàn)lutter 會回調(diào)這個方法,隨后觸發(fā)組件構(gòu)建。哪些情況下 State 對象的依賴關(guān)系會發(fā)生變化呢?典型的場景是,系統(tǒng)語言 Locale 或應(yīng)用主題改變時,系統(tǒng)會通知 State 執(zhí)行 didChangeDependencies 回調(diào)方法。
didUpdateWidget:當 Widget 的配置發(fā)生變化時,比如,父 Widget 觸發(fā)重建(即父 Widget 的狀態(tài)發(fā)生變化時),熱重載時,系統(tǒng)會調(diào)用這個函數(shù)。
一旦這三個方法被調(diào)用,F(xiàn)lutter 隨后就會銷毀老 Widget,并調(diào)用 build 方法重建 Widget。
組件銷毀相對比較簡單。比如組件被移除,或是頁面銷毀的時候,系統(tǒng)會調(diào)用 deactivate 和 dispose 這兩個方法,來移除或銷毀組件。
當組件的可見狀態(tài)發(fā)生變化時,deactivate 函數(shù)會被調(diào)用,這時 State 會被暫時從視圖樹中移除。值得注意的是,頁面切換時,由于 State 對象在視圖樹中的位置發(fā)生了變化,需要先暫時移除后再重新添加,重新觸發(fā)組件構(gòu)建,因此這個函數(shù)也會被調(diào)用。
-
當 State 被永久地從視圖樹中移除時,F(xiàn)lutter 會調(diào)用 dispose 函數(shù)。而一旦到這個階段,組件就要被銷毀了,所以我們可以在這里進行最終的資源釋放、移除監(jiān)聽、清理環(huán)境,等等。
image-20211029162633985.png
APP生命周期
App 的生命周期,則定義了 App 從啟動到退出的全過程。在 Flutter 中,我們可以利用WidgetsBindingObserver類,來實現(xiàn)對APP生命周期的監(jiān)聽。
abstract class WidgetsBindingObserver {
// 頁面 pop
Future<bool> didPopRoute() => Future<bool>.value(false);
// 頁面 push
Future<bool> didPushRoute(String route) => Future<bool>.value(false);
// 系統(tǒng)窗口相關(guān)改變回調(diào),如旋轉(zhuǎn)
void didChangeMetrics() { }
// 文本縮放系數(shù)變化
void didChangeTextScaleFactor() { }
// 系統(tǒng)亮度變化
void didChangePlatformBrightness() { }
// 本地化語言變化
void didChangeLocales(List<Locale> locale) { }
//App 生命周期變化
void didChangeAppLifecycleState(AppLifecycleState state) { }
// 內(nèi)存警告回調(diào)
void didHaveMemoryPressure() { }
//Accessibility 相關(guān)特性回調(diào)
void didChangeAccessibilityFeatures() {}
}
常見的屏幕旋轉(zhuǎn)、屏幕亮度、語言變化、內(nèi)存警告都可以通過這個實現(xiàn)進行回調(diào)。我們通過給 WidgetsBinding 的單例對象設(shè)置監(jiān)聽器,就可以監(jiān)聽對應(yīng)的回調(diào)方法。
didChangeAppLifecycleState 回調(diào)函數(shù)中,有一個參數(shù)類型為 AppLifecycleState 的枚舉類,這個枚舉類是 Flutter 對 App 生命周期狀態(tài)的封裝。它的常用狀態(tài)包括 resumed、inactive、paused 這三個。
resumed:可見的,并能響應(yīng)用戶的輸入。
inactive:處在不活動狀態(tài),無法處理用戶響應(yīng)。
paused:不可見并不能響應(yīng)用戶的輸入,但是在后臺繼續(xù)活動中。

幀繪制回調(diào):
WidgetsBinding 提供了單次 Frame 繪制回調(diào),以及實時 Frame 繪制回調(diào)兩種機制:
- 單次 Frame 繪制回調(diào),通過 addPostFrameCallback 實現(xiàn)。它會在當前 Frame 繪制完成后進行進行回調(diào),并且只會回調(diào)一次,如果要再次監(jiān)聽則需要再設(shè)置一次。
WidgetsBinding.instance.addPostFrameCallback((_){
print(" 單次 Frame 繪制回調(diào) ");// 只回調(diào)一次
});
- 實時 Frame 繪制回調(diào),則通過 addPersistentFrameCallback 實現(xiàn)。這個函數(shù)會在每次繪制 Frame 結(jié)束后進行回調(diào),可以用做 FPS 監(jiān)測。
WidgetsBinding.instance.addPersistentFrameCallback((_){
print(" 實時 Frame 繪制回調(diào) ");// 每幀都回調(diào)
});
