Flutter 視圖渲染與生命周期

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),通過布局、繪制完成界面的展示。

image-20211029143430486.png

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 渲染過程,可以分為三步:

  1. 通過 Widget 樹生成對應(yīng)的 Element 樹;

  2. 創(chuàng)建相應(yīng)的 RenderObject 并關(guān)聯(lián)到 Element.renderObject 屬性上

  3. 構(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 生命周期

image-20211029162054332.png

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ù)活動中。

image-20211029171208782.png

幀繪制回調(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)
});
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容