Flutter 詳細(xì)講解

一.Flutter 的簡介

Flutter 是 Google 推出并開源的移動應(yīng)用開發(fā)框架,主打跨平臺、高保真、高性能。開發(fā)者可以通過 Dart 語言開發(fā) App,一套代碼同時運(yùn)行在 iOS 和 Android等多個平臺。 Flutter 提供了豐富的組件、接口,開發(fā)者可以很快地為 Flutter 添加 Native 擴(kuò)展。下面我們整體介紹一下 Flutter 技術(shù)的主要特點。

1. 跨平臺自繪引擎

Flutter 與用于構(gòu)建移動應(yīng)用程序的其他大多數(shù)框架不同,因為 Flutter 既不使用 WebView,也不使用操作系統(tǒng)的原生控件。 相反,F(xiàn)lutter 使用自己的高性能渲染引擎來繪制 Widget(組件)。這樣不僅可以保證在 Android 和iOS 上 UI 的一致性,也可以避免對原生控件依賴而帶來的限制及高昂的維護(hù)成本。

Flutter 底層使用 Skia 作為其 2D 渲染引擎,Skia 是 Google的一個 2D 圖形處理函數(shù)庫,包含字型、坐標(biāo)轉(zhuǎn)換,以及點陣圖,它們都有高效能且簡潔的表現(xiàn)。Skia 是跨平臺的,并提供了非常友好的 API,目前 Google Chrome瀏覽器和 Android 均采用 Skia 作為其 2D 繪圖引擎。

目前 Flutter 已經(jīng)支持 iOS、Android、Web、Windows、macOS、Linux、Fuchsia(Google新的自研操作系統(tǒng))等眾多平臺,但本書的示例和介紹主要是基于 iOS 和 Android 平臺的,其他平臺讀者可以自行了解。

2. 高性能

Flutter 高性能主要靠兩點來保證:

第一:Flutter APP 采用 Dart 語言開發(fā)。Dart 在 JIT(即時編譯)模式下,執(zhí)行速度與 JavaScript 基本持平。但是 Dart 支持 AOT,當(dāng)以 AOT模式運(yùn)行時,JavaScript 便遠(yuǎn)遠(yuǎn)追不上了。執(zhí)行速度的提升對高幀率下的視圖數(shù)據(jù)計算很有幫助。

第二:Flutter 使用自己的渲染引擎來繪制 UI ,布局?jǐn)?shù)據(jù)等由 Dart 語言直接控制,所以在布局過程中不需要像 RN 那樣要在 JavaScript 和 Native 之間通信,這在一些滑動和拖動的場景下具有明顯優(yōu)勢,因為在滑動和拖動過程往往都會引起布局發(fā)生變化,所以 JavaScript 需要和 Native 之間不停的同步布局信息,這和在瀏覽器中JavaScript 頻繁操作 DOM 所帶來的問題是類似的,都會導(dǎo)致比較可觀的性能開銷。

3. 采用Dart語言開發(fā)

這個是一個很有意思但也很有爭議的問題,在了解 Flutter 為什么選擇了 Dart 而不是 JavaScript 之前我們先來介紹一下之前提到過的兩個概念:JIT 和 AOT。

程序主要有兩種運(yùn)行方式:靜態(tài)編譯與動態(tài)解釋。靜態(tài)編譯的程序在執(zhí)行前程序會被提前編譯為機(jī)器碼(或中間字節(jié)碼),通常將這種類型稱為AOT?(Ahead of time)即 “提前編譯”。而解釋執(zhí)行則是在運(yùn)行時將源碼實時翻譯為機(jī)器碼來執(zhí)行,通常將這種類型稱為JIT(Just-in-time)即“即時編譯”。

AOT 程序的典型代表是用 C/C++ 開發(fā)的應(yīng)用,它們必須在執(zhí)行前編譯成機(jī)器碼;而JIT的代表則非常多,如JavaScript、python等,事實上,所有腳本語言都支持 JIT 模式。但需要注意的是 JIT 和 AOT 指的是程序運(yùn)行方式,和編程語言并非強(qiáng)關(guān)聯(lián)的,有些語言既可以以 JIT 方式運(yùn)行也可以以 AOT 方式運(yùn)行,如Python,它可以在第一次執(zhí)行時編譯成中間字節(jié)碼,然后在之后執(zhí)行時再將字節(jié)碼實施轉(zhuǎn)為機(jī)器碼執(zhí)行。也許有人會說,中間字節(jié)碼并非機(jī)器碼,在程序執(zhí)行時仍然需要動態(tài)將字節(jié)碼轉(zhuǎn)為機(jī)器碼,這不應(yīng)該是 JIT 嗎 ? 是這樣,但通常我們區(qū)分是否為AOT 的標(biāo)準(zhǔn)就是看代碼在執(zhí)行之前是否需要編譯,只要需要編譯,無論其編譯產(chǎn)物是字節(jié)碼還是機(jī)器碼,都屬于AOT。在此,讀者不必糾結(jié)于概念,概念就是為了傳達(dá)精神而發(fā)明的,只要讀者能夠理解其原理即可,得其神忘其形。

現(xiàn)在我們看看 Flutter 為什么選擇 Dart 語言?筆者根據(jù)官方解釋以及自己對 Flutter 的理解總結(jié)了以下幾條(由于其他跨平臺框架都將 JavaScript 作為其開發(fā)語言,所以主要將 Dart 和 JavaScript 做一個對比):

1.開發(fā)效率高

Dart 運(yùn)行時和編譯器支持 Flutter 的兩個關(guān)鍵特性的組合:

基于 JIT 的快速開發(fā)周期:Flutter 在開發(fā)階段采用,采用 JIT 模式,這樣就避免了每次改動都要進(jìn)行編譯,極大的節(jié)省了開發(fā)時間;

基于 AOT 的發(fā)布包: Flutter 在發(fā)布時可以通過 AOT 生成高效的機(jī)器碼以保證應(yīng)用性能。而 JavaScript 則不具有這個能力。

2.高性能

Flutter 旨在提供流暢、高保真的的 UI 體驗。為了實現(xiàn)這一點,F(xiàn)lutter 中需要能夠在每個動畫幀中運(yùn)行大量的代碼。這意味著需要一種既能提供高性能的語言,而不會出現(xiàn)會丟幀的周期性暫停,而 Dart 支持 AOT,在這一點上可以做的比 JavaScript 更好。

3.快速內(nèi)存分配

Flutter 框架使用函數(shù)式流,這使得它在很大程度上依賴于底層的內(nèi)存分配器。因此,擁有一個能夠有效地處理瑣碎任務(wù)的內(nèi)存分配器將顯得十分重要,在缺乏此功能的語言中,F(xiàn)lutter 將無法有效地工作。當(dāng)然 Chrome V8 的 JavaScript 引擎在內(nèi)存分配上也已經(jīng)做的很好,事實上 Dart 開發(fā)團(tuán)隊的很多成員都是來自Chrome 團(tuán)隊的,所以在內(nèi)存分配上 Dart 并不能作為超越 JavaScript 的優(yōu)勢,而對于Flutter來說,它需要這樣的特性,而 Dart 也正好滿足而已。

4.類型安全和空安全

由于 Dart 是類型安全的語言,且 2.12 版本后也支持了空安全特性,所以 Dart 支持靜態(tài)類型檢測,可以在編譯前發(fā)現(xiàn)一些類型的錯誤,并排除潛在問題,這一點對于前端開發(fā)者來說可能會更具有吸引力。與之不同的,JavaScript 是一個弱類型語言,也因此前端社區(qū)出現(xiàn)了很多給 JavaScript 代碼添加靜態(tài)類型檢測的擴(kuò)展語言和工具,如:微軟的 TypeScript 以及Facebook 的 Flow。相比之下,Dart 本身就支持靜態(tài)類型,這是它的一個重要優(yōu)勢。

5.flutter開發(fā)團(tuán)隊與Dart社區(qū)密切合作

看似不起眼,實則舉足輕重。由于有 Dart 團(tuán)隊的積極投入,F(xiàn)lutter 團(tuán)隊可以獲得更多、更方便的支持,正如Flutter 官網(wǎng)所述我們正與 Dart 社區(qū)進(jìn)行密切合作,以改進(jìn) Dart 在 Flutter 中的使用。例如,當(dāng)我們最初采用 Dart 時,該語言并沒有提供生成原生二進(jìn)制文件的工具鏈(這對于實現(xiàn)可預(yù)測的高性能具有很大的幫助),但是現(xiàn)在它實現(xiàn)了,因為 Dart 團(tuán)隊專門為 Flutter 構(gòu)建了它。同樣,Dart VM 之前已經(jīng)針對吞吐量進(jìn)行了優(yōu)化,但團(tuán)隊現(xiàn)在正在優(yōu)化 VM 的延遲時間,這對于 Flutter 的工作負(fù)載更為重要。

二.Flutter框架結(jié)構(gòu)

Flutter框架是一個分層的結(jié)構(gòu),每個層都建立在前一層之上。

簡單來講,F(xiàn)lutter 從上到下可以分為三層:框架層、引擎層和嵌入層,下面我們分別介紹:

1. 框架層

Flutter Framework,即框架層。這是一個純 Dart實現(xiàn)的 SDK,它實現(xiàn)了一套基礎(chǔ)庫,自底向上,我們來簡單介紹一下:

底下兩層(Foundation 和 Animation、Painting、Gestures)在 Google 的一些視頻中被合并為一個dart UI層,對應(yīng)的是Flutter中的dart:ui包,它是 Flutter Engine 暴露的底層UI庫,提供動畫、手勢及繪制能力。

Rendering 層,即渲染層,這一層是一個抽象的布局層,它依賴于 Dart UI 層,渲染層會構(gòu)建一棵由可渲染對象的組成的渲染樹,當(dāng)動態(tài)更新這些對象時,渲染樹會找出變化的部分,然后更新渲染。渲染層可以說是Flutter 框架層中最核心的部分,它除了確定每個渲染對象的位置、大小之外還要進(jìn)行坐標(biāo)變換、繪制(調(diào)用底層 dart:ui )。

Widgets 層是 Flutter 提供的的一套基礎(chǔ)組件庫,在基礎(chǔ)組件庫之上,F(xiàn)lutter 還提供了 Material 和 Cupertino 兩種視覺風(fēng)格的組件庫,它們分別實現(xiàn)了 Material 和 iOS 設(shè)計規(guī)范。

Flutter 框架相對較小,因為一些開發(fā)者可能會使用到的更高層級的功能已經(jīng)被拆分到不同的軟件包中,使用 Dart 和 Flutter 的核心庫實現(xiàn),其中包括平臺插件,例如?camera(opens new window)和?webview(opens new window),以及和平臺無關(guān)的功能,例如?animations(opens new window)。

我們進(jìn)行Flutter 開發(fā)時,大多數(shù)時候都是和 Flutter Framework 打交道。

2. 引擎層

Engine,即引擎層。毫無疑問是 Flutter 的核心, 該層主要是 C++ 實現(xiàn),其中包括了 Skia 引擎、Dart 運(yùn)行時、文字排版引擎等。在代碼調(diào)用?dart:ui庫時,調(diào)用最終會走到引擎層,然后實現(xiàn)真正的繪制和顯示。

Skia是開源的二維圖形庫,提供了適用于多種軟硬件平臺的通用API。其已作為Google Chrome,Chrome OS,Android, Mozilla Firefox, Firefox OS等其他眾多產(chǎn)品的圖形引擎,支持平臺還包括Windows, macOS, iOS,Android,Ubuntu等。

Dart 部分主要包括:Dart Runtime,Garbage Collection(GC),如果是Debug模式的話,還包括JIT(Just In Time)支持。Release和Profile模式下,是AOT(Ahead Of Time)編譯成了原生的arm代碼,并不存在JIT部分。

Text 即文本渲染,其渲染層次如下:衍生自 Minikin的libtxt庫(用于字體選擇,分隔行);HartBuzz用于字形選擇和成型;Skia作為渲染/GPU后端,在Android和Fuchsia上使用FreeType渲染,在iOS上使用CoreGraphics來渲染字體。

3. 嵌入層

Embedder,即嵌入層。Flutter 最終渲染、交互是要依賴其所在平臺的操作系統(tǒng) API,嵌入層主要是將 Flutter 引擎 ”安裝“ 到特定平臺上。嵌入層采用了當(dāng)前平臺的語言編寫,例如 Android 使用的是 Java 和 C++, iOS 和 macOS 使用的是 Objective-C 和 Objective-C++,Windows 和 Linux 使用的是 C++。 Flutter 代碼可以通過嵌入層,以模塊方式集成到現(xiàn)有的應(yīng)用中,也可以作為應(yīng)用的主體。Flutter 本身包含了各個常見平臺的嵌入層,假如以后 Flutter 要支持新的平臺,則需要針對該新的平臺編寫一個嵌入層。

二.Flutter 的安裝和使用

1.安裝流程

Flutter安裝圖解

2.集成

將 Flutter module 集成到 iOS 項目

將 Flutter module 集成到 Android 項目

四.平臺相關(guān)及Stateful 組件的生命周期

1.Widget說明

在 Flutter 中一切皆?組件,而組件又分為?StatefulWidget(有狀態(tài))?和StatelessWidget(無狀態(tài))組件 ,他們之間的區(qū)別是 StatelessWidget 組件發(fā)生變化時必須重新創(chuàng)建新的實例,而 StatefulWidget 組件則可以直接改變當(dāng)前組件的狀態(tài)而無需重新創(chuàng)建新的實例。

生命周期一:createState

classStatefulWidgetDemoextendsStatefulWidget{@override_StatefulWidgetDemoStatecreateState()=>_StatefulWidgetDemoState();}class_StatefulWidgetDemoStateextendsState{@overrideWidgetbuild(BuildContext context){returnContainer();? }}

當(dāng) StatefulWidget 組件插入到組件樹中時?createState?函數(shù)由?Framework?調(diào)用,此函數(shù)在樹中給定的位置為此組件創(chuàng)建?State

createState?函數(shù)執(zhí)行完畢后表示當(dāng)前組件已經(jīng)在組件樹中,此時有一個非常重要的屬性?mounted?被?Framework?設(shè)置為?true。

生命周期二:initState

initState?函數(shù)在組件被插入樹中時被 Framework 調(diào)用(在?createState?之后),此函數(shù)只會被調(diào)用一次,子類通常會重寫此方法,在其中進(jìn)行初始化操作,比如加載網(wǎng)絡(luò)數(shù)據(jù),重寫此方法時一定要調(diào)用?super.initState(),如下:

@override

void initState() {

super.initState();

//初始化...

}

如果此組件需要訂閱通知,比如?ChangeNotifier?或者?Stream,則需要在不同的生命周期內(nèi)正確處理訂閱和取消訂閱通知。

在?initState?中訂閱通知。

在?didUpdateWidget?中,如果需要替換舊組件,則在舊對象中取消訂閱,并在新對象中訂閱通知。

并在?dispose?中取消訂閱。

生命周期三:didChangeDependencies

didChangeDependencies 方法在 initState 之后由 Framework 立即調(diào)用。另外,當(dāng)此?State?對象的依賴項更改時被調(diào)用,比如其所依賴的?InheritedWidget?發(fā)生變化時, Framework 會調(diào)用此方法通知組件發(fā)生變化。

didChangeDependencies 方法調(diào)用后,組件的狀態(tài)變?yōu)?dirty,立即調(diào)用 build 方法。

生命周期四:build

此方法是我們最熟悉的,在方法中創(chuàng)建各種組件,繪制到屏幕上。 Framework會在多種情況下調(diào)用此方法:

調(diào)用?initState?方法后。

調(diào)用?didUpdateWidget?方法后。

收到對?setState?的調(diào)用后。

此?State?對象的依存關(guān)系發(fā)生更改后(例如,依賴的?InheritedWidget?發(fā)生了更改)。

調(diào)用?deactivate?之后,然后將?State?對象重新插入樹的另一個位置。

此方法可以在每一幀中調(diào)用,此方法中應(yīng)該只包含構(gòu)建組件的代碼,不應(yīng)該包含其他額外的功能,尤其是耗時任務(wù)。

InheritedWidget是 Flutter 中非常重要的一個功能型組件,它提供了一種在 widget 樹中從上到下共享數(shù)據(jù)的方式)

生命周期五:didUpdateWidget

當(dāng)組件的?configuration?發(fā)生變化時調(diào)用此函數(shù),當(dāng)父組件使用相同的?runtimeType?和?Widget.key?重新構(gòu)建一個新的組件時,F(xiàn)ramework 將更新此?State?對象的組件屬性以引用新的組件,然后使用先前的組件作為參數(shù)調(diào)用此方法。

生命周期六:deactivate

當(dāng)框架從樹中移除此 State 對象時將會調(diào)用此方法,在某些情況下,框架將重新插入 State 對象到樹的其他位置(例如,如果包含該樹的子樹 State 對象從樹中的一個位置移植到另一位置),框架將會調(diào)用 build 方法來提供 State 對象適應(yīng)其在樹中的新位置。

生命周期七:dispose

當(dāng)框架從樹中永久移除此 State 對象時將會調(diào)用此方法,與?deactivate?的區(qū)別是,deactivate?還可以重新插入到樹中,而?dispose?表示此 State 對象永遠(yuǎn)不會在?build。調(diào)用完?dispose后,mounted?屬性被設(shè)置為 false,也代表組件生命周期的結(jié)束,此時再調(diào)用?setState?方法將會拋出異常。

子類重寫此方法,釋放相關(guān)資源,比如動畫等。

一、生命周期階段

flutter生命周期大體上可以分為三個階段:初始化、狀態(tài)變化、銷毀。

1、初始化階段

對應(yīng)執(zhí)行構(gòu)造方法和initState時候

2、狀態(tài)變化階段

開新的widget或者調(diào)用setState方法的時候

3、銷毀階段

deactivate 和 dispose

二、生命周期階段執(zhí)行的函數(shù)

1、initState

調(diào)用次數(shù):1次

插入渲染樹時調(diào)用,只調(diào)用一次,widget創(chuàng)建執(zhí)行的第一個方法,這里可以做一些初始化工作,比如初始化State的變量。

2、didChangeDependencies

調(diào)用次數(shù):多次

初始化時,在initState()之后立刻調(diào)用

當(dāng)依賴的InheritedWidget rebuild,會觸發(fā)此接口被調(diào)用

實測在組件可見狀態(tài)變化的時候會調(diào)用

3、build

調(diào)用次數(shù):多次

初始化之后開始繪制界面

setState觸發(fā)的時候會

4、didUpdateWidget

調(diào)用次數(shù):多次

組件狀態(tài)改變時候調(diào)用

5、deactivate

當(dāng)State對象從樹中被移除時,會調(diào)用此回調(diào),會在dispose之前調(diào)用。

頁面銷毀的時候會依次執(zhí)行:deactivate > dispose

6、dispose

調(diào)用次數(shù):1次

當(dāng)State對象從樹中被永久移除時調(diào)用;通常在此回調(diào)中釋放資源。

7、reassemble

在熱重載(hot reload)時會被調(diào)用,此回調(diào)在Release模式下永遠(yuǎn)不會被調(diào)用

三、App生命周期

通過WidgetsBindingObserver的didChangeAppLifecycleState 來獲取。通過該接口可以獲取是生命周期在AppLifecycleState類中。

class_MyHomePageStateextendsState<MyHomePage>withWidgetsBindingObserver{// initState 里添加監(jiān)聽@overridevoidinitState(){super.initState();WidgetsBinding.instance.addObserver(this);}//dispose 里移除監(jiān)聽@overridevoiddispose(){// TODO: implement disposesuper.dispose();WidgetsBinding.instance.removeObserver(this);}//重寫didChangeAppLifecycleState@overridevoiddidChangeAppLifecycleState(AppLifecycleState state){super.didChangeAppLifecycleState(state);switch(state){caseAppLifecycleState.resumed:print(AppLifecycleState.resumed);break;caseAppLifecycleState.inactive:print(AppLifecycleState.inactive);break;caseAppLifecycleState.paused:print(AppLifecycleState.paused);break;caseAppLifecycleState.suspending:print(AppLifecycleState.suspending);break;}}}

下面是生命周期:

初次打開widget時,不執(zhí)行AppLifecycleState的回調(diào);

從后臺到前臺:AppLifecycleState inactive--->ApplifecycleState resumed

back鍵退出應(yīng)用: AppLifecycleState inactive--->AppLifecycleState pause

五.Flutter渲染之Widget、Element 和 RenderObject

提出問題

createState 方法在什么時候調(diào)用?state 里面為啥可以直接獲取到 widget 對象?

build 方法是在什么時候調(diào)用的?

BuildContext 是什么?

Widget 頻繁更改創(chuàng)建是否會影響性能?復(fù)用和更新機(jī)制是什么樣的?

要理解Flutter的渲染原理,那么就必須了解Widget、RenderObject 和 Element及其作用。總的來說,F(xiàn)lutter調(diào)用runApp(rootWidget),將rootWidget傳給rootElement,做為rootElement的子節(jié)點,生成Element樹,由Element樹生成Render樹,如下圖所示。


從上面的介紹中,我們隱約知道了Widget、RenderObject 和 Element的作用,簡單的介紹一下。 -?Widget:Widget 的主要作用是用來保存 Element 信息的(包括布局、渲染屬性、事件響應(yīng)等信息),本身是不可變的,Element 也是根據(jù) Widget 里面保存的配置信息來管理渲染樹,以及決定自身是否需要執(zhí)行渲染。 -?RenderObject:RenderObject 做為渲染樹中的對象存在,主要作用是處理布局、繪制相關(guān)的事情,而繪制的內(nèi)容是Widget傳入的內(nèi)容。 -?Element:Element 可以理解為是其關(guān)聯(lián)的 Widget 的實例,存放視圖構(gòu)建的上下文數(shù)據(jù),可以通過遍歷Element來查看視圖樹,Element同時持有Widget和RenderObject對象。

Flutter通過Widget樹中的每個控件創(chuàng)建不同類型的渲染對象,組成渲染對象樹,而渲染對象樹在Flutter中的展示分為四階段:布局、繪制、合成及渲染。其中,布局和繪制由RenderObject負(fù)責(zé)完成,F(xiàn)lutter采用深度優(yōu)先機(jī)制遍歷渲染樹對象,確定樹中每個對象的位置和尺寸,并把他們繪制到不同的圖層上,而合成及渲染則交給Skia完成。

三棵樹

首先先了解三棵樹,這是我們的核心,需要首先建立一個概念。

Widget 樹

我們平時用 Widget 使用聲明式的形式寫出來的界面,可以理解為 Widget 樹,這是要介紹的第一棵樹。

RenderObject 樹

Flutter 引擎需要把我們寫的 Widget 樹的信息都渲染到界面上,這樣人眼才能看到,跟渲染有關(guān)的當(dāng)然有一顆渲染樹 RenderObject tree,這是第二顆樹,渲染樹節(jié)點叫做 RenderObject,這個節(jié)點里面處理布局、繪制相關(guān)的事情。這兩個樹的節(jié)點并不是一一對應(yīng)的關(guān)系,有些 Widget是要顯示的,有些 Widget ,比如那些繼承自 StatelessWidget & StatefulWidget 的 Widget 只是將其他 Widget 做一個組合,這些 Widget 本身并不需要顯示,因此在 RenderObject 樹上并沒有相對應(yīng)的節(jié)點。

Element 樹

Widget 樹是非常不穩(wěn)定的,動不動就執(zhí)行 build方法,一旦調(diào)用 build 方法意味著這個 Widget 依賴的所有其他 Widget 都會重新創(chuàng)建,如果 Flutter 直接解析 Widget樹,將其轉(zhuǎn)化為 RenderObject 樹來直接進(jìn)行渲染,那么將會是一個非常消耗性能的過程,那對應(yīng)的肯定有一個東西來消化這些變化中的不便,來做cache。因此,這里就有另外一棵樹 Element 樹。Element 樹這一層將 Widget 樹的變化(類似 React 虛擬 DOM diff)做了抽象,可以只將真正需要修改的部分同步到真實的 RenderObject 樹中,最大程度降低對真實渲染視圖的修改,提高渲染效率,而不是銷毀整個渲染視圖樹重建。

這三棵樹如下圖所示,是我們討論的核心內(nèi)容。


從上圖可以看出,widget 樹和 Element 樹節(jié)點是一一對應(yīng)關(guān)系,每一個 Widget 都會有其對應(yīng)的 Element,但是 RenderObject 樹則不然,只有需要渲染的 Widget 才會有對應(yīng)的節(jié)點。Element 樹相當(dāng)于一個中間層,大管家,它對 Widget 和 RenderObject 都有引用。當(dāng) Widget 不斷變化的時候,將新 Widget 拿到 Element 來進(jìn)行對比,看一下和之前保留的 Widget 類型和 Key 是否相同,如果都一樣,那完全沒有必要重新創(chuàng)建 Element 和 RenderObject,只需要更新里面的一些屬性即可,這樣可以以最小的開銷更新 RenderObject,引擎在解析 RenderObject 的時候,發(fā)現(xiàn)只有屬性修改了,那么也可以以最小的開銷來做渲染。

以上只是引出了非常重要的三棵樹和他們之間的關(guān)系,簡而言之,Widget 樹就是配置信息的樹,我們平時寫代碼寫的就是這棵樹,RenderObject 樹是渲染樹,負(fù)責(zé)計算布局,繪制,F(xiàn)lutter 引擎就是根據(jù)這棵樹來進(jìn)行渲染的,Element 樹作為中間者,管理著將 Widget 生成 RenderObject和一些更新操作。

從源碼來了解 Widget、Element 和 RenderObject

Widget

下面對 Widget 的概述截圖來自官網(wǎng)

Widget 是描述 Element 的配置信息,是 Flutter 框架里的核心類層次結(jié)構(gòu),一個 Widget 是用戶界面某一部分的不可變描述。Widgets 可以轉(zhuǎn)為 Elements,Elements 管理著底層的渲染樹。

有這么多 Widget,我們來簡單分各類吧,前面已經(jīng)提到 Widget 有可渲染和不可渲染的分別了??射秩纠锩娣譃?child 或 children,在不可渲染的 Widgets 里面又分為有狀態(tài)和無狀態(tài),也就是 StatefullWidget 和 StatelessWidget。我們選擇四個典型的Widgets來看看吧,如 Padding、RichText、Container、TextField。通過查閱源碼,我們看到這幾個類的繼承關(guān)系如下圖所示。


來到Widget 類里面可以看到有以下方法

@protectedElementcreateElement();

Widget 是個抽象類,所有的 Widgets 都是它的子類,其抽象方法 createElement 需要子類實現(xiàn),這里體現(xiàn)了之前我們說的 Widget 和 Element 的一一對應(yīng)關(guān)系。來到 StatelessWidget、StatefulWidget、MultiChildRenderObjectWidget、SingleChildRenderObjectWidget 里面我們可以找到 createElement 的實現(xiàn)。

SingleChildRenderObjectWidget

@overrideSingleChildRenderObjectElementcreateElement()=>SingleChildRenderObjectElement(this);

MultiChildRenderObjectWidget

@overrideMultiChildRenderObjectElementcreateElement()=>MultiChildRenderObjectElement(this);

StatefulWidget

@overrideStatefulElementcreateElement()=>StatefulElement(this);

StatelessWidget

@overrideStatelessElementcreateElement()=>StatelessElement(this);

可以發(fā)現(xiàn)規(guī)律,創(chuàng)建 Element 都會傳入 this,也就是當(dāng)前 Widget,然后返回對應(yīng)的 Element,這些 Element 都是繼承自 Element,Element 會有引用指向當(dāng)前 Widget。

我們繼續(xù)來到 RichText 和 Padding 類定義里面,他們都是繼承自 RenderObjectWidget,可以看到他們都有 createRenderObject 方法,如下

Padding

@overrideRenderPaddingcreateRenderObject(BuildContextcontext){returnRenderPadding(padding:padding,textDirection:Directionality.of(context),);}

RichText

@overrideRenderParagraphcreateRenderObject(BuildContextcontext){assert(textDirection!=null||debugCheckHasDirectionality(context));returnRenderParagraph(text,textAlign:textAlign,textDirection:textDirection??Directionality.of(context),softWrap:softWrap,overflow:overflow,textScaleFactor:textScaleFactor,maxLines:maxLines,strutStyle:strutStyle,textWidthBasis:textWidthBasis,locale:locale??Localizations.localeOf(context,nullOk:true),);}

RenderPadding 和 RenderParagraph 最終都是繼承自 RenderObject。通過以上源碼分析,我們可以看出來 Widget 里面有生成 Element 和 RenderObject 的方法,所以我們平時只需要埋頭寫好 Widget 就行,F(xiàn)lutter 框架會幫我們生成對應(yīng)的 Element 和 RenderObject。

Element

以下對 Element 描述來自官網(wǎng)

直接翻譯過來就是,Element 是 樹中特定位置 Widget 的一個實例化對象。這句話有兩層意思:1. 表示 Widget 是一個配置,Element 才是最終的對象;2. Element 是通過遍歷 Widget 樹時,調(diào)用 Widget 的方法創(chuàng)建的。Element 承載了視圖構(gòu)建的上下文數(shù)據(jù),是連接結(jié)構(gòu)化的配置信息到完成最終渲染的橋梁。

上面從源碼里面介紹 Widget 都會生成對應(yīng)的 Element,這里我們也對 Element 簡單做一個分類,和 Widget 相對應(yīng),如下圖所示。



首先還是進(jìn)入 Element 類里面看看,這是個抽象類,可以看到一些關(guān)鍵的方法和屬性。

/// Typically called by an override of [Widget.createElement].Element(Widget widget):assert(widget!=null),_widget=widget;

上面介紹Widget 里面 createElement 方法的時候可以看到會傳入 this,這里從 Element 的構(gòu)造方法中可以看到,this 最后傳給了 Element 里面的 _widget。也就是說每個 Element 里面都會有一個 Widget 的引用。_widget 在 Element 里面定義如下

/// The configuration for this element.@overrideWidgetgetwidget=>_widget;Widget _widget;

從源碼里面知道 Element 里面的 widget 是一個 get 方法,直接返回 _widget。從上面的注釋信息也再一次提到 Widget 和 Element 的關(guān)系,Widget 是 Element 的配置。

對于 Element 的構(gòu)造方法,StatelessfulElement 有一些特殊的地方,如下

classStatefulElementextendsComponentElement{/// Creates an element that uses the given widget as its configuration.StatefulElement(StatefulWidgetwidget):_state=widget.createState(),super(widget){...省略斷言...assert(_state._element==null);_state._element=this;...省略斷言..._state._widget=widget;assert(_state._debugLifecycleState==_StateLifecycle.created);}/// The [State] instance associated with this location in the tree.////// There is a one-to-one relationship between [State] objects and the/// [StatefulElement] objects that hold them. The [State] objects are created/// by [StatefulElement] in [mount].State<StatefulWidget>get state=>_state;State<StatefulWidget>_state;}

StatefulElement 的構(gòu)造方法中還調(diào)用了對應(yīng) Widget 的 createState 方法,并賦值給 _state,這也解答了我們在文章開頭提出的問題(createState 方法在什么時候調(diào)用?)。StatefulElement 里面不僅有對 Widget 的引用,也有對 StatefulWidget 的 State 的引用。并且在構(gòu)造函數(shù)里面還將 widget 賦值給了 _state 里面的 _widget。所以我們在 State 里面可以直接使用 widget 就可以拿到 State 對應(yīng)的 Widget。原來是在 StatefulElement 構(gòu)造函數(shù)的時候賦值的。解釋了開頭提到的問題(state 里面為啥可以直接獲取到 widget 對象?)。

Element 還有一個關(guān)鍵的方法 mount,如下

@mustCallSupervoidmount(Element parent,dynamicnewSlot){...省略斷言..._parent=parent;_slot=newSlot;_depth=_parent!=null?_parent.depth+1:1;_active=true;if(parent!=null)// Only assign ownership if the parent is non-null_owner=parent.owner;if(widget.keyisGlobalKey){finalGlobalKey key=widget.key;key._register(this);}_updateInheritance();...省略斷言...}

Flutter 框架會根據(jù) Widget 創(chuàng)建對應(yīng)的 Element,Element 生成以后會調(diào)用 Element 的 mount 方法,將生成的 Element 掛載到 Element 樹上。這里的 createElement 和 mount 都是 Flutter 框架自動調(diào)用的,不需要開發(fā)者手動調(diào)用。

這里一步一步看源碼,發(fā)現(xiàn)執(zhí)行鏈路如下:

_firstBuild()【ComponentElement】 -> rebuild() 【Element】-> performRebuild()【ComponentElement】 -> build()【StatelessElement】

StatelessElement build() 的源碼

@overrideStatelessWidgetgetwidget=>super.widget;@overrideWidgetbuild()=>widget.build(this);

StatefulElement 的 build() 的源碼如下

@overrideWidgetbuild()=>state.build(this);

可以看出ComponentElement 的 mount 最后執(zhí)行的是 build 方法。不過 StatelessElement 和 StatefulElement 是有區(qū)別的,StatelessElement 執(zhí)行的是 Widget 里的 build 方法,而 StatefulElement 里面執(zhí)行的是 state 的 build 方法。因此,這里也解決了文章開始提到的一個問題(build 方法是在什么時候調(diào)用的?)。也知道了 StatefulWidget 和 它的 State 是如何聯(lián)系起來的。

另外,我們看到上面執(zhí)行執(zhí)行build 方法傳遞的參數(shù) this,也就是當(dāng)前 Element,而我們在寫代碼的時候 build 方法是這樣的

@overrideWidgetbuild(BuildContextcontext){}

因此我們知道了,這個 BuildContext 其實就是這個 Widget 所對應(yīng)的 Element??纯?Element 的定義就更清楚了。這也解釋了開始提到的問題(BuildContext 是什么?)。

abstractclassElementextendsDiagnosticableTreeimplementsBuildContext{}

再來看看 RenderObjectElement 里的 mount 方法

@overridevoidmount(Elementparent,dynamicnewSlot){...省略斷言..._renderObject=widget.createRenderObject(this);...省略斷言...attachRenderObject(newSlot);_dirty=false;}

對比一下 ComponentElement 和 RenderObjectElement 里面的 mount 方法,前面介紹過,ComponentElement 是非渲染 Widget 對應(yīng)的 Element,而 RenderObjectElement 是渲染 Widget 對應(yīng)的 Element,前者的mount 方法主要是負(fù)責(zé)執(zhí)行 build 方法,而后者的 mount 方法主要是調(diào)用 Widget 里面的 createRenderObject 方法生成 RenderObject,然后賦值給自己的 _renderObject。

因此可以總結(jié),ComponentElement 的 mount 方法主要作用是執(zhí)行 build,而 RenderObjectElement 的 mount 方法主要作用是生成 RenderObject。

Widget 類里面有一個很重要的靜態(tài)方法,本來可以放到上面講 Widget 的時候說,但是還是放到 Element 里面吧。就是這個

/// Whether the `newWidget` can be used to update an [Element] that currently/// has the `oldWidget` as its configuration.////// An element that uses a given widget as its configuration can be updated to/// use another widget as its configuration if, and only if, the two widgets/// have [runtimeType] and [key] properties that are [operator==].////// If the widgets have no key (their key is null), then they are considered a/// match if they have the same type, even if their children are completely/// different.staticboolcanUpdate(Widget oldWidget,Widget newWidget){returnoldWidget.runtimeType==newWidget.runtimeType&&oldWidget.key==newWidget.key;}

Element 里面有一個 _widget 作為其配置信息,當(dāng)widget變化或重新生成以后,Element 要不要銷毀重建呢,還是直接將新生成的 Widget 替換舊的 Widget。答案就是通過這個方法判斷的,上面的注釋可以翻譯如下

判斷新 Widget 是否可以用來取代 Element 當(dāng)前的配置信息 _widget。

Element 使用特定的 widget 作為其配置信息,如果 runtimeType 和 key 和之前的 widget 相同,那么可以使用一個新的 widget 更新 Element 里面舊的 widget。

如果這兩個widget 都沒有賦值 key,那么只要 runtimeType 相同也可以更新,即使這兩個 widget 的孩子 widget 都完全不一樣。

因此可以看出,即使外面的 widget 樹經(jīng)常變換重建,我們的 Element 可以維持相對穩(wěn)定,不會重復(fù)創(chuàng)建,當(dāng)然也就不會重復(fù) mount, 生成 RenderObject,只需要以最小代價更新相關(guān)屬性即可,最大可能減小了性能消耗。Widget 本身只是一些配置信息,簡單的對象,它的變更重建不直接影響渲染,對性能影響很小。這就解決了上文提到的另外一個問題(Widget 頻繁更改創(chuàng)建是否會影響性能?復(fù)用和更新機(jī)制是什么樣的?)。

RenderObject

從 RenderObject 的名字,我們就能很直觀地知道,RendreObject 是主要負(fù)責(zé)實現(xiàn)視圖渲染的對象。從上文中我們知道了一下幾點

RenderObject 和 widget 并不是一一對應(yīng)的,只有繼承自 RenderObjectWidget 的 widget 才有對應(yīng)的 RenderObject;

生成 RenderObject 的方法 createRenderObject 是在 Widget 里面定義的;

在 RenderObjectElement 執(zhí)行 mount 方法的時候調(diào)用的 widget 里面的 createRenderObject 方法的;

RenderObjectElement 里面既有對 Widget 的引用也有對 RenderObject 的引用,它作為中間者,管理著雙方。

RenderObject 在 Flutter 的展示分為四個階段,即布局、繪制、合成和渲染。其中,布局和繪制在 RenderObject 中完成,F(xiàn)lutter 采用深度優(yōu)先機(jī)制遍歷渲染對象樹,確定樹中各個對象的位置和尺寸,并把它們繪制在不同的圖層上。繪制完畢后,合成和渲染的工作則交給 Skia 搞定。

總結(jié)

上面通過源碼講解了一下 Widget、Element、RenderObject 的聯(lián)系。下面簡單來個總結(jié)。

我們寫好 Widget 樹后,F(xiàn)lutter 會在遍歷 Widget 樹時調(diào)用 Widget 里面的 createElement 方法去生成對應(yīng)節(jié)點的 Element 對象,同時 Element 里面也有了對 Widget 的引用。特別的是當(dāng) StatefulElement 創(chuàng)建的時候也執(zhí)行 StatefulWidget 里面的 createState 方法創(chuàng)建 state,并且賦值給 Element 里的 _state 屬性,當(dāng)前 widget 也同時賦值給了 state 里的_widget。Element 創(chuàng)建好以后 Flutter 框架會執(zhí)行 mount 方法,對于非渲染的 ComponentElement 來說 mount 主要執(zhí)行 widget 里的 build 方法,而對于渲染的 RenderObjectElement 來說 mount 里面會調(diào)用 widget 里面的 createRenderObject 方法 生成 RenderObject,并賦值給 RenderObjectElement 里的相應(yīng)屬性。StatefulElement 執(zhí)行 build 方法的時候是執(zhí)行的 state 里面的 build 方法,并且將自身傳入,也就是 常見的 BuildContext。

如果 Widget 的配置數(shù)據(jù)發(fā)生了改變,那么持有該 Widget 的 Element 節(jié)點也會被標(biāo)記為 dirty。在下一個周期的繪制時,F(xiàn)lutter 就會觸發(fā)該 Element 樹的更新,通過 canUpdate 方法來判斷是否可以使用新的 Widget 來更新 Element 里面的配置,還是重新生成 Element。并使用最新的 Widget 數(shù)據(jù)更新自身以及關(guān)聯(lián)的 RenderObject對象。布局和繪制完成后,接下來的事情交給 Skia 了。在 VSync 信號同步時直接從渲染樹合成 Bitmap,然后提交給 GPU。

回答開頭提出的問題

createState 方法在什么時候調(diào)用?state 里面為啥可以直接獲取到 widget 對象?

答:Flutter 會在遍歷 Widget 樹時調(diào)用 Widget 里面的 createElement 方法去生成對應(yīng)節(jié)點的 Element 對象,同時執(zhí)行 StatefulWidget 里面的 createState 方法創(chuàng)建 state,并且賦值給 Element 里的 _state 屬性,當(dāng)前 widget 也同時賦值給了 state 里的_widget,state 里面有個 widget 的get 方法可以獲取到 _widget 對象。

build 方法是在什么時候調(diào)用的?

答:Element 創(chuàng)建好以后 Flutter 框架會執(zhí)行 mount 方法,對于非渲染的 ComponentElement 來說 mount 主要執(zhí)行 widget 里的 build 方法,StatefulElement 執(zhí)行 build 方法的時候是執(zhí)行的 state 里面的 build 方法,并且將自身傳入,也就是常見的 BuildContext

BuildContext 是什么?

答:StatefulElement 執(zhí)行 build 方法的時候是執(zhí)行的 state 里面的 build 方法,并且將自身傳入,也就是 常見的 BuildContext。簡而言之 BuidContext 就是 Element。

Widget 頻繁更改創(chuàng)建是否會影響性能?復(fù)用和更新機(jī)制是什么樣的?

答:不會影響性能,widget 只是簡單的配置信息,并不直接涉及布局渲染相關(guān)。Element 層通過判斷新舊 widget 的runtimeType 和 key 是否相同決定是否可以直接更新之前的配置信息,也就是替換之前的 widget,而不必每次都重新創(chuàng)建新的 Element。

?著作權(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)容