關于三棵樹
Flutter 的核心設計思想是一切皆組件 椒拗。Flutter 將組件的概念進行了擴展波丰,把組件的組織和渲染抽象為三部分叨恨,即 Widget识藤,Element 和 RenderObject邪狞。
Widget
Widget 只是一個配置祷蝌,里面存儲的是有關視圖渲染的配置信息,包括布局帆卓、渲染屬性巨朦、事件響應信息等。
Widget 是不可變的剑令,無法更新糊啡,數(shù)據(jù)更新是以重建 Widget 樹的方式進行,會涉及對象的銷毀重建和垃圾回收吁津,所以但是因為只有配置信息棚蓄,不涉及渲染繪制,所以重建的成本很低碍脏。
Element
Element 是 Widget 的一個實例化對象梭依,它承載了視圖構建的上下文數(shù)據(jù),是連接結(jié)構化的配置信息到完成最終渲染的橋梁典尾。
Element 是可變的役拴。Element 樹這一層將 Widget 樹的變化(類似 React 虛擬 DOM diff)做了抽象,可以只將真正需要修改的部分同步到真實的 RenderObject 樹中钾埂,最大程度降低對真實渲染視圖的修改河闰,提高渲染效率科平,而不是銷毀整個渲染視圖樹重建。
當新的 Widget 替換舊的 Widget姜性,導致 Element 變化匠抗,也就是說,多個 Widget 對應一個 Element污抬。
RenderObject
RenderObject 是主要負責實現(xiàn)視圖渲染的對象。
RenderObject 樹在 Flutter 的展示過程分為四個階段绳军,即布局印机、繪制、合成和渲染门驾。 其中射赛,布局和繪制在 RenderObject 中完成,F(xiàn)lutter 采用深度優(yōu)先機制遍歷渲染對象樹奶是,確定樹中各個對象的位置和尺寸楣责,并把它們繪制到不同的圖層上。繪制完畢后聂沙,合成和渲染的工作則交給 Skia 搞定秆麸。
BuildContext
BuildContext 對象實際上是 Element 對象。BuildContext 接口用于阻止對 Element 對象的直接操作及汉。
我們?nèi)粘i_發(fā)中沮趣,一般接觸的都是 Widget,并沒有使用到 Element坷随,其實我們也在一直操作著 Element房铭,BuildContext 對象實際上就是 Element 對象, Element 實現(xiàn)了 BuildContext温眉,告訴了使用者控件在哪里缸匪、可以做什么。BuildContext 接口設計用于阻止對 Element 對象的直接操作类溢。
我們可以把 Widget 當做菜譜凌蔬,Element 是配菜,RenderObject 是燒菜和出菜豌骏。
流程
RenderObjectWidget 介紹
StatelessWidget 和 StatefulWidget 只是用來組裝控件的容器龟梦,并不負責組件最后的布局和繪制。在 Flutter 中窃躲,布局和繪制工作實際上是在 Widget 的另一個子類 RenderObjectWidget 內(nèi)完成的计贰。比如 Text:
class Text extends StatelessWidget {
@override
Widget build(BuildContext context) {
// 返回的是 RichText
Widget result = RichText(...)
...
}
}
class RichText extends MultiChildRenderObjectWidget{}
abstract class MultiChildRenderObjectWidget extends 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) { }
...
}
對于 Element 的創(chuàng)建躁倒,F(xiàn)lutter 會在遍歷 Widget 樹時荞怒,調(diào)用 createElement 去同步 Widget 自身配置,從而生成對應節(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 方法荧嵌。在這個方法里,會完成與之關聯(lián)的 RenderObject 對象的創(chuàng)建砾淌,以及與渲染樹的插入工作啦撮,插入到渲染樹后的 Element 就可以顯示到屏幕中了。
構建
當mount
被調(diào)用汪厨,發(fā)生第一次構建或者 Widget 改變update
被調(diào)用赃春,會在rebuild
中調(diào)用performRebuild
,在此方法中劫乱,父元素調(diào)用updateChild
方法织中。在此構建階段,父元素檢查子元素是否可以更新衷戈、刪除或添加到元素樹中狭吼。
@protected
@pragma('vm:prefer-inline')
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {}
通過注釋我們知道,這個是 widgets 系統(tǒng)的核心脱惰。每次要根據(jù)更新的配置添加搏嗡、更新或刪除子元素時,都會調(diào)用它拉一。
child
:由其父元素檢查以在當前構建過程中添加采盒、刪除、更新或重用的元素蔚润。newWidget
:子元素將在當前構建過程中引用的小部件 Widget 磅氨。
我們從第一個 case 開始看:
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
如果新小部件為null,并且子元素不為空嫡纠,則框架從元素樹中刪除子元素烦租。父元素調(diào)用deactivateChild
將子元素置于非活動狀態(tài)的方法,并將子元素的相應渲染對象從渲染樹中分離出來除盏。對應的場景例如:刷新列表后列表數(shù)據(jù)為空叉橱。
再看第二個 case:
if (child != null) {
...
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
}
...
如果子元素和新小部件都是非空的,并且子元素的舊小部件和新小部件是相同的實例者蠕,那么當前子元素被重用而不需要更新窃祝。因此,不會調(diào)用相應子小部件的構建方法踱侣。就性能而言粪小,這是最理想的情況大磺。也就是負責配置的 Widget 沒有改變,那么會重用這個元素探膊,直接返回杠愧。
再看第三個 case:
else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
assert(child.widget == newWidget);
assert(() {
child.owner!._debugElementWasRebuilt(child);
return true;
}());
newChild = child;
}
如果子元素和新小部件都是非空的,并且新舊小部件不是同一個實例逞壁,但 canUpdate 方法返回 true流济,通過新的配置更新子元素。對應的場景例如:Text
的文本發(fā)生改變后引起的構建腌闯。
再看第四個 case:
else {
deactivateChild(child);
assert(child._parent == null);
newChild = inflateWidget(newWidget, newSlot);
}
如果子元素和新小部件都是非空的袭灯,并且新舊小部件不相同但canUpdate返回false,那么deactivateChild
刪除子元素绑嘹,相應的渲染對象與渲染樹分離。最后橘茉,將新小部件的新元素返回到元素樹工腋。這是在性能方面最昂貴的情況,因為實例化了新元素節(jié)點和渲染對象節(jié)點畅卓。對應的場景例如:Text
變成了Button
擅腰。
再看第五個 case:
else {
newChild = inflateWidget(newWidget, newSlot);
}
如果子元素為 null,并且其新子元素為非 null翁潘,那么說明構建階段的樹的此位置有一個新的小部件趁冈。因此,父元素首先調(diào)用inflateWidget
拜马,在其中調(diào)用新小部件方法的createElement
方法渗勘,并返回新小部件的新元素。此時俩莽,父級也為創(chuàng)建的元素設置了一個槽旺坠。對應的場景例如:在空的row
里添加了一個Button
。
當然扮超,第一個構建時取刃,都會走到這個 case。
更新
在StatefulWidget
被加載時出刷,StatefulElement
會被創(chuàng)建璧疗,StatefulElement
的構造方法中,通過createState
方法創(chuàng)建State
馁龟,并將StatefulWidget
和State
對象將永久關聯(lián)崩侠。
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
在mount
中,調(diào)用_firstBuild
屁柏,然后調(diào)用state.initState()
啦膜,初始化State
有送,并且之后調(diào)用一次didChangeDependencies
。
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
...
_firstBuild();
...
}
@override
void _firstBuild() {
assert(state._debugLifecycleState == _StateLifecycle.created);
try {
_debugSetAllowIgnoredCallsToMarkNeedsBuild(true);
final Object? debugCheckForReturnedFuture = state.initState() as dynamic;
...
state.didChangeDependencies();
...
}
而在上面第三個case中僧家,child.update(newWidget)
會先調(diào)用state.didUpdateWidget(oldWidget)
雀摘,再build
,所以我們重寫didUpdateWidget
方法時八拱,State
由于保證該build
方法會在didUpdateWidget
之后被調(diào)用阵赠,因此無需在didUpdateWidget
中顯式觸發(fā)構建。
@override
void update(StatefulWidget newWidget) {
super.update(newWidget);
...
final Object? debugCheckForReturnedFuture = state.didUpdateWidget(oldWidget) as dynamic;
...
rebuild();
}