Widget,Element,RenderObject树的构建和更新流程

void updateRenderObject(BuildContext context, RenderObject renderObject) { }
RenderObjectToWidgetElement attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement 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方法只做了几个简单的事情

  1. 更新_parent值为传入的值
  2. 更新_slot,_depth,_activie。_slot是一个位置标记,_depth是该Elemnt在Elemnt树中的深度,_activit是指该Elemnt的状态
  3. 更新这个Elemnt的owner为父Elemnt的owner。因为管理树只需要一个BuildOwner就可以了,根Elemnt的owner属性会被赋予一个BuildOwner,子Element只要使用这个值就可以了
  4. 注册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()方法的实现

  1. StatelessElement

Widget build() => widget.build(this);

  1. StatefulElement

Widget build() => _state.build(this);

  1. 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 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(widget.children.length);
Element previousChild;
for (int i = 0; i < _children.length; i += 1) {
final Element newChild = inflateWidget(
widget.children[i], IndexedSlot(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方法,一直往下创建节点,直到没有子节点的叶子节点为止。如下图

Widget ,Elemnt构建流程.jpg

三 树的更新过程

更新流程

当构建完树后,是如何对树进行增加,删除,修改树中节点的操作呢?前面提到过,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过程中根据传入的参数做了以下的一些处理。

  1. 如果new widget为空,但是element不为空(也就是原有的widget被删除了)。首先deactivateChild(child),如果child不为空,则解绑child的renderobjce,并添加到build owner中的_inactiveElements列表中,并返回函数。deactivateChild(child会把child添加到BuildOwner_inactiveElements中)

  2. 如果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

  1. 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,

更新流程下图所示

Widget ,Elemnt更新流程.jpg

在说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;

最后

对于程序员来说,要学习的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

以下是今天给大家分享的一些独家干货:

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
的知识内容、技术有太多太多,要想不被环境淘汰就只有不断提升自己,从来都是我们去适应环境,而不是环境来适应我们!

最后,我再重复一次,如果你想成为一个优秀的 Android 开发人员,请集中精力,对基础和重要的事情做深度研究

对于很多初中级Android工程师而言,想要提升技能,往往是自己摸索成长,不成体系的学习效果低效漫长且无助。整理的这些架构技术希望对Android开发的朋友们有所参考以及少走弯路,本文的重点是你有没有收获与成长,其余的都不重要,希望读者们能谨记这一点。

为了大家能够顺利进阶中高级、架构师,我特地为大家准备了一套高手学习的源码和框架视频等精品Android架构师教程,保证你学了以后保证薪资上升一个台阶。

以下是今天给大家分享的一些独家干货:

[外链图片转存中…(img-yDd7RCPK-1715601739332)]

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值