Flutter源码阅读(1)-Widget,Element,RenderObject树的构建和更新流程
前言
最近在做Flutter项目,写代码的时候一直在想一个问题,为什么Flutter会用这种嵌套的方式去写代码。为什么不像原生开发那样,将一个View定义为一个属性,然后添加到父View上。
带着这个疑问,去看了Widget的源码(定义在framework.dart里),也顺道发现了其他的问题
- Widget,Elemnet,RenderObject的三棵树的关系,以及三棵树的构建和更新流程
- 为什么通过setState方法可以去更新状态
- GlobalKey是如何实现的
- ParentData是什么东西
- 如何通过InheritedWidget在Widget间传递数据
下面先从Widget的构建,更新流程讲起,顺便也引出一些其他的问题的答案
Tips:
- 如果不想看代码,可以直接看下方小节末的流程图,可以了解到树的构建和更新流程
- 下方的树如果没有指明是什么树,一般都是指Widget,Elemnet,RenderObject三棵树
- 下方代码块中用...代替的一般都是断言和调试代码,因为篇幅限制,这里就省略掉,但是建议在开发的时候可以详细去看一下,因为这有助于了解调试信息
一 概念说明
Widget,State,Elemnet,RenderObject,BuildOwner和BuildContext的概念
Widget 和 State
用于描述UI的配置数据,用于描述Element.同时也是一个生产者,产生Element,RenderObject。
Element
Element是真正的节点。同时也是一个管理者,管理着Widget,State节点,并利用Widget,State构建和更新Element树。同时也管理着RenderObject树的构建和更新
BuildOwner
BuildOwner是一个调度中心。使用Widget管理Element树的构建和更新流程,再由Element树构建Widget和RenderObject树。
在学习Flutter的过程中,我们一般都会接触到。但是我们平时开发的时候一般也只会接触Widget,那么三棵树是怎么构建起来的呢?这里就得说一下BuildOwner.BuildOwner其实是一个调度调度中心,在接收到外部调用时, 会执行下面的流程
- 通过Widget去产生一个Element。然后BuildOwner会绑定这个Element.
- 在绑定Element过程中,Element会调用Widget的build方法或是State中的build方法去产生一个子Widget
- 这个子Widget又会产生一个子Element。并去绑定这个子Element,绑定子Element B的时候,会把子Element中parent指向步骤2中的Element。绑定时又会走到步骤2 通过步骤2,3的不断重复,就会从上往下的去创建Element节点,从而构建Element树。在这个流程里,Widget和Element是一对一的。每产生一个Widget就会对应一个Element.
RenderObject
RenderObject是渲染在屏幕上的对象,类似于iOS开发中的UIView。持有一个用于绘制的Layer。
在绑定Element的时候,如果有需要,会调用widget去创建一个RenderObject,Element会持有这个RenderObject。注意这里并不是每个Element都会拥有一个RenderObject。因为不是每个element都会渲染在屏幕上。有些Element只是用来描述这个RenderObject是大小,位置,有些Element是用来存放依赖关系的。
当沿着Element树往下构建的时候,如果出现了持有RenderObject的子Element,子Element中的RenderObject的parent属性就会指向上一级的RenderObject。所以构建出来的树有可能是下图那样的
Widget树和Element树一一对应,但是Element树和RenderObject不是一一对应。
几个角色关系如下
为了理解构建和更新流程,我们从源码一步步地看这个流程
二 树的构建过程
构建流程
从main函数开始,会调用runApp()方法,并传入一个widget,这个runApp(Widget app)的实现如下
WidgetsFlutterBinding.ensureInitialized()
..scheduleAttachRootWidget(app)
..scheduleWarmUpFrame();
我们可以看到,其中调用了WidgetsFlutterBinding的scheduleAttachRootWidget方法,WidgetsFlutterBinding是什么我们我们往后再讲,不在这篇文章的范围内,看一下scheduleAttachRootWidget(Widget rootWidget)这个方法,这个方法里调用了attachRootWidget方法,这个attachRootWidget如下
void attachRootWidget(Widget rootWidget) {
_readyToProduceFrames = true;
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner, renderViewElement as RenderObjectToWidgetElement<RenderBox>);
}
attachRootWidget(Widget rootWidget)里面主要是用传入的rootWidget创建一个RenderObjectToWidgetAdapter对象并调用RenderObjectToWidgetAdapter对象的attachToRenderTree方法 attachToRenderTree方法的实现如下
void updateRenderObject(BuildContext context, RenderObject renderObject) { }
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);
});
SchedulerBinding.instance.ensureVisualUpdate();
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
如果传入的elemnt为空,则去创建一个新的RenderObjectToWidgetElement。如果不为空,则会调用elemnt的markNeedsBuild方法。 _elemnt对象在正常像调用runApp(MyApp())时一般都为null,非null的情况一般是在更新widget的时候才会出现,更新widget的情况在下一小节讨论。这里先讨论一下初始构建的情况
在初始构建过程,会调用owner的下面这个方法
buildScope(Element context, [VoidCallback callback])
前面也提到,BuildOwner是一个调度中心,主要是负责调用Element去处理树的构建和更新流程,其主要是在buildScope这个方法处理,因为buildScope这个方法设计到树的更新流程,也留到下一个小节再讲。 再看下上方调用buildScope方法中传入了一个callback回调,如下
() {
element.mount(null, null);
}
这个callback会在buildScope方法进行具体的处理前执行,也就是说在buildScope进行具体的处理前会先调用element.mount(null, null);
这个mount方法是用来将一个Elemnt绑定到Elemnt树中的。那么是如何绑定的呢?先看下Element类中mount方法的实现
mount方法
void mount(Element parent, dynamic newSlot) {
...
_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;
final Key key = widget.key;
if (key is GlobalKey) {
key._register(this);
}
_updateInheritance();
...
}
Element类中的mount方法只做了几个简单的事情
- 更新_parent值为传入的值
- 更新_slot,_depth,_activie。_slot是一个位置标记,_depth是该Elemnt在Elemnt树中的深度,_activit是指该Elemnt的状态
- 更新这个Elemnt的owner为父Elemnt的owner。因为管理树只需要一个BuildOwner就可以了,根Elemnt的owner属性会被赋予一个BuildOwner,子Element只要使用这个值就可以了
- 注册global key,更新依赖
对于第4点,因为涉及到更新流程,放在第三小节去讲述。
对于第一点,将传入的parent赋值到Elemnt的_parent属性中,代表该Elemnt的父节点是parent对应的Elemnt。如果parent为null,那基本上这个就是一个根节点。
从mount方法可以看出,Elemnt类中在绑定过程更新了父节点的引用,每一个Elemnt都知道父节点是谁。但是Elemnt的子节点呢?怎么去构建子节点呢?
首先得明确一点的是,对于Element,RenderObjct树中,除了根节点,每一个节点都有父节点,所以对于Element,RenderObjct这两个基础类中,都会有一个parent的属性,去指明谁是它的父节点。
但Element,RenderObjct树中不是每一个节点都会有子节点,所以在设计中能看的出来,Widet,Element,RenderObjct这三个基础类都没有子节点的医用。都是交由它们的子类去实现。
Elemnt中有两个大的子类,一个是ComponentElement,对应着不是直接渲染在屏幕上的Elemnt,用于组合其他的Element,一个是RenderObjctElemnt,对应着渲染在屏幕上的Elemnt。
ComponentElemnt类的mount方法
在ComponentElemnt类及其子类中,mount方法的实现如下
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_firstBuild();
}
可以看出,额外的调用了_firstBuild方法,_firstBuild里的实现很简单,就是调用了Element类中的rebuild()方法,rebuild()实现如下
void rebuild() {
assert(_debugLifecycleState != _ElementLifecycle.initial);
if (!_active || !_dirty) return; //如果是非活跃状态或是非dirty状态,则直接返回
...
performRebuild(); //调用performRebuild方法
...
}
rebuild()方法里,先判断是否是处于活跃且不为dirty的状态,如果不是,则不执行任何操作。这样可以。如果是,则调用performRebuild()方法,ComponentElemnt中的performRebuild()实现如下
void performRebuild() {
...
Widget built;
try {
...
built = build(); //执行build方法
...
} catch (e, stack) {
_debugDoingBuild = false;
built = ErrorWidget.builder(
_debugReportException(
ErrorDescription('building $this'),
e,
stack,
informationCollector: () sync* {
yield DiagnosticsDebugCreator(DebugCreator(this));
},
),
);
} finally {
// We delay marking the element as clean until after calling build() so
// that attempts to markNeedsBuild() during build() will be ignored.
// 为了防止在build方法期间执行markNeedsBuild造成影响,所以这里在build方法以后才恢复状态
_dirty = false;
...
}
try {
//第一次_child为空,则创建一个新的element
//更新时候_child不为空
_child = updateChild(_child, built, slot);
...
} catch (e, stack) {
built = ErrorWidget.builder(
_debugReportException(
ErrorDescription('building $this'),
e,
stack,
informationCollector: () sync* {
yield DiagnosticsDebugCreator(DebugCreator(this));
},
),
);
_child = updateChild(null, built, slot);
}
...
}
这段代码做了两个事情,第一个是调用build()方法去得到一个Widget,第二个是用得到的Widget去产生一个Elemnt,并更新Element树。 我们先看下build()方法, ComponentElemnt类中的build()是一个空的方法,具体的实现交由子类去做, ComponentElemnt有三个子主要的子类,我们常用的类基本都继承这三个类,我们看下这三个类中build()方法的实现
- StatelessElement
Widget build() => widget.build(this);
- StatefulElement
Widget build() => _state.build(this);
- ProxyElement
Widget build() => widget.child;
对于StatelessElement和StatefulElement就是调用我们开发中直接接触到的
Widget build(BuildContext context);
对于这三个类,其实都是为了获得一个widget.然后这个拿这个widet去调用Elemnt中的updateChild方法 而这个updateChild方法,就是framework.dart的最重要的方法了,贯穿整个Element树的构建和更新流程. updateChild方法的定义如下
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
if (newWidget == null) {
//1 移除Element
if (child != null) deactivateChild(child);
return null;
}
Element newChild;
if (child != null) {
//2 更新Element
...
if (hasSameSuperclass && child.widget == newWidget) {
//2.1 有相同的widget
if (child.slot != newSlot)
updateSlotForChild(child, newSlot); //如果插口位置不一致,则更新插口位置
newChild = child;
} else if (hasSameSuperclass &&
Widget.canUpdate(child.widget, newWidget)) {
//2.2 widget不是同一个,但是canUpdate不是返回yes,重用elemnt,更新element的widget
if (child.slot != newSlot) updateSlotForChild(child, newSlot);
child.update(newWidget);
...
newChild = child;
} else {
//2.3 element和widget都不是同一个,在将element变为deactivate并增加一个新的element
deactivateChild(child);
...
newChild = inflateWidget(newWidget, newSlot);
}
} else {
//3 创建或是从GloabalKey中重用一个Element
//根据newwidget的值判断是否需要返回一个新的element
newChild = inflateWidget(newWidget, newSlot);
}
...
return newChild;
}
因为这里设计到了很多widget树更新的逻辑,我们在讲到更新流程的时候再回过头来去说这个方法。当我们首次构建的时候,我们创建了widget,但是还没创建Element,所以此时widget不为null,child为null。这时候会直接走到inflateWidget(newWidget, newSlot);这个方法里面。inflateWidget的定义如下
Element inflateWidget(Widget newWidget, dynamic newSlot) {
...
final Key key = newWidget.key;
if (key is GlobalKey) {
final Element newChild = _retakeInactiveElement(key, newWidget);
if (newChild != null) {
//如果存在globalkey而且已经纳入到 inactive elements中,则拿出来重用,否则创建新的
...
newChild._activateWithParent(this, newSlot);
final Element updatedChild = updateChild(newChild, newWidget, newSlot);
assert(newChild == updatedChild);
return updatedChild;
}
}
final Element newChild = newWidget.createElement(); //创建新的element
...
newChild.mount(this, newSlot); //对新的element进行mount
...
return newChild;
}
这个方法中,首先处理GloabalKey相关的逻辑(第3小节会说到),然后调用widget的createElement()方法去创建一个Elemnt。创建完Element方法后,会对该Element调用mount()方法。
这时候又会调用Elemnt的mount方法,但是注意,这个是子Element的mount方法。当调用这个子Element的mount()方法的时候,又会重新走一遍绑定流程,直到没有子节点为止。调用流程如下
mount -> firstBuild -> rebuild - > performRebuild -> updateChild -> inflateWidget -> mount
如果看到这里,觉得调用流程有点复杂,可以先不用记住流程,后面会给出构建的流程图
RenderObjctElemnt的mount方法
在RenderObjctElemnt中,mount方法的定义如下
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
...
attachRenderObject(newSlot); //绑定render object
_dirty = false;
}
这里调用了RenderObjctElemnt的attachRenderObject方法为该RenderObjctElemnt去绑定一个RenderObjcet,RenderObjctElemnt 中 attachRenderObject的实现如下
@override
void attachRenderObject(dynamic newSlot) {
_slot = newSlot;
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertChildRenderObject(
renderObject, newSlot);
final ParentDataElement<ParentData> parentDataElement =
_findAncestorParentDataElement();
if (parentDataElement != null) _updateParentData(parentDataElement.widget);
}
attachRenderObject方法中,先调用_findAncestorRenderObjectElement找到是RenderObjctElement类型的Element.赋值给_ancestorRenderObjectElement。再调用insertChildRenderObject把对应的RenderObjct插入到_ancestorRenderObjectElement的子RenderObelct列表中。这样做是因为RenderObjct树不是和Element树一一对应的。RenderObjct是会渲染屏幕上的节点,不是所有的Element都会渲染在屏幕上。所以为了保证renderObjct树正确的构建,需要调用_findAncestorRenderObjectElement找到有RenderObjct的上级Element节点,忽略与渲染无关的节点,找到父RenderObject。
可以看出RenderObjctElemnt中的mount方法中只调用了attachRenderObject去绑定由widget产生的RenderObjct,并没有对子Element节点的处理。那么对于RenderObjctElemnt,是怎么构建子树的呢?
RenderObjctElemnt是一个渲染的节点。但不是每一个渲染的节点都会有子节点,所以对于子节点的构建,交由具体有子节点的子类去实现。RenderObjctElemnt有两个子类是有子节点的,分别是有一个子节点的SingleChildRenderObjectElement和多个子节点的MultiChildRenderObjectElement。
SingleChildRenderObjectElement的mount方法如下(其中的child就是创建widget中的child)
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_child = updateChild(_child, widget.child, null);
}
MultiChildRenderObjectElement的mount方法如下(其中的children就是创建widget中的childrenl列表)
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_children = List<Element>(widget.children.length);
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;
}
}
从上面的代码可知其中的updateChild,会调用inflateWidget方法。所以对于带有的子节点的RederObjctElement的子类,构建过程中都会去调用inflateWidget方法。inflateWidget方法中会产生一个子节点,再调用子节点的mount方法。流程如下图
这里总结一下树的构建流程
当调用runAPP()方法开始,调用根Widge的createElement方法,得到一个根Element,调用根Elemnt的mount方法,然后不断的往下去创建子Widget,再调用子Widget的createElement方法创建子Elemnt,再调用子Elemnt的mount方法,一直往下创建节点,直到没有子节点的叶子节点为止。如下图
三 树的更新过程
更新流程
当构建完树后,是如何对树进行增加,删除,修改树中节点的操作呢?前面提到过,BuildOwner是负责处理构建,更新树的管理者。它与树的角色大概如图所示。
更新树的流程大概如下:BuildOwner负责一个调度的作用,当我们操作树的时候,通知BuildOwner需要更新。当框架知道需要更新的时候,会调用BuildOwner的buildScope方法。buildScope方法中会对需要更新的Elemnt进行更新。
现在我们先从最熟悉的方法,也就是State的setState方法开始讲起。setState方法定义如下
void setState(VoidCallback fn) {
...
final dynamic result = fn() as dynamic;
...
_element.markNeedsBuild();
}
可以看出,这个方法先是我们调用了传入的方法,执行方法后,就调用了State中的_element的markNeedsBuild()方法,markNeedsBuild()方法是将一个Element标记为需要更新。方法的定义如下
void markNeedsBuild() {
...
if (!_active) return;
...
_dirty = true;
owner.scheduleBuildFor(this); //加入build owner的rebuild计划
}
markNeedsBuild方法首先判断一个Elemnt是否是_active,如果不是,则不做任何处理,因为一个非活跃的状态不会显示在界面上,所以不需做处理。然后将其_dirty值设置为true,标记这个Element 需要更新。然后调用BuildOwner的scheduleBuildFor方法,并传入需要更新的Element。BuildOwner的scheduleBuildFor定义如下
void scheduleBuildFor(Element element) {
...
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled(); // 通知Engine 在下一帧需要做更新操作;
}
_dirtyElements.add(element);
...
}
方法里的_scheduledFlushDirtyElements表明是否是在更新过程中,可以看到,这里先判断_scheduledFlushDirtyElements是否为true,就是不在更新过程中,则调用onBuildScheduled通知框架需要进行更新树。 然后将刚才传入的element添加到BuildOwner的_dirtyElements中。这个_dirtyElements是一个列表,存储着需要所有更新的Elemnt。
当框架收到需要更新树的信息后,就会调用BuildOwner的buildScope()方法,前面在构建过程中提到过这个buildScope方法,但是没有细说,在这里我们看一下buildScope方法的实现
void buildScope(Element context, [VoidCallback callback]) {
if (callback == null && _dirtyElements.isEmpty) return;
...
//deng 问题 这个timeline是什么
Timeline.startSync('Build',
arguments: timelineArgumentsIndicatingLandmarkEvent);
try {
_scheduledFlushDirtyElements = true;
if (callback != null) {
...
try {
callback(); //callback先执行
} finally {
...
}
}
...
try {
_dirtyElements[index].rebuild(); //进行rebuild
} catch (e, stack) {
...
}
...
}
...
} finally {
...
_dirtyElements.clear();
...
}
...
}
buildScope()方法中先是执行了callback方法,对于首次构建的流程,这个callback方法就是调用根Elemnt的mount方法进行构建。
然后这个方法主要就做了一件事,就是从_dirtyElements列表中取出每一个需要更新的Element,然后对Elemnt调用rebuild()方法,再清空_dirtyElements列表,标志着这一轮更新完成。
从前面的构建流程可知, rebuild方法会调用performRebuild方法。performRebuild对于不同的Element子类,有着不一样的额外的实现。
首先对于RenderObjctElemnt类,只会调用updateRenderObject去更新RenderObjct对象,不会涉及任何的子节点的更新.那这里就有疑问了,该怎么对有子节点的RenderObjctElemnt进行更新子树呢?这里先留一下小疑问,等讲完ComponetElemnt然后再解答。
对于ComponetElemnt及其子类,调用performRebuild方法会调用updateChild方法。前面在构建过程中,就贴出了performRebuild和updateChild这两个方法,这里就不再重复去贴代码。在这里重点说一下updateChild这个方法
updateChild
updateChild中传入了(Element child, Widget newWidget, dynamic newSlot) 三个参数。child代表的是该ComponetElemnt的子节点,newWidget是子节点对应的Widget,newSlot是子节点的位置信息
在执行updateChild过程中根据传入的参数做了以下的一些处理。
-
如果new widget为空,但是element不为空(也就是原有的widget被删除了)。首先deactivateChild(child),如果child不为空,则解绑child的renderobjce,并添加到build owner中的_inactiveElements列表中,并返回函数。deactivateChild(child会把child添加到BuildOwner_inactiveElements中)
-
如果new widget非空,child不为空
-
2.1 如果传入的widget和原来的widget是同一个,但是slot不一样,则调用updateSlotForChild更新位置
-
2.2 如果不是同一个widget,则判断widget的canUpdate是否是true,如果是true的话(代表element的可以重用),先判断slot是否和原来的slot相等,不相等,则调用updateSlotForChild更新位置。然后调用elemnt的update方法进行更新。
-
2.3 如果不符合上面2.1,2.2的情况(则代表element不可重用,newWidget需要用例外一个新的element),则调用deactivateChild(child)方法,并调用inflateWidget(newWidget, newSlot)产生新的child
- newWidget为空,child为空。则表明该Wiget还不存在Element,则调用inflateWidget去创建一个新的Element。
上面的1,2,3分别代表着删除,修改,增加 Elemnt子节点的三种情况。当对一个element(ComponetElemnt及其子类的实例)调用markNeedsBuild方法的时候,会调用到updateChild方法去更新该element。
对于步骤1,当一个Widget不再使用的时候,会调用deactivateChild方法,这个方法会把对应的Element放入到BuildlOwner的_inactiveElements列表中,如果Element被再次使用到(如使用了GlobalKey),就会从_inactiveElements列表中移除。如果没有被再次用到,再一次更新树的时候就会被销毁。
对于步骤2.2,elemnt的update方法只会简单的设置widget。具体的实现由各个子类实现。这一步可以buid方法可以沿下更新树
如StatelessElement会调用rebuild方法,RenderObjectElemnt会更新renderobjct。像SingleChildRenderObject和MutilChildRederObjectElemnt等有子elemnt的还会调用updateChild方法(MutilChildRederObjectElemnt 是调用updateChildren,但是updateChildren是对updateChild的一个包装,对传入的列表逐个调用updateChild)更新子elemnt,
更新流程下图所示
在说RenderObjctElemnt的时候,留了一个小疑问:如何更新有子节点的RenderObjcetElement的子树。
其实在开发过程中,我们是不会直接调用RenderObjcetElement的markNeedsBuild方法的。就拿Stack这个Wigdet来说。Stack继承自MultiChildRenderObjectWidget。MultiChildRenderObjectWidget对应的是MutilChildRederObjectElemnt。
但会发现,Stack是没有setState方法的,也就说不会直接调用markNeedsBuild方法。Stack一般是被ComponetElemnt及其子类对应的Widget(如StatefuleWidget)所包裹着。当对应的Widget更新的时候,就会调用markNeedsBuild方法。从而进入updateChild更新子树,从而完成对子树的更新。
假设你要在更新流程中,保留Stack,只删除Stack里的一个子节点,会走到updateChild的步骤2.2中,调用Stack对应的MutilChildRederObjectElemnt的update()方法。utilChildRederObjectElemnt的update()如下
@override
void update(MultiChildRenderObjectWidget newWidget) {
super.update(newWidget);
...
_children = updateChildren(_children, widget.children,
forgottenChildren: _forgottenChildren);
_forgottenChildren.clear();
}
}
可以看到,update方法调用了updateChildren方法,updateChildren这个方法会遍历子节点,对每一个子节点调用updateChild方法,从而完成子树的更新。
综合上面的流程可以看出,标记一个Element需要更新,其实就是调用markNeedsRebuild方法标记Element需要更新并通知到Flutter框架需要更细。当框架更新该Element的时候,会调用updateChildren方法向下递归更新子树,直到叶子节点为止。
三GlobalKey,ParentData,依赖更新等机制的实现原理
GloabalKey
在开发的过程中,在一个Widget可以通过GloabalKey去找到另外一个Widget,这是怎样实现的呢?
当我们调用GlobalKey的currentWidget去获取一个Widget的时候,调用的是下面的方法
Widget get currentWidget => _currentElement?.widget;
_currentElement方法定义如下
Element get _currentElement => _registry[this];
可这个_registry[this]是什么呢?
看一下GlobalKey的定义,会发现GlobalKey中这个类有一个类属性
static final Map<GlobalKey, Element> _registry = <GlobalKey, Element>{};
这个_registry以GlobalKey为键,以Element为值。他的作用是存储Widget的key是GloabalKey的Element
前面介绍到,在绑定Element的时候会调用mount方法 mount方法中还会判断对应的widget的key是否是GlobalKey,如果是GlobakKey,就会调用
key._register(this);
_register是GlobakKey中的一个方法
void _register(Element element) {
_registry[this] = element;
}
这个方法将GloabakKey和Element作为健值对存放在GlobalKey类中的_registry中。这样就完成一个GlobalKey和Element的绑定
_registry[this] 就是从GlobakKey类中的_registry中取出这一个GlobakKey对应的Element
这样看来,当我们调用GlobalKey的currentWidget去获取一个Widget的时候,实际上就是从GlobalKey这个类中的_registry取出Element,并返回Element的Widget。
只要GlobalKey对应的Element还存在,就可以通过获取到对应的Widget。
流程大概如下
ParentData
在RenderObjct树中,父RenerObjct负责子RenderObjct布局, 每一个RenderObjct中都有一个parentData属性,用于向父RenderObject提供布局信息,父RenderObject根据子RenderObjct的parentData进行布局。但是这个parentData是什么时候提供的呢?
其实RenderObecjElement通常是不会直接拥有一个RenderObecjElement类型的子节点,通常两个RenderObecjElement中间都会有一个ParentDataElemnt类型的中间节点,这是因为需要给子RenderObecjElement设置parentData的属性,而RenderObjct的parentData信息就是在这里设置的。
就拿Stack为列,Stack通常都不会直接嵌套一个的Elemment是RenderObjctElementl类型的子Widget。通常都会嵌套一个Position。而Positio继承自ParentDataWidget,而ParentDataWidget对应的Element就是ParentDataElemnt类型的。而用Stack和Position构建的Widget树对应的Element树如下图所示。
如果上图的Widget D对应的Element是RenderObjctElement类型的,按照树的构建流程,在绑定这个Widget的Element的时候,会调用attachRenderObject方法,attachRenderObject方法的实现在前面已经贴出来了。可以看到,除了构建RenderObjct树外,还做了寻找上级ParentData的操作 如下
final ParentDataElement<ParentData> parentDataElement =
_findAncestorParentDataElement();
if (parentDataElement != null) _updateParentData(parentDataElement.widget);
如果存在是ParentDataElement类型的上级Element,则调用_updateParentData方法并传入parentDataElement的widget. _updateParentData的方法如下
void _updateParentData(ParentDataWidget<ParentData> parentDataWidget) {
bool applyParentData = true;
...
if (applyParentData) parentDataWidget.applyParentData(renderObject);
}
这个_updateParentData中就做了一个事情,就是调用传入的Elementd的widget的applyParentData方法,并传入Elementd的renderObjct.
回到刚才的例子,在绑定Widget D的Element D得时候,Element D 会向上查找,得到ParentDataElement B,并通过ParentDataElement B找到对应的Positon B,调用Position B的applyParentData方法。Position中的applyParentData方法如下
@override
void applyParentData(RenderObject renderObject) {
assert(renderObject.parentData is StackParentData);
final StackParentData parentData =
renderObject.parentData as StackParentData;
bool needsLayout = false;
if (parentData.left != left) {
parentData.left = left;
needsLayout = true;
}
if (parentData.top != top) {
parentData.top = top;
needsLayout = true;
}
if (parentData.right != right) {
parentData.right = right;
needsLayout = true;
}
if (parentData.bottom != bottom) {
parentData.bottom = bottom;
needsLayout = true;
}
if (parentData.width != width) {
parentData.width = width;
needsLayout = true;
}
if (parentData.height != height) {
parentData.height = height;
needsLayout = true;
}
if (needsLayout) {
final AbstractNode targetParent = renderObject.parent;
if (targetParent is RenderObject) targetParent.markNeedsLayout();
}
}
这个方法中,首先会取到renderObject(在这个例子中就是 Element D的renderObjcet)的parentData,并根据Positon B的情况去更新parentData的数据和判断是否需要更新,如果需要更新,则调用renderObject的父节点,并告诉父节点需要重新布局,从而更新这个RenderObjct
流程如下
找到上级的ParentDataElement -> 调用ParentDataElement的widget的applyParentData方法 ->设置parentData ->通知父RenderObjct更新
Element树和RenderObjct树是紧密相连的,随着Element树的构建和更新,RenderObjct树也会随着改变。
这里虽然是只拿了Stack,Position做例子,但是RenderObjct树中的节点基本上都是依靠这一套机制实现布局信息的传递的。
依赖和更新
开发过程中,可以通过Theme.of(context)这样的方法去获取主题相关的设置。当我们使用了主题信息,而主题信息改变的时候,我们的Widget也会重新执行build方法。其实这个也是依赖于Element树的构建和更新去实现的。可以参照InheritedElement的实现。 这个点留个坑位,分析Provider的时候一起分析。
总结
从上面Element的构建和更新的流程中,我们可以看到其实都是通过递归调用,一级一级的往下去对节点进行修改。Flutter代码中会以嵌套的方式去写Widget,可以保证每个Widget都是最新的,从而确保Element树是正确的。
其实有一个地方需要注意的是,每一个Element和RenderParent都会有一个parent属性,用于指明父节点是谁,但是Widget是没有这个父节点的,也就是说Widget是没有上下级关系的。所以严格一点来说,Widget树并不是一个真正的节点树,只是说每一个Element都会对应一个Widget,而且从开发过程看,Widget看起来像是一颗树而已。
Element是核心,整个构建和更新流程是由BuildOwner的buildScope方法触发,由Element实现,Element协调Widget树和RenderObjct树的构建和更新。
结束语
在这一篇文章中主要讲述了Widget,Elemnet,RenderObjct树的构建和更新流程,至于RenderObjct的实现描述不多。 其实RenderObjct中如何去布局,绘制,layer生成,合成对于理解Flutter的界面构建也是一个很重要的部分,篇幅也较大,留给下一篇文章再去讲述
转载自:https://juejin.cn/post/6952076273891934221