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

  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;

_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 parentDataElement =
_findAncestorParentDataElement();
if (parentDataElement != null) _updateParentData(parentDataElement.widget);

如果存在是ParentDataElement类型的上级Element,则调用_updateParentData方法并传入parentDataElement的widget. _updateParentData的方法如下

void _updateParentData(ParentDataWidget 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的界面构建也是一个很重要的部分,篇幅也较大,留给下一篇文章再去讲述

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

本文讲解了我对Android开发现状的一些看法,也许有些人会觉得我的观点不对,但我认为没有绝对的对与错,一切交给时间去证明吧!愿与各位坚守的同胞们互相学习,共同进步!

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

外链图片转存中…(img-jNxzwmvU-1712302897036)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

总结

本文讲解了我对Android开发现状的一些看法,也许有些人会觉得我的观点不对,但我认为没有绝对的对与错,一切交给时间去证明吧!愿与各位坚守的同胞们互相学习,共同进步!

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 21
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值