Widget太闺、Element和RenderObject
Widget
Widget 是用戶頁(yè)面的描述,表示了Element的配置信息返敬,F(xiàn)lutter頁(yè)面都是由各種各樣的Widget組合聲明成的讹剔。Widget本身是不可變的immutable,注解如下:
@immutable
abstract class Widget extends DiagnosticableTree {/// ...}
這也就意味著啊易,所有它直接聲明或繼承的變量都必須為final類(lèi)型的。如果想給widget關(guān)聯(lián)一個(gè)可變的狀態(tài)饮睬,考慮使用StatefulWidget租谈,它會(huì)通過(guò)[StatefulWidget.createState]創(chuàng)建一個(gè)State對(duì)象,然后捆愁,每當(dāng)它轉(zhuǎn)化成一個(gè)element時(shí)會(huì)合并到樹(shù)上割去。
子類(lèi):
StatelessWidget、StatefulWidget我們很熟悉是用來(lái)編寫(xiě)頁(yè)面和組件的昼丑,那另外三個(gè)都是做什么用的呢呻逆?
- RenderObjectWidget,從名字上就能看出它是一個(gè)Widget菩帝,然后和實(shí)際渲染對(duì)象RenderObject有撇不清的關(guān)系咖城。它提供了RenderObjectElement的配置信息,其中包裝了RenderObject呼奢。也就是從頁(yè)面上編寫(xiě)的StatelessWidget和StatefulWidget在遞歸的build過(guò)程中宜雀,會(huì)最終返回實(shí)際可渲染的Widget對(duì)象,也就是RenderObjectWidget控妻,那么這個(gè)轉(zhuǎn)化關(guān)系是一一對(duì)應(yīng)的嗎州袒,其實(shí)不是的,后邊再具體分析
- PreferredSizeWidget弓候,一個(gè)返回它自身想要大小的組件郎哭,如果它在布局過(guò)程中是不受限制的他匪,例如,AppBar和TabBar
- ProxyWidget夸研,代理組件邦蜜,提供一個(gè)子組件,而不是自己創(chuàng)建亥至,例如悼沈,InheritedWidget和ParentDataWidget
Element
元素樹(shù),是Widget在具體位置的實(shí)例化姐扮,它負(fù)責(zé)控制Widget的生命周期絮供,持有了widget實(shí)例和renderObject實(shí)例,它和Widget繼承自同一個(gè)類(lèi)茶敏,DiagnosticableTree可診斷樹(shù)壤靶,并且實(shí)現(xiàn)了BuildContext類(lèi)。
Element有兩種基本類(lèi)型:
- ComponentElement惊搏,其他elements的宿主贮乳,它本身不包含RenderObject,而由它持有的element節(jié)點(diǎn)包含恬惯,像StatelessWidget 和StatefulWidget 中分別創(chuàng)建的StatelessElement和StatefulElement都是繼承自ComponentElement
- RenderObjectElement向拆,參與layout或者繪制階段的元素
RenderObject
渲染樹(shù)中的每個(gè)節(jié)點(diǎn)基類(lèi)是RenderObject,它定義了布局和繪制的抽象模型酪耳。每一個(gè)RenderObject有一個(gè)parent浓恳,和一個(gè)parentData,父級(jí)的RenderObject可以在其中存儲(chǔ)孩子的具體數(shù)據(jù)葡兑,例如奖蔓,child的位置信息。
- RenderObject 僅實(shí)現(xiàn)了基本的布局和繪制讹堤,沒(méi)有具體的布局繪制模型,相當(dāng)于ViewGroup厨疙,其子類(lèi)RenderBox使用了笛卡爾坐標(biāo)系洲守,它的一些子類(lèi)是真正的渲染樹(shù)上的節(jié)點(diǎn)。大多數(shù)情況下沾凄,當(dāng)我們想自定義一個(gè)渲染對(duì)象時(shí)梗醇,直接繼承RenderObject有些過(guò)重overkill,更好的選擇是繼承RenderBox撒蟀,除非你不想使用笛卡爾坐標(biāo)系統(tǒng)叙谨。
- RenderView,通常情況下是Flutter渲染樹(shù)的根節(jié)點(diǎn)保屯,可以理解為DecorView手负,它只有一個(gè)子節(jié)點(diǎn)涤垫,必須是RenderBox類(lèi)型的。
對(duì)應(yīng)關(guān)系
從Widget構(gòu)建Element
看這段簡(jiǎn)單的代碼片段竟终,顯示了widget樹(shù)形結(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到頁(yè)面時(shí)蝠猬,會(huì)調(diào)用它的build()方法,返回一個(gè)widget的子樹(shù)统捶,包含它的child樹(shù)Row及其children的子樹(shù)榆芦,還有一些其它的樹(shù)的節(jié)點(diǎn),看下它的build()函數(shù):
class Container extends StatelessWidget {
/// 創(chuàng)建一個(gè)結(jié)合常用的繪畫(huà)喘鸟、定位和控制大小的組件
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è)封裝崎淳,替我們組合了大量小部件,減輕了開(kāi)發(fā)工作量兑凿。我們?cè)O(shè)置了color屬性凯力,它會(huì)插入一個(gè)ColoredBox節(jié)點(diǎn),顯示它的顏色礼华。
相應(yīng)的咐鹤,Image和Text在build期間也可能插入子節(jié)點(diǎn)比如RawImage和RichText,所以widget樹(shù)的層級(jí)結(jié)構(gòu)可能比代碼展示的更深
在構(gòu)建階段圣絮,F(xiàn)lutter將上述的widget轉(zhuǎn)換成相應(yīng)的element tree 祈惶,一一對(duì)應(yīng),樹(shù)的層級(jí)結(jié)構(gòu)上的每個(gè)元素代表了一個(gè)具體位置的widget實(shí)例扮匠。
這里的一一對(duì)應(yīng)其實(shí)是framework層的經(jīng)過(guò)轉(zhuǎn)化后的widget捧请,并不是代碼層的用戶編寫(xiě)的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可以通過(guò)build()方法中傳入的BuildContext參數(shù)訪問(wèn)到力麸,它是widget在樹(shù)上操作的句柄可款。例如,可以調(diào)用Theme.of(context)克蚂,查找widget樹(shù)上最近的主題闺鲸,如果widget定義了單獨(dú)的主題就返回它,如果沒(méi)有返回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樹(shù)的任何修改(比如,Text('A') to Text('B'))會(huì)導(dǎo)致一系列新的widget對(duì)象的被重建谜慌。但這并不意味下層必須被重建然想,element tree可能在界面刷新時(shí)是持久的(persistent),因此對(duì)性能起著關(guān)鍵作用欣范,因?yàn)镕lutter緩存了底層表示变泄,使它表現(xiàn)的可以像完全丟棄上層的widget層一樣。通過(guò)遍歷widgets的修改恼琼,可以做到只重新構(gòu)建一部分的element tree妨蛹。
Element到RenderObject
只繪制單個(gè)的widget的應(yīng)用是很少見(jiàn)的,所以晴竞,任何的UI框架的一個(gè)重要的部分就是能夠高效的布局一個(gè)層級(jí)結(jié)構(gòu)的widget蛙卤,確定它們的大小、位置然后繪制到屏幕上噩死。
渲染樹(shù)上的每個(gè)節(jié)點(diǎn)的基類(lèi)型是RenderObject颤难,在構(gòu)建階段,F(xiàn)lutter僅將element tree中的RenderObjectElement對(duì)象生成可渲染的對(duì)象已维,不同的Render對(duì)象渲染不同類(lèi)型行嗤,RenderParagraph
渲染text酱畅,RenderImage
渲染image
Flutter中多數(shù)widgets的渲染對(duì)象是繼承自RenderBox的鹦倚,它使用了笛卡爾坐標(biāo)系在2D空間,它提供了一個(gè)盒子約束模型耸彪,限制了widget的最小和最大寬度和高度堂鲜。
layout期間栈雳,F(xiàn)lutter會(huì)以深度優(yōu)先遍歷渲染樹(shù),并將constraints約束傳遞給child缔莲,用來(lái)確定child的大小哥纫,然后將結(jié)果傳遞給parent的size變量。
/// 子類(lèi)不應(yīng)該直接重寫(xiě)[layout]方法痴奏,而應(yīng)該重寫(xiě)[performResize] and/or [performLayout]磺箕, [layout]方法
/// 代理它的工作放在 [performResize] and [performLayout]
/// parent's的[performLayout]方法應(yīng)該無(wú)條件的調(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),由子類(lèi)重寫(xiě)
@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,
));
}
這樣就完成了樹(shù)的深度遍歷過(guò)程
盒子約束模型是一種很強(qiáng)大的布局對(duì)象的方式棉姐,時(shí)間復(fù)雜度為O(n)
所有RenderObjects的根節(jié)點(diǎn)是RenderView屠列,它代表了整個(gè)渲染樹(shù)的輸出。當(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)系厅须,并不代表全部
所以說(shuō)widget和element和renderObject是一一對(duì)應(yīng)是有語(yǔ)境的仿畸,在展示型這一行的情況下是沒(méi)問(wèn)題的,但是在全局范圍這么說(shuō)朗和,是不準(zhǔn)確的错沽。
建立過(guò)程
上面粗略的看了三顆樹(shù)的轉(zhuǎn)化過(guò)程,那么在代碼層面眶拉,他們是如何經(jīng)過(guò)方法的調(diào)用串聯(lián)起來(lái)的呢千埃?可以主要分為兩個(gè)過(guò)程:
根view的attachRootWidget
初始化Widget樹(shù)Element樹(shù)和RenderObject樹(shù)的root節(jié)點(diǎn),分別是RenderObjectToWidgetAdapter镀层、RenderObjectToWidgetElement镰禾、RenderView。
然后在WidgetsBinding.attachRootWidget方法中唱逢,將runApp傳入的rootWidget添加到widget樹(shù)根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類(lèi)中被初始化的
/// 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使用來(lái)引導(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ù)的深度遍歷潦俺,時(shí)序圖如下(粗略)
關(guān)鍵的一點(diǎn)是,newChild.mount方法會(huì)調(diào)用Element的子類(lèi)型主要是兩個(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;
}
}
可見(jiàn)它們都做了兩件事:
- 調(diào)用super.mount(),掛載element到Element tree肖爵,createRenderObject卢鹦,attachRenderObject,掛載_renderObject到RenderObject tree
- updateChild劝堪,傳入widget.child冀自,繼續(xù)下一層級(jí)的widget樹(shù)的轉(zhuǎn)換,這里slot分別傳的為null幅聘,和IndexedSlot對(duì)象
如果Element節(jié)點(diǎn)是ComponentElement類(lèi)型凡纳,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)常在代碼中重寫(xiě)的build()函數(shù),就是這里
built = build();
} catch (e, stack) {
/// 構(gòu)建錯(cuò)誤頁(yè)面ErrorWidget帝蒿,我們看的到錯(cuò)誤紅色頁(yè)面
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中重寫(xiě)的方法
@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)务豺。所以,在它們的父類(lèi)ComponentElement的updateChild方法中傳的slot值就是要掛載的位置嗦明。比如這樣的element節(jié)點(diǎn)笼沥,會(huì)一直向下傳遞slot直到是RenderObjectElement節(jié)點(diǎn)。
那么這個(gè)值什么情況下會(huì)初始化并往下傳遞呢娶牌?SingleChildRenderObjectElement往下傳遞的是null奔浅,看來(lái)它并不需要插槽,看下attachRenderObject方法
@override
void attachRenderObject(Object? newSlot) {
assert(_ancestorRenderObjectElement == null);
_slot = newSlot;
/// 找到是RenderObjectElement對(duì)象的祖先節(jié)點(diǎn)
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
/// 根據(jù)newSlot插槽诗良,插入renderObject到渲染樹(shù)
_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)按順序排列起來(lái),生成IndexedSlot<Element?>(i, previousChild)的slot葛作,這就有了初始的slot往下傳遞,所以slot是從MultiChildRenderObjectElement這樣的節(jié)點(diǎn)開(kāi)始分化的
這里排除了剛開(kāi)始建立渲染樹(shù)的根節(jié)點(diǎn)_rootChildSlot
這樣就完成了猖凛,Element tree赂蠢,和RenderObject tree的父子節(jié)點(diǎn)/兄弟節(jié)點(diǎn)之間的錯(cuò)落有致的樹(shù)型結(jié)構(gòu)。RenderObjectElement在整個(gè)過(guò)程中辨泳,占據(jù)核心的功能虱岂,同時(shí)負(fù)責(zé)控制widget向下更新,和RenderObject生成菠红,掛載到Render tree的正確節(jié)點(diǎn)上第岖。
總結(jié)
本篇為三棵樹(shù)理解的第一篇,重點(diǎn)分析了三棵樹(shù)的建立過(guò)程试溯,下一篇我們繼續(xù)分析三棵樹(shù)的刷新過(guò)程蔑滓,以及為什么要設(shè)計(jì)三棵樹(shù),以及理解了三棵樹(shù)的概念遇绞,對(duì)我們開(kāi)發(fā)中有哪些指導(dǎo)或者注意的點(diǎn)键袱。
文中難免有個(gè)人理解,有偏差的地方摹闽,請(qǐng)大家批評(píng)指正蹄咖,多謝!