了解 HTML 的讀者一定聽(tīng)說(shuō)過(guò) DOM 樹(shù)這個(gè)概念,它由頁(yè)面中每一個(gè)控件組成,這些控件所形成的一種天然的嵌套關(guān)系使其可以表示為 “樹(shù)” 結(jié)構(gòu),我們也可以將這個(gè)概念應(yīng)用在 Flutter 中,例如默認(rèn)的計(jì)數(shù)器應(yīng)用的結(jié)構(gòu)如下圖:
我們也可以看到上圖中每個(gè)控件所形成的樹(shù)結(jié)構(gòu)中隱含了一些關(guān)系,例如在上圖中,我們可以說(shuō) Text 組件是 Column 組件的子組件,Scaffold 是 AppBar 的父組件,這樣的層級(jí)關(guān)系使得每個(gè)控件都清晰的連接到了一起,樹(shù)結(jié)構(gòu)由此而來(lái)。
我們也知道 Container、Text 等組件都屬于 Widget,所以在 Flutter 中我們將這種樹(shù)稱(chēng)為 Widget 樹(shù),也可以叫做控件樹(shù),它就表示了我們?cè)?dart 代碼中所寫(xiě)的控件的結(jié)構(gòu)。
另外,在 Flutter 體系結(jié)構(gòu)中,真正做組件渲染在屏幕上這個(gè)任務(wù)的并非在 控件層(Widget)層,而是在渲染(Rending)層,那么我們?cè)诖a中所寫(xiě)組件又是怎么通過(guò)渲染層顯示的呢?Flutter 中又引入了 Element 樹(shù)和 RenderingObject 樹(shù)兩棵樹(shù)。
Element 是什么,我們可以把它稱(chēng)之為 Widget 另一種抽象。讀者也可以把它看作一個(gè)更為實(shí)際控件,因?yàn)樵谖覀兊氖謾C(jī)屏幕上顯示的控件并非我們?cè)诖a中所寫(xiě)的 Widget,我們?cè)诖a中所使用的像 Container、Text 等這類(lèi)組件和其屬性只不過(guò)是我們想要構(gòu)建的組件的配置信息,當(dāng)我們第一次調(diào)用 build()
方法想要在屏幕上顯示這些組件時(shí),F(xiàn)lutter 會(huì)根據(jù)這些信息生成該 Widget 控件對(duì)應(yīng)的 Element,同樣地,Element 也會(huì)被放到相應(yīng)的 Element 樹(shù)當(dāng)中。在 Flutter 中,一個(gè) Widget 通過(guò)多次復(fù)用可以對(duì)應(yīng)多個(gè) Element 實(shí)例,Element 才是我們真正在屏幕上顯示的元素。
Element 與 Widget 另一個(gè)區(qū)別在于,Widget 天然是不可變的(immutable),它如要更新便需要重建,如果想要把可變狀態(tài)與 Widget 關(guān)聯(lián)起來(lái),可以使用 StatefulWidget,StatefulWidget 通過(guò)使用StatefulWidget.createState 方法創(chuàng)建 State 對(duì)象,并將之?dāng)U充到 Element 以及合并到樹(shù)中;
這里,為了更為深刻的理解以上描述的含義,我們可以舉一個(gè)更為形象的例子。Widget 作為大 Boss,他把近期的戰(zhàn)略部署,即配置信息,寫(xiě)在紙上下發(fā)給經(jīng)理人 Element,Element 看到詳細(xì)的配置信息開(kāi)始真正的開(kāi)起活來(lái)了。我們還需要注意一點(diǎn),大 Boss 隨時(shí)會(huì)改變戰(zhàn)略部署,然后不會(huì)在原有的紙上修改而是重新寫(xiě)下來(lái),這時(shí)經(jīng)理人為了減少工作量需要將新的計(jì)劃與舊的計(jì)劃比較來(lái)作出相應(yīng)的更新措施。這也是 Flutter 框架層做的一大優(yōu)化。下面又來(lái)了,Element 作為經(jīng)理人也很體面,當(dāng)然不會(huì)把活全干完,于是又找了一個(gè) RenderObject 的員工來(lái)幫它做粗重的累活。
RenderObject 在 Flutter 當(dāng)中做組件布局渲染的工作,其為了組件間的渲染搭配及布局約束也有對(duì)應(yīng)的 RenderObject 樹(shù),我們也稱(chēng)之為渲染樹(shù)。
熟悉了 Flutter 中的上述三顆樹(shù),相信讀者會(huì)對(duì)組件的渲染過(guò)程有了一個(gè)清晰的認(rèn)識(shí),這對(duì)我們之后學(xué)習(xí)常用組件有很大的幫助,我們需要用不同的眼光去看待我們所建立的布局和控件,之后我們也會(huì)更加深入的去理解其中更不為人知的奧秘。
組件渲染過(guò)程簡(jiǎn)述
從上文中,我們知道控件樹(shù)中的每個(gè)控件都會(huì)實(shí)現(xiàn)一個(gè) RenderObject 對(duì)象做渲染任務(wù),并將所有的RenderObject 組成渲染樹(shù)。Flutter 渲染組件的過(guò)程如下:
Flutter 的渲染過(guò)程由用戶(hù)的輸入開(kāi)始,當(dāng)接受到用戶(hù)輸入的信號(hào)時(shí),就會(huì)觸發(fā)動(dòng)畫(huà)的進(jìn)度更新,例如我們第一次渲染時(shí)的啟動(dòng)動(dòng)畫(huà),或者我們?cè)跐L動(dòng)手機(jī)屏幕時(shí)單個(gè)列表項(xiàng)復(fù)用時(shí)的移動(dòng)動(dòng)畫(huà)。之后便需要開(kāi)始視圖數(shù)據(jù)的構(gòu)建(build),這一步中 Flutter 創(chuàng)建了前文所描述的三棵視圖樹(shù)。
在這之后,視圖才會(huì)進(jìn)行布局(layout),計(jì)算各個(gè)部分的大小,然后進(jìn)行繪制(paint),生成每個(gè)視圖的視覺(jué)數(shù)據(jù),這部分的任務(wù)主要就是由 RenderObject 所做。這里,F(xiàn)lutter 中的布局過(guò)程可用下圖表示,在上述構(gòu)建完成渲染樹(shù)后,父渲染對(duì)象會(huì)將布局約束信息向下傳遞,子渲染對(duì)象根據(jù)自己的渲染情況返回 Size,Size 數(shù)據(jù)會(huì)向上傳遞,最終父渲染對(duì)象完成布局過(guò)程。
最后一步進(jìn)行“光柵化”(Rasterize),前一步得到合成的視圖數(shù)據(jù)其實(shí)還是一份矢量描述數(shù)據(jù),光柵化幫助把這份數(shù)據(jù)真正地生成一個(gè)一個(gè)的像素填充數(shù)據(jù)。在 Flutter 中,光柵化這個(gè)步驟被放在了 Engine 層中。
在日常開(kāi)發(fā)學(xué)習(xí)中,我們只需要在代碼層配置好我們的 Widget 樹(shù),了解各種 Widget 特性及使用方法,其余的工作都可以交給我們的框架層去實(shí)現(xiàn)。
元素樹(shù)詳解
我們已經(jīng)知道了各類(lèi)控件的作用及其使用方法,這些 Widget 被我們開(kāi)發(fā)人員配置了多個(gè)屬性來(lái)定義它的展現(xiàn)形式,例如配置 Text 組件需要顯示的字符串,配置輸入框組件需要顯示的內(nèi)容。我們 Element 樹(shù)會(huì)記錄這些配置信息。熟悉 React 的讀者可能了解過(guò)其中的 “虛擬 DOM” 這個(gè)概念,上述 Flutter 這種操作也正體現(xiàn)了這一概念。Widget 是不可變,它的改變就意味著要重建,而其重建也非常頻繁,如果我們將更多的任務(wù)都交給它將會(huì)對(duì)性能造成很大的損傷,因此我們把 Widget 組件當(dāng)作一個(gè)虛擬的組件樹(shù),而真正被渲染在屏幕上的其實(shí)是 Elememt 這棵樹(shù),它持有其對(duì)應(yīng) Widget 的引用,如果他對(duì)應(yīng)的 Widget 發(fā)生改變,它就會(huì)被標(biāo)記為 dirty Element,于是下一次更新視圖時(shí)根據(jù)這個(gè)狀態(tài)只更新被修改的內(nèi)容,從而達(dá)到提升性能的效果。
每次,當(dāng)控件掛載到控件樹(shù)上時(shí),F(xiàn)lutter 調(diào)用其 createElement() 方法,創(chuàng)建其對(duì)應(yīng)的 Element。Flutter 再將這個(gè) Element 放到元素樹(shù)上,并持有創(chuàng)建它控件的引用,如下圖:
控件會(huì)有它的子樹(shù):
子控件也會(huì)創(chuàng)建相應(yīng) Element 被放在元素樹(shù)上:
Element 中的狀態(tài)
我們上文提到了 Widget 的不可變性,相應(yīng)的 Element 就有其可變性,正如我們前文所說(shuō)的它被標(biāo)記為 dirty Element 便是作為需要更新的狀態(tài),另外一個(gè)我們需要格外注意的是,有狀態(tài)組件(statefulWidget)對(duì)應(yīng)的 State 對(duì)象其實(shí)也被 Element 所管理,如下圖所示。
Flutter 中的 Widget 一直在重建,每次重建之后,Element 都會(huì)采用相應(yīng)的措施來(lái)確定是否我對(duì)應(yīng)的新控件跟之前引用舊控件是否有所改變,如果沒(méi)改變則只需要做更新操作,如果前后不同則會(huì)重創(chuàng)建。那么,Element 根據(jù)什么來(lái)確定控件是否改變呢?它會(huì)比較 Widget 以下兩個(gè)屬性:
- 組件類(lèi)型
- Widget 的 Key (如果有)
組件類(lèi)型即前后控件的是否是同一個(gè)類(lèi)所創(chuàng)建的,Key 即為每個(gè)控件的唯一標(biāo)識(shí)。
渲染樹(shù)詳解
我們已經(jīng)大致知道 Flutter 中的三棵重要的樹(shù)及 Element 樹(shù)的工作原理,其中第三棵渲染樹(shù)的任務(wù)就是做組件的具體的布局渲染工作。
渲染樹(shù)上每個(gè)節(jié)點(diǎn)都是一個(gè)繼承自 RenderObject 類(lèi)的對(duì)象,其由 Element 中的 renderObject 或 RenderObjectWidget 中的 createRenderObject 方法生成,該對(duì)象內(nèi)部提供多個(gè)屬性及方法來(lái)幫助框架層中的組件如何布局渲染。
我們知道 StatelessWidget 和 StatefulWidget 兩種直接繼承自 Widget 的類(lèi),在 Flutter 中,還有另一個(gè)類(lèi) RenderObjectWidget 也同樣直接繼承自 Widget,它沒(méi)有 build 方法,可通過(guò) createRenderObject 直接創(chuàng)建 RenderObject 對(duì)象放入渲染樹(shù)中。Column 和 Row 等控件都間接繼承自RenderObjectWidget。
主要屬性和方法如下:
- constraints 對(duì)象,從其父級(jí)傳遞給它的約束
- parentData 對(duì)象,其父對(duì)象附加有用的信息。
- performLayout 方法,計(jì)算此渲染對(duì)象的布局。
- paint 方法,繪制該組件及其子組件。
RenderObject 作為一個(gè)抽象類(lèi)。每個(gè)節(jié)點(diǎn)需要實(shí)現(xiàn)它才能進(jìn)行實(shí)際渲染。擴(kuò)展 RenderOject 的兩個(gè)最重要的類(lèi)是RenderBox 和 RenderSliver。這兩個(gè)類(lèi)分別是應(yīng)用了 Box 協(xié)議和 Sliver 協(xié)議這兩種布局協(xié)議的所有渲染對(duì)象的父類(lèi),其還擴(kuò)展了數(shù)十個(gè)和其他幾個(gè)處理特定場(chǎng)景的類(lèi),并實(shí)現(xiàn)了渲染過(guò)程的細(xì)節(jié),如 RenderShiftedBox 和 RenderStack 等等。
布局約束
在上面,我們介紹組件渲染流程時(shí),我們了解到了 Flutter 中的控件在屏幕上繪制渲染之前需要先進(jìn)行布局(layout)操作。其具體可分為兩個(gè)線性過(guò)程:從頂部向下傳遞約束,從底部向上傳遞布局信息,其過(guò)程可用下圖表示。
第一個(gè)線性過(guò)程用于傳遞布局約束。父節(jié)點(diǎn)給每個(gè)子節(jié)點(diǎn)傳遞約束,這些約束是每個(gè)子節(jié)點(diǎn)在布局階段必須要遵守的規(guī)則。就好像父母告訴自己的孩子 :“你必須遵守學(xué)校的規(guī)定,才可以做其他的事”。常見(jiàn)的約束包括規(guī)定子節(jié)點(diǎn)最大最小寬度或者子節(jié)點(diǎn)最大最小的高度。這種約束會(huì)向下延伸,子組件也會(huì)產(chǎn)生約束傳遞給自己的孩子,一直到葉子結(jié)點(diǎn)。
第二的線性過(guò)程用來(lái)傳遞具體的布局信息。子節(jié)點(diǎn)接受到來(lái)自父節(jié)點(diǎn)的約束后,會(huì)依據(jù)它產(chǎn)生自己具體的布局信息,如父節(jié)點(diǎn)規(guī)定我的最小寬度是 500 的單位像素,子節(jié)點(diǎn)按照這個(gè)規(guī)則可能定義自己的寬度為 500 個(gè)像素,或者低于 500 像素的任何一個(gè)值。這樣,確定好自己的布局信息之后,將這些信息告訴父節(jié)點(diǎn)。父節(jié)點(diǎn)也會(huì)繼續(xù)此操作向上傳遞一直到最頂部。
下面我們具體介紹有哪些具體的布局約束可在樹(shù)中傳遞。Flutter 中有兩種主要的布局協(xié)議:Box 盒子協(xié)議和 Sliver 滑動(dòng)協(xié)議。這里我們先以盒子協(xié)議為例展開(kāi)具體的介紹。
在盒子協(xié)議中,父節(jié)點(diǎn)傳遞給其子節(jié)點(diǎn)的約束為 BoxConstraints。該約束規(guī)定了允許每個(gè)子節(jié)點(diǎn)的最大和最小寬度和高度。如下圖,父節(jié)點(diǎn)傳入 Min Width 為 150,Max Width 為 300 的 BoxConstraints:
當(dāng)子節(jié)點(diǎn)接受到該約束,便可以取得上圖中綠色范圍內(nèi)的值,即寬度在 150 到 300 之間,高度大于 100,當(dāng)取得具體的值之后再將取得具體的大小的值上傳給父節(jié)點(diǎn),從而達(dá)到父子的布局通信。
自定義一個(gè) Center 控件
之后更新,大家也可以看各組件的源碼探究其如何應(yīng)用上面提到的原理。
---2019.07.03 更新
現(xiàn)在,我們可以應(yīng)用前文中提到的布局約束與渲染樹(shù)相關(guān)的概念自己定義一個(gè)類(lèi)似居中布局的組件 RenderObject 對(duì)象渲染在屏幕上。
我們稱(chēng)自己自定義的組件為 CustomCenter:
void main() {
runApp(MaterialApp(
home: Scaffold(
body: Container(
color: Colors.blue,
constraints: BoxConstraints(
maxWidth: double.infinity,
minWidth: 100.0,
maxHeight: double.infinity,
minHeight: 100.0),
child: CustomCenter(
child: Container(
color: Colors.red,
),
),
),
),
));
}
現(xiàn)在我們來(lái)實(shí)現(xiàn)我們的 CustomCenter:
class CustomCenter extends SingleChildRenderObjectWidget {
Stingy({Widget child}) : super(child: child);
@override
RenderObject createRenderObject(BuildContext context) {
// TODO: implement createRenderObject
return RenderCustomCenter();
}
}
CustomCenter
繼承了 SingleChildRenderObjectWidget
,表明這個(gè) Widget 只能有一個(gè)子控件, 其中,createRenderObject(...)
方法用于真正創(chuàng)建并返回我們的 RenderObject
對(duì)象實(shí)例, 我們的 RenderObject 為 RenderCustomCenter
,代碼如下:
class RenderCustomCenter extends RenderShiftedBox {
RenderStingy() : super(null);
// 重寫(xiě)繪制方法
@override
void paint(PaintingContext context, Offset offset) {
// TODO: implement paint
super.paint(context, offset);
}
// 重寫(xiě)布局方法
@override
void performLayout() {
// 布局子元素并向下傳遞布局約束
child.layout(
BoxConstraints(
minHeight: 0.0,
maxHeight: constraints.minHeight,
minWidth: 0.0,
maxWidth: constraints.minWidth),
parentUsesSize: true);
print('constraints: $constraints');
// 指定子元素的偏移位置
final BoxParentData childParentData = child.parentData;
childParentData.offset = Offset((constraints.maxWidth - child.size.width)/2,
(constraints.maxHeight - child.size.height)/2);
print('childParentData: $childParentData');
// 定義自己(CustomCenter)的大小,這里選擇約束對(duì)象的最大值
size = Size(constraints.maxWidth, constraints.maxHeight);
print('size: $size');
}
}
RenderCustomCenter
繼承自 RenderShiftedBox
,該類(lèi)是繼承自 RenderBox
。RenderShiftedBox
滿(mǎn)足盒子協(xié)議,并且提供了 performLayout()
方法的實(shí)現(xiàn)。我們需要在 performLayout()
方法中布局我們的子元素。
我們?cè)谑褂?child.layout(...)
方法布局 child 的時(shí)候傳遞了兩個(gè)參數(shù),第一個(gè)為 child 的布局約束,而另外一個(gè)參數(shù)是 parentUserSize
, 該參數(shù)如果設(shè)置為 false
,則意味著 parent 不關(guān)心 child 選擇的大小,這對(duì)布局優(yōu)化比較有用;因?yàn)槿绻?child 改變了自己的大小,parent 就不必重新 layout
了。但是在我們的例子中,我們的需要把 child 放置在 parent 的中心,就是 child 的大小(Size)一旦改變,則其對(duì)應(yīng)的偏移量(Offset) 也會(huì)改變,于是 parent 需要重新布局,所以我們這里傳遞了一個(gè) true
。
當(dāng) child.layout(...)
完成了以后,child 就確定了自己的 Layout Details。然后我們就還可以為其設(shè)置偏移量來(lái)將它放置到我們想放的位置。在我們的例子中為 居中。
最后,和 child 根據(jù) parent 傳遞過(guò)來(lái)的約束選擇了一個(gè)尺寸一樣,我們也需要為 CustomCenter 選擇一個(gè)尺寸。
運(yùn)行效果如下:
---2019.07.03 更新結(jié)束
應(yīng)用視圖的構(gòu)建
Flutter App 入口的部分發(fā)生于如下代碼:
import 'package:flutter/material.dart';
// 這里的 MyApp是一個(gè) Widget
void main() => runApp(new MyApp());
runApp
函數(shù)接受一個(gè) Widget類(lèi)型的對(duì)象作為參數(shù),也就是說(shuō)在 Flutter的概念中,只存在 View,而其他的任何邏輯都只為 View的數(shù)據(jù)、狀態(tài)改變服務(wù),不存在 ViewController(或者叫 Activity)。
接下來(lái)看 runApp
做了什么:
void runApp(Widget app) {
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)
..scheduleWarmUpFrame();
}
class WidgetsFlutterBinding extends BindingBase with GestureBinding, ServicesBinding, SchedulerBinding, PaintingBinding, RendererBinding, WidgetsBinding {
static WidgetsBinding ensureInitialized() {
if (WidgetsBinding.instance == null)
new WidgetsFlutterBinding();
return WidgetsBinding.instance;
}
}
在 runApp
中,傳入的 widget 被掛載到根 widget 上。這個(gè) WidgetsFlutterBinding
其實(shí)是一個(gè)單例,通過(guò) mixin 來(lái)使用框架中實(shí)現(xiàn)的其他 binding 的 Service,比如手勢(shì)、基礎(chǔ)服務(wù)、隊(duì)列、繪圖等等。然后會(huì)調(diào)用 scheduleWarmUpFrame
這個(gè)方法,從這個(gè)方法注釋可知,調(diào)用這個(gè)方法會(huì)主動(dòng)構(gòu)建視圖數(shù)據(jù)。這樣做的好處是因?yàn)?Flutter 依賴(lài) Dart 的 MicroTask 來(lái)進(jìn)行幀數(shù)據(jù)構(gòu)建任務(wù)的 schedule,這里通過(guò)主動(dòng)調(diào)用進(jìn)行整個(gè)周期的 “熱身”,這樣最近的下次 VSync 信號(hào)同步時(shí)就有視圖數(shù)據(jù)可提供,而不用等到 MicroTask 的 next Tick。
然后我們?cè)賮?lái)看 attachRootWidget
這個(gè)函數(shù)干了什么:
void attachRootWidget(Widget rootWidget) {
_renderViewElement = new RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget
).attachToRenderTree(buildOwner, renderViewElement);
}
attachRootWidget
把 widget交給了 RenderObjectToWidgetAdapter
這座橋梁,通過(guò)這座橋梁,Element 被創(chuàng)建,并且同時(shí)能持有 Widget 和 RenderObject的引用。然后我們從上文就知道后面發(fā)生的就是第一次的視圖數(shù)據(jù)構(gòu)建了。
從這一部分能印證了:Flutter應(yīng)用通過(guò) Widget、Element、RenderObject 三種樹(shù)結(jié)構(gòu)來(lái)維護(hù)整個(gè)應(yīng)用的視圖數(shù)據(jù)。
附言
在沒(méi)更新文章的這段期間一直在準(zhǔn)備春招,原本就準(zhǔn)備寫(xiě)一些關(guān)于 Flutter 原理的文章,今天發(fā)現(xiàn)已經(jīng)有不少大佬在解析源碼,尤其看到了 戀貓de小郭 的文章寫(xiě)得很好,希望我的一些總結(jié)也能幫助到大家吧!
我的博客:https://meandni.com/2019/05/05/flutter-principle/
我的Github:https://github.com/MeandNi/
歡迎一起討論!