Widget、Element和RenderObject
Widget
Widget 是用戶頁面的描述,表示了Element的配置信息,F(xiàn)lutter頁面都是由各種各樣的Widget組合聲明成的。Widget本身是不可變的immutable,注解如下:
@immutable
abstract class Widget extends DiagnosticableTree {/// ...}
這也就意味著,所有它直接聲明或繼承的變量都必須為final類型的。如果想給widget關(guān)聯(lián)一個(gè)可變的狀態(tài),考慮使用StatefulWidget,它會(huì)通過[StatefulWidget.createState]創(chuàng)建一個(gè)State對(duì)象,然后,每當(dāng)它轉(zhuǎn)化成一個(gè)element時(shí)會(huì)合并到樹上。
子類:
StatelessWidget、StatefulWidget我們很熟悉是用來編寫頁面和組件的,那另外三個(gè)都是做什么用的呢?
- RenderObjectWidget,從名字上就能看出它是一個(gè)Widget,然后和實(shí)際渲染對(duì)象RenderObject有撇不清的關(guān)系。它提供了RenderObjectElement的配置信息,其中包裝了RenderObject。也就是從頁面上編寫的StatelessWidget和StatefulWidget在遞歸的build過程中,會(huì)最終返回實(shí)際可渲染的Widget對(duì)象,也就是RenderObjectWidget,那么這個(gè)轉(zhuǎn)化關(guān)系是一一對(duì)應(yīng)的嗎,其實(shí)不是的,后邊再具體分析
- PreferredSizeWidget,一個(gè)返回它自身想要大小的組件,如果它在布局過程中是不受限制的,例如,AppBar和TabBar
- ProxyWidget,代理組件,提供一個(gè)子組件,而不是自己創(chuàng)建,例如,InheritedWidget和ParentDataWidget
Element
元素樹,是Widget在具體位置的實(shí)例化,它負(fù)責(zé)控制Widget的生命周期,持有了widget實(shí)例和renderObject實(shí)例,它和Widget繼承自同一個(gè)類,DiagnosticableTree可診斷樹,并且實(shí)現(xiàn)了BuildContext類。
Element有兩種基本類型:
- ComponentElement,其他elements的宿主,它本身不包含RenderObject,而由它持有的element節(jié)點(diǎn)包含,像StatelessWidget 和StatefulWidget 中分別創(chuàng)建的StatelessElement和StatefulElement都是繼承自ComponentElement
- RenderObjectElement,參與layout或者繪制階段的元素
RenderObject
渲染樹中的每個(gè)節(jié)點(diǎn)基類是RenderObject,它定義了布局和繪制的抽象模型。每一個(gè)RenderObject有一個(gè)parent,和一個(gè)parentData,父級(jí)的RenderObject可以在其中存儲(chǔ)孩子的具體數(shù)據(jù),例如,child的位置信息。
- RenderObject 僅實(shí)現(xiàn)了基本的布局和繪制,沒有具體的布局繪制模型,相當(dāng)于ViewGroup,其子類RenderBox使用了笛卡爾坐標(biāo)系,它的一些子類是真正的渲染樹上的節(jié)點(diǎn)。大多數(shù)情況下,當(dāng)我們想自定義一個(gè)渲染對(duì)象時(shí),直接繼承RenderObject有些過重overkill,更好的選擇是繼承RenderBox,除非你不想使用笛卡爾坐標(biāo)系統(tǒng)。
- RenderView,通常情況下是Flutter渲染樹的根節(jié)點(diǎn),可以理解為DecorView,它只有一個(gè)子節(jié)點(diǎn),必須是RenderBox類型的。
對(duì)應(yīng)關(guān)系
從Widget構(gòu)建Element
看這段簡(jiǎn)單的代碼片段,顯示了widget樹形結(jié)構(gòu)
Container(
color: Colors.blue,
child: Row(
children: [
Image.network('https://www.example.com/1.png'),
const Text('A'),
],
),
);
當(dāng)Flutter要渲染這個(gè)Container到頁面時(shí),會(huì)調(diào)用它的build()方法,返回一個(gè)widget的子樹,包含它的child樹Row及其children的子樹,還有一些其它的樹的節(jié)點(diǎn),看下它的build()函數(shù):
class Container extends StatelessWidget {
/// 創(chuàng)建一個(gè)結(jié)合常用的繪畫、定位和控制大小的組件
Container({
Key? key,
this.alignment,
this.padding,
this.color,
this.decoration,
this.foregroundDecoration,
double? width,
double? height,
BoxConstraints? constraints,
this.margin,
this.transform,
this.transformAlignment,
this.child,
this.clipBehavior = Clip.none,
}) : // ...
@override
Widget build(BuildContext context) {
Widget? current = child;
// ...
if (alignment != null)
current = Align(alignment: alignment!, child: current);
// ...
if (effectivePadding != null)
current = Padding(padding: effectivePadding, child: current);
if (color != null)
current = ColoredBox(color: color!, child: current);
// ...
if (decoration != null)
current = DecoratedBox(decoration: decoration!, child: current);
return current!;
}
}
可以看到,Container的一些屬性,都代表插入一個(gè)控制該屬性的新節(jié)點(diǎn)widget,所以它本身就是一個(gè)封裝,替我們組合了大量小部件,減輕了開發(fā)工作量。我們?cè)O(shè)置了color屬性,它會(huì)插入一個(gè)ColoredBox節(jié)點(diǎn),顯示它的顏色。
相應(yīng)的,Image和Text在build期間也可能插入子節(jié)點(diǎn)比如RawImage和RichText,所以widget樹的層級(jí)結(jié)構(gòu)可能比代碼展示的更深

在構(gòu)建階段,F(xiàn)lutter將上述的widget轉(zhuǎn)換成相應(yīng)的element tree ,一一對(duì)應(yīng),樹的層級(jí)結(jié)構(gòu)上的每個(gè)元素代表了一個(gè)具體位置的widget實(shí)例。
這里的一一對(duì)應(yīng)其實(shí)是framework層的經(jīng)過轉(zhuǎn)化后的widget,并不是代碼層的用戶編寫的widget跟element的對(duì)應(yīng),比如一個(gè)Container在設(shè)置屬性后被轉(zhuǎn)化成多個(gè)子widget,同時(shí)對(duì)應(yīng)了多個(gè)element節(jié)點(diǎn)。
上邊提到了Element實(shí)現(xiàn)了BuildContext,任何widget的element可以通過build()方法中傳入的BuildContext參數(shù)訪問到,它是widget在樹上操作的句柄。例如,可以調(diào)用Theme.of(context),查找widget樹上最近的主題,如果widget定義了單獨(dú)的主題就返回它,如果沒有返回app的主題
/// An [Element] that uses a [StatelessWidget] as its configuration.
class StatelessElement extends ComponentElement {
/// Creates an element that uses the given widget as its configuration.
StatelessElement(StatelessWidget widget) : super(widget);
@override
StatelessWidget get widget => super.widget as StatelessWidget;
@override
Widget build() => widget.build(this);
@override
void update(StatelessWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
_dirty = true;
rebuild();
}
}
可以看到,StatelessElement元素在構(gòu)建的時(shí)候調(diào)用build方法,會(huì)調(diào)用StatelessWidget的build方法,傳入BuildContext為this。
因?yàn)閣idgets是immutable的,包括節(jié)點(diǎn)之間的父/子關(guān)系,對(duì)widget樹的任何修改(比如,Text('A') to Text('B'))會(huì)導(dǎo)致一系列新的widget對(duì)象的被重建。但這并不意味下層必須被重建,element tree可能在界面刷新時(shí)是持久的(persistent),因此對(duì)性能起著關(guān)鍵作用,因?yàn)镕lutter緩存了底層表示,使它表現(xiàn)的可以像完全丟棄上層的widget層一樣。通過遍歷widgets的修改,可以做到只重新構(gòu)建一部分的element tree。
Element到RenderObject
只繪制單個(gè)的widget的應(yīng)用是很少見的,所以,任何的UI框架的一個(gè)重要的部分就是能夠高效的布局一個(gè)層級(jí)結(jié)構(gòu)的widget,確定它們的大小、位置然后繪制到屏幕上。
渲染樹上的每個(gè)節(jié)點(diǎn)的基類型是RenderObject,在構(gòu)建階段,F(xiàn)lutter僅將element tree中的RenderObjectElement對(duì)象生成可渲染的對(duì)象,不同的Render對(duì)象渲染不同類型,RenderParagraph渲染text,RenderImage 渲染image
Flutter中多數(shù)widgets的渲染對(duì)象是繼承自RenderBox的,它使用了笛卡爾坐標(biāo)系在2D空間,它提供了一個(gè)盒子約束模型,限制了widget的最小和最大寬度和高度。
layout期間,F(xiàn)lutter會(huì)以深度優(yōu)先遍歷渲染樹,并將constraints約束傳遞給child,用來確定child的大小,然后將結(jié)果傳遞給parent的size變量。
/// 子類不應(yīng)該直接重寫[layout]方法,而應(yīng)該重寫[performResize] and/or [performLayout], [layout]方法
/// 代理它的工作放在 [performResize] and [performLayout]
/// parent's的[performLayout]方法應(yīng)該無條件的調(diào)用所有它的child的[layout]
void layout(Constraints constraints, { bool parentUsesSize = false }) {
/// ...
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
}
/// ...
_needsLayout = false;
markNeedsPaint();
}
/// 空實(shí)現(xiàn),由子類重寫
@protected
void performLayout();
舉例,看下RenderPadding的performLayout方法:
@override
void performLayout() {
/// 第一步,拿到constraints
final BoxConstraints constraints = this.constraints;
// ...
/// 第二步,根據(jù)parent的constraints,計(jì)算自己內(nèi)部的constraints
final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding!);
/// 第三步,繼續(xù)向下遍歷layout
child!.layout(innerConstraints, parentUsesSize: true);
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = Offset(_resolvedPadding!.left, _resolvedPadding!.top);
/// 第四步,根據(jù)constraints生成size
size = constraints.constrain(Size(
_resolvedPadding!.left + child!.size.width + _resolvedPadding!.right,
_resolvedPadding!.top + child!.size.height + _resolvedPadding!.bottom,
));
}
這樣就完成了樹的深度遍歷過程
盒子約束模型是一種很強(qiáng)大的布局對(duì)象的方式,時(shí)間復(fù)雜度為O(n)
所有RenderObjects的根節(jié)點(diǎn)是RenderView,它代表了整個(gè)渲染樹的輸出。當(dāng)平臺(tái)需要渲染新的幀時(shí)(例如,一個(gè)vsync信號(hào)觸發(fā),或者texture的解壓/上傳完成)會(huì)調(diào)用RenderView對(duì)象中的compositeFrame()方法,它創(chuàng)建了一個(gè)SceneBuilder觸發(fā)屏幕的更新。當(dāng)更新完成時(shí),RenderView會(huì)傳遞這個(gè)壓縮的scene到dart:ui包中的Window.render()方法,該方法控制GPU將它渲染。
是一一對(duì)應(yīng)的關(guān)系嗎
從上面圖中可以輕松看出,并不是。

表中僅列出了常用Widget和對(duì)應(yīng)關(guān)系,并不代表全部
所以說widget和element和renderObject是一一對(duì)應(yīng)是有語境的,在展示型這一行的情況下是沒問題的,但是在全局范圍這么說,是不準(zhǔn)確的。
建立過程
上面粗略的看了三顆樹的轉(zhuǎn)化過程,那么在代碼層面,他們是如何經(jīng)過方法的調(diào)用串聯(lián)起來的呢?可以主要分為兩個(gè)過程:
根view的attachRootWidget
初始化Widget樹Element樹和RenderObject樹的root節(jié)點(diǎn),分別是RenderObjectToWidgetAdapter、RenderObjectToWidgetElement、RenderView。
然后在WidgetsBinding.attachRootWidget方法中,將runApp傳入的rootWidget添加到widget樹根RenderObjectToWidgetAdapter實(shí)例的child上,調(diào)用它的attachToRenderTree,將element關(guān)聯(lián)到RenderTree上,調(diào)用了element的mount方法。
/// Takes a widget and attaches it to the [renderViewElement], creating it if
/// necessary.
/// This is called by [runApp] to configure the widget tree.
/// * [RenderObjectToWidgetAdapter.attachToRenderTree], which inflates a
/// widget and attaches it to the render tree.
void attachRootWidget(Widget rootWidget) {
final bool isBootstrapFrame = renderViewElement == null;
_readyToProduceFrames = true;
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
if (isBootstrapFrame) {
SchedulerBinding.instance!.ensureVisualUpdate();
}
}
其中的renderView就是RenderObject tree上的根節(jié)點(diǎn),它是在RendererBinding類中被初始化的
/// The glue between the render tree and the Flutter engine.
/// render tree 和 Flutter engine之間的膠水
mixin RendererBinding on BindingBase, ServicesBinding, SchedulerBinding, GestureBinding, SemanticsBinding, HitTestable {
@override
void initInstances() {
super.initInstances();
/// ...
initRenderView();
/// ...
}
void initRenderView() {
/// ...
renderView = RenderView(configuration: createViewConfiguration(), window: window);
renderView.prepareInitialFrame();
}
}
attachToRenderTree方法
/// Used by [runApp] to bootstrap applications.
/// 供runApp使用來引導(dǎo)程序
class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
/// Used by [runApp] to bootstrap applications.
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T>? element ]) {
if (element == null) {
owner.lockState(() {
element = createElement();
assert(element != null);
element!.assignOwner(owner);
});
owner.buildScope(element!, () {
element!.mount(null, null);
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element!;
}
RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
}
這里element為空,所以創(chuàng)建了RenderObjectToWidgetElement的實(shí)例,然后mount。
子view的attachToRenderTree
element的mount方法中,這里觸發(fā)了掛載element到Element tree,判斷是包含渲染對(duì)象的RenderObjectElement就創(chuàng)建RenderObject,調(diào)用attachRenderObject掛載到RenderObject tree上。然后_rebuild→updateChild→inflateWidget→newWidget.createElement→newChild.mount(this, newSlot)觸發(fā)了樹的深度遍歷,時(shí)序圖如下(粗略)
關(guān)鍵的一點(diǎn)是,newChild.mount方法會(huì)調(diào)用Element的子類型主要是兩個(gè)SingleChildRenderObjectElement和MultiChildRenderObjectElement,名字起的很明顯,一個(gè)孩子或者多個(gè)孩子的Element。mount方法如下
class SingleChildRenderObjectElement extends RenderObjectElement {
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
_child = updateChild(_child, widget.child, null);
}
}
class MultiChildRenderObjectElement extends RenderObjectElement {
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
final List<Element> children = List<Element>.filled(widget.children.length, _NullElement.instance, growable: false);
Element? previousChild;
for (int i = 0; i < children.length; i += 1) {
final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element?>(i, previousChild));
children[i] = newChild;
previousChild = newChild;
}
_children = children;
}
}
可見它們都做了兩件事:
- 調(diào)用super.mount(),掛載element到Element tree,createRenderObject,attachRenderObject,掛載_renderObject到RenderObject tree
- updateChild,傳入widget.child,繼續(xù)下一層級(jí)的widget樹的轉(zhuǎn)換,這里slot分別傳的為null,和IndexedSlot對(duì)象
如果Element節(jié)點(diǎn)是ComponentElement類型,mount方法如下
abstract class ComponentElement extends Element {
@override
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
/// ...
_firstBuild();
assert(_child != null);
}
/// 最終會(huì)調(diào)到performRebuild
@override
void performRebuild() {
Widget? built;
try {
/// 我們經(jīng)常在代碼中重寫的build()函數(shù),就是這里
built = build();
} catch (e, stack) {
/// 構(gòu)建錯(cuò)誤頁面ErrorWidget,我們看的到錯(cuò)誤紅色頁面
built = ErrorWidget.builder(
_debugReportException(
ErrorDescription('building $this'),
e,
stack,
informationCollector: () sync* {
yield DiagnosticsDebugCreator(DebugCreator(this));
},
),
);
}
/// 更新widget,繼續(xù)循環(huán)
_child = updateChild(_child, built, slot);
}
/// 在StatelessWidget/StafulWidget中重寫的方法
@protected
Widget build();
}
Slot對(duì)象
updateChild傳入的slot對(duì)象是干什么用的呢?一句話總結(jié)就是,為了標(biāo)記RenderObject掛載到RenderObject tree上的位置。
首先,每一個(gè)Element都會(huì)最終包裹一個(gè)RenderObject,最終掛載到RenderObject tree上,不管是自身包裹,或者是它的子孫包裹。所以,當(dāng)Element的直接child不包含RenderObject時(shí),例如StatelessElement/StatefulElement,它就要標(biāo)記下一個(gè)RenderObject對(duì)象要掛載到RenderObject tree上的哪個(gè)節(jié)點(diǎn)。所以,在它們的父類ComponentElement的updateChild方法中傳的slot值就是要掛載的位置。比如這樣的element節(jié)點(diǎn),會(huì)一直向下傳遞slot直到是RenderObjectElement節(jié)點(diǎn)。
那么這個(gè)值什么情況下會(huì)初始化并往下傳遞呢?SingleChildRenderObjectElement往下傳遞的是null,看來它并不需要插槽,看下attachRenderObject方法
@override
void attachRenderObject(Object? newSlot) {
assert(_ancestorRenderObjectElement == null);
_slot = newSlot;
/// 找到是RenderObjectElement對(duì)象的祖先節(jié)點(diǎn)
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
/// 根據(jù)newSlot插槽,插入renderObject到渲染樹
_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
if (parentDataElement != null)
_updateParentData(parentDataElement.widget);
}
RenderObjectElement? _findAncestorRenderObjectElement() {
Element? ancestor = _parent;
/// 循環(huán)向上找到第一個(gè)RenderObjectElement的對(duì)象,其實(shí)就是為了找到RenderObject的父節(jié)點(diǎn)
while (ancestor != null && ancestor is! RenderObjectElement)
ancestor = ancestor._parent;
return ancestor as RenderObjectElement?;
}
所以單個(gè)孩子的SingleChildRenderObjectElement不需要slot,因?yàn)榭偰苷业?ancestor掛載點(diǎn)。而MultiChildRenderObjectElement,由于多個(gè)孩子都找到同一個(gè)ancestor節(jié)點(diǎn),所以就有了slot將兄弟節(jié)點(diǎn)按順序排列起來,生成IndexedSlot<Element?>(i, previousChild)的slot,這就有了初始的slot往下傳遞,所以slot是從MultiChildRenderObjectElement這樣的節(jié)點(diǎn)開始分化的
這里排除了剛開始建立渲染樹的根節(jié)點(diǎn)_rootChildSlot
這樣就完成了,Element tree,和RenderObject tree的父子節(jié)點(diǎn)/兄弟節(jié)點(diǎn)之間的錯(cuò)落有致的樹型結(jié)構(gòu)。RenderObjectElement在整個(gè)過程中,占據(jù)核心的功能,同時(shí)負(fù)責(zé)控制widget向下更新,和RenderObject生成,掛載到Render tree的正確節(jié)點(diǎn)上。
總結(jié)
本篇為三棵樹理解的第一篇,重點(diǎn)分析了三棵樹的建立過程,下一篇我們繼續(xù)分析三棵樹的刷新過程,以及為什么要設(shè)計(jì)三棵樹,以及理解了三棵樹的概念,對(duì)我們開發(fā)中有哪些指導(dǎo)或者注意的點(diǎn)。
文中難免有個(gè)人理解,有偏差的地方,請(qǐng)大家批評(píng)指正,多謝!