百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

Flutter框架分析(三)-- Element(flutter架构设计)

ccwgpt 2024-09-15 14:56 27 浏览 0 评论

1. 前言

上一篇文章讲到,Widget是描述一个UI元素的配置数据,Element才真正代表屏幕显示元素,是某个位置的Widget生成的实例。本篇文章则主要介绍Element的主要功能。

通过上篇文章介绍的Widget TreeFlutter Framework会生成一系列Element,这些Element构成了Element Tree,其主要功能如下:

  • 维护这棵Element Tree,根据Widget Tree的变化来更新Element Tree,包括:节点的插入、更新、删除、移动等;
  • WidgetRenderObject关联到Element Tree上。

2. Element分类

如上图所示,Element从功能上看,可以分为两大类:

  • ComponentElement

组合类Element。这类Element主要用来组合其他更基础的Element,得到功能更加复杂的Element。开发时常用到的StatelessWidgetStatefulWidget相对应的ElementStatelessElementStatefulElement,即属于ComponentElement

  • RenderObjectElement

渲染类Element,对应Renderer Widget,是框架最核心的ElementRenderObjectElement主要包括LeafRenderObjectElementSingleChildRenderObjectElement,和MultiChildRenderObjectElement。其中,LeafRenderObjectElement对应的WidgetLeafRenderObjectWidget,没有子节点;SingleChildRenderObjectElement对应的WidgetSingleChildRenderObjectWidget,有一个子节点;MultiChildRenderObjectElement对应的WidgetMultiChildRenderObjecWidget,有多个子节点。

3. Element生命周期

Element有4种状态:initial,active,inactive,defunct。其对应的意义如下:

  • initial:初始状态,Element刚创建时就是该状态。
  • active:激活状态。此时ElementParent已经通过mount将该Element插入Element Tree的指定的插槽处(Slot),Element此时随时可能显示在屏幕上。
  • inactive:未激活状态。当Widget Tree发生变化,Element对应的Widget发生变化,同时由于新旧WidgetKey或者的RunTimeType不匹配等原因导致该Element也被移除,因此该Element的状态变为未激活状态,被从屏幕上移除。并将该ElementElement Tree中移除,如果该Element有对应的RenderObject,还会将对应的RenderObjectRender Tree移除。但是,此Element还是有被复用的机会,例如通过GlobalKey进行复用。
  • defunct:失效状态。如果一个处于未激活状态的Element在当前帧动画结束时还是未被复用,此时会调用该Element的unmount函数,将Element的状态改为defunct,并对其中的资源进行清理。

Element4种状态间的转换关系如下图所示:

4. ComponentElement

4.1 与核心元素关系

如上文所述,ComponentElement分为StatelessElementStatefulElement,这两种Element同核心元素Widget以及State之间的关系如下图所示。

如图:

  • ComponentElement持有Parent ElementChild Element,由此构成Element Tree.
  • ComponentElement持有其对应的Widget,对于StatefulElement,其还持有对应的State,以此实现ElementWidget之间的绑定。
  • State是被StatefulElement持有,而不是被StatefulWidget持有,便于State的复用。事实上,StateStatefulElement是一一对应的,只有在初始化StatefulElement时,才会初始化对应的State并将其绑定到StatefulElement上。

4.2 核心流程

一个Element的核心操作流程有,创建、更新、销毁三种,下面将分别介绍这三个流程。

  • 创建

ComponentElement的创建起源与父Widget调用inflateWidget,然后通过mount将该Element挂载至Element Tree,并递归创建子节点。

  • 更新

由父Element执行更新子节点的操作(updateChild),由于新旧Widget的类型和Key均未发生变化,因此触发了Element的更新操作,并通过performRebuild将更新操作传递下去。其核心函数updateChild之后会详细介绍。

  • 销毁

由父Element或更上级的节点执行更新子节点的操作(updateChild),由于新旧Widget的类型或者Key发生变化,或者新Widget被移除,因此导致该Element被转为未激活状态,并被加入未激活列表,并在下一帧被失效。

4.3 核心函数

下面对ComponentElement中的核心方法进行介绍。

  • inflateWidget
Element inflateWidget(Widget newWidget, dynamic newSlot) {
  final Key key = newWidget.key;
//复用GlobalKey对应的Element
  if (key is GlobalKey) {
    final Element newChild = _retakeInactiveElement(key, newWidget);
    if (newChild != null) {
      newChild._activateWithParent(this, newSlot);
      final Element updatedChild = updateChild(newChild, newWidget, newSlot);
      return updatedChild;
    }
  }
//创建Element,并挂载至Element Tree
  final Element newChild = newWidget.createElement();
  newChild.mount(this, newSlot);
  return newChild;
}

inflateWidget的主要职责如下:

  1. 判断新Widget是否有GlobalKey,如果有GlobalKey,则从Inactive Elements列表中找到对应的Element并进行复用。
  2. 无可复用Element,则根据新Widget创建对应的Element,并将其挂载至Element Tree
  • mount
void mount(Element parent, dynamic newSlot) {
//更新_parent等属性,将元素加入Element Tree
  _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;
//注册GlobalKey
  final Key key = widget.key;
  if (key is GlobalKey) {
    key._register(this);
  }
  _updateInheritance();
}

Element第一次被插入Element Tree的时候,该方法被调用。其主要职责如下:

  1. 将给Element加入Element Tree,更新_parent,_slot等树相关的属性。
  2. 如果新WidgetGlobalKey,将该Element注册进GlobalKey中,其作用下文会详细分析。
  3. ComponentElement的mount函数会调用_firstBuild函数,触发子Widget的创建和更新。
  • performRebuild
@override
void performRebuild() {
//调用build函数,生成子Widget
  Widget built;
  built = build();
//根据新的子Widget更新子Element
  _child = updateChild(_child, built, slot);
}

performRebuild的主要职责如下:

  1. 调用build函数,生成子Widget
  2. 根据新的子Widget更新子Element
  • update
@mustCallSuper
void update(covariant Widget newWidget) {
  _widget = newWidget;
}

此函数主要职责为:

  1. 将对应的Widget更新为新的Widget
  2. ComponentElement的各种子类中,还会调用rebuild函数触发对子Widget的重建。
  • updateChild
@protected
Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
  if (newWidget == null) {
//新的Child Widget为null,则返回null;如果旧Child Widget,使其未激活
    if (child != null)
      deactivateChild(child);
    return null;
  }
  Element newChild;
  if (child != null) {
//新的Child Widget不为null,旧的Child Widget也不为null
    bool hasSameSuperclass = true;
    if (hasSameSuperclass && child.widget == newWidget) {
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      newChild = child;
    } else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)){
//Key和RuntimeType相同,使用update更新
      if (child.slot != newSlot)
        updateSlotForChild(child, newSlot);
      child.update(newWidget);
      newChild = child;
    } else {
//Key或RuntimeType不相同,使旧的Child Widget未激活,并对新的Child Widget使用inflateWidget
      deactivateChild(child);
      newChild = inflateWidget(newWidget, newSlot);
    }
  } else {
//新的Child Widget不为null,旧的Child Widget为null,对新的Child Widget使用inflateWidget
    newChild = inflateWidget(newWidget, newSlot);
  }

  return newChild;
}

该方法的主要职责为:

根据新的子Widget,更新旧的子Element,或者得到新的子Element。其核心逻辑可以用表格表示:


newWidget == null

newWidget != null

Child == null

返回null

返回新Element

Child != null

移除旧的子Element,返回null

如果Widget能更新,

更新旧的子Element,并返回之;否则创建新的子Element并返回。

该逻辑概括如下:

  • 如果newWidget为null,则返回null,同时如果有旧的子Element则移除之。
  • 如果newWidget不为null,旧Child为null,则创建新的子Element,并返回之。
  • 如果newWidget不为null,旧Child不为null,新旧子WidgetKeyRuntimeType等都相同,则调用update方法更新子Element并返回之。
  • 如果newWidget不为null,旧Child不为null,新旧子WidgetKeyRuntimeType等不完全相同,则说明Widget Tree有变动,此时移除旧的子Element,并创建新的子Element,并返回之。

5. RenderObjectElement

5.1 与核心元素关系

RenderObjectElement同核心元素WidgetRenderObject之间的关系如下图所示:

如图:

  • RenderObjectElement持有Parent Element,但是不一定持有Child Element,有可能无Child Element,有可能持有一个Child ElementChild),有可能持有多个Child Element(Children)。
  • RenderObjectElement持有对应的WidgetRenderObject,将WidgetRenderObject串联起来,实现了WidgetElementRenderObject之间的绑定。

5.2 核心流程

ComponentElement一样,RenderObjectElement的核心操作流程有,创建、更新、销毁三种,接下来会详细介绍这三种流程。

  • 创建

RenderObjectElement的创建流程和ComponentElement的创建流程基本一致,其最大的区别是ComponentElement在mount后,会调用build来创建子Widget,而RenderObjectElement则是create和attach其RenderObject

  • 更新

RenderObjectElement的更新流程和ComponentElement的更新流程也基本一致,其最大的区别是ComponentElement的update函数会调用build函数,重新触发子Widget的构建,而RenderObjectElement则是调用updateRenderObject对绑定的RenderObject进行更新。

  • 销毁

RenderObjectElement的销毁流程和ComponentElement的销毁流程也基本一致。也是由父Element或更上级的节点执行更新子节点的操作(updateChild),导致该Element被停用,并被加入未激活列表,并在下一帧被失效。其不一样的地方是在unmount Element的时候,会调用didUnmountRenderObject失效对应的RenderObject

5.3 核心函数

下面对RenderObjectElement中的核心方法进行介绍。

  • inflateWidget

该函数和ComponentElement的inflateWidget函数完全一致,此处不再复述。

  • mount
void mount(Element parent, dynamic newSlot) {
  super.mount(parent, newSlot);
  _renderObject = widget.createRenderObject(this);
  attachRenderObject(newSlot);
  _dirty = false;
}

该函数的调用时机和ComponentElement的一致,当Element第一次被插入Element Tree的时候,该方法被调用。其主要职责也和ComponentElement的一致,此处只列举不一样的职责,职责如下:

  1. 调用createRenderObject创建RenderObject,并使用attachRenderObject将RenderObject关联到Element上。
  2. SingleChildRenderObjectElement会调用updateChild更新子节点,MultiChildRenderObjectElement会调用每个子节点的inflateWidget重建所有子Widget
  • performRebuild
@override
void performRebuild() {
//更新renderObject
  widget.updateRenderObject(this, renderObject);
  _dirty = false;
}

performRebuild的主要职责如下:

调用updateRenderObject更新对应的RenderObject

  • update
@override
void update(covariant RenderObjectWidget newWidget) {
  super.update(newWidget);
  widget.updateRenderObject(this, renderObject);
  _dirty = false;
}

update的主要职责如下:

  1. 将对应的Widget更新为新的Widget

2) 调用updateRenderObject更新对应的RenderObject

  • updateChild

该函数和ComponentElement的inflateWidget函数完全一致,此处不再复述。

  • updateChildren
@protected
List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element> forgottenChildren }) {
  int newChildrenTop = 0;
  int oldChildrenTop = 0;
  int newChildrenBottom = newWidgets.length - 1;
  int oldChildrenBottom = oldChildren.length - 1;

  final List<Element> newChildren = oldChildren.length == newWidgets.length ?
      oldChildren : List<Element>(newWidgets.length);

  Element previousChild;

// 从顶部向下更新子Element
  // Update the top of the list.
  while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
    final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
    final Widget newWidget = newWidgets[newChildrenTop];
    if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
      break;
    final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
    newChildren[newChildrenTop] = newChild;
    previousChild = newChild;
    newChildrenTop += 1;
    oldChildrenTop += 1;
  }

// 从底部向上扫描子Element
  // Scan the bottom of the list.
  while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
    final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
    final Widget newWidget = newWidgets[newChildrenBottom];
    if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
      break;
    oldChildrenBottom -= 1;
    newChildrenBottom -= 1;
  }

// 扫描旧的子Element列表里面中间的子Element,保存Widget有Key的Element到oldKeyChildren,其他的失效
  // Scan the old children in the middle of the list.
  final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
  Map<Key, Element> oldKeyedChildren;
  if (haveOldChildren) {
    oldKeyedChildren = <Key, Element>{};
    while (oldChildrenTop <= oldChildrenBottom) {
      final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
      if (oldChild != null) {
        if (oldChild.widget.key != null)
          oldKeyedChildren[oldChild.widget.key] = oldChild;
        else
          deactivateChild(oldChild);
      }
      oldChildrenTop += 1;
    }
  }

// 根据Widget的Key更新oldKeyChildren中的Element。
  // Update the middle of the list.
  while (newChildrenTop <= newChildrenBottom) {
    Element oldChild;
    final Widget newWidget = newWidgets[newChildrenTop];
    if (haveOldChildren) {
      final Key key = newWidget.key;
      if (key != null) {
        oldChild = oldKeyedChildren[key];
        if (oldChild != null) {
          if (Widget.canUpdate(oldChild.widget, newWidget)) {
            // we found a match!
            // remove it from oldKeyedChildren so we don't unsync it later
            oldKeyedChildren.remove(key);
          } else {
            // Not a match, let's pretend we didn't see it for now.
            oldChild = null;
          }
        }
      }
    }

    final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
    newChildren[newChildrenTop] = newChild;
    previousChild = newChild;
    newChildrenTop += 1;
  }

  newChildrenBottom = newWidgets.length - 1;
  oldChildrenBottom = oldChildren.length - 1;

  // 从下到上更新底部的Element。.
  while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
    final Element oldChild = oldChildren[oldChildrenTop];
    final Widget newWidget = newWidgets[newChildrenTop];
    final Element newChild = updateChild(oldChild, newWidget, IndexedSlot<Element>(newChildrenTop, previousChild));
    newChildren[newChildrenTop] = newChild;
    previousChild = newChild;
    newChildrenTop += 1;
    oldChildrenTop += 1;
  }

// 清除旧子Element列表中其他所有剩余Element
  // Clean up any of the remaining middle nodes from the old list.
  if (haveOldChildren && oldKeyedChildren.isNotEmpty) {
    for (final Element oldChild in oldKeyedChildren.values) {
      if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
        deactivateChild(oldChild);
    }
  }

  return newChildren;
}

该函数的主要职责如下:

  1. 复用能复用的子节点,并调用updateChild对子节点进行更新。
  2. 对不能更新的子节点,调用deactivateChild对该子节点进行失效。

其步骤如下:

  1. 从顶部向下更新子Element
  2. 从底部向上扫描子Element
  3. 扫描旧的子Element列表里面中间的子Element,保存WidgetKeyElement到oldKeyChildren,其他的失效。
  4. 对于新的子Element列表,如果其对应的WidgetKey和oldKeyChildren中的Key相同,更新oldKeyChildren中的Element
  5. 从下到上更新底部的Element
  6. 清除旧子Element列表中其他所有剩余Element

6. 小结

本文主要介绍了Element相关知识,重点介绍了其分类,生命周期,和核心函数。重点如下:

  • 维护Element Tree,根据Widget Tree的变化来更新Element Tree,包括:节点的插入、更新、删除、移动等;并起到纽带的作用,将Widget以及RenderObject关联到Element Tree上。
  • Element分为ComponentElementRenderObjectElement,前者负责组合子Element,后者负责渲染。
  • Element的主要复用和更新逻辑由其核心函数updateChild实现,具体逻辑见上。

7. 相关文章

Flutter框架分析(一)——架构总览

Flutter框架分析(二)-- Widget

相关推荐

团队管理“布阵术”:3招让你的团队战斗力爆表!

为何古代军队能够以一当十?为何现代企业有的团队高效似“特种部队”,有的却松散若“游击队”?**答案正隐匿于“布阵术”之中!**今时今日,让我们从古代兵法里萃取3个核心要义,助您塑造一支战斗力爆棚的...

知情人士回应字节大模型团队架构调整

【知情人士回应字节大模型团队架构调整】财联社2月21日电,针对原谷歌DeepMind副总裁吴永辉加入字节跳动后引发的团队调整问题,知情人士回应称:吴永辉博士主要负责AI基础研究探索工作,偏基础研究;A...

豆包大模型团队开源RLHF框架,训练吞吐量最高提升20倍

强化学习(RL)对大模型复杂推理能力提升有关键作用,但其复杂的计算流程对训练和部署也带来了巨大挑战。近日,字节跳动豆包大模型团队与香港大学联合提出HybridFlow。这是一个灵活高效的RL/RL...

创业团队如何设计股权架构及分配(创业团队如何设计股权架构及分配方案)

创业团队的股权架构设计,决定了公司在随后发展中呈现出的股权布局。如果最初的股权架构就存在先天不足,公司就很难顺利、稳定地成长起来。因此,创业之初,对股权设计应慎之又慎,避免留下巨大隐患和风险。两个人如...

消息称吴永辉入职后引发字节大模型团队架构大调整

2月21日,有消息称前谷歌大佬吴永辉加入字节跳动,并担任大模型团队Seed基础研究负责人后,引发了字节跳动大模型团队架构大调整。多名原本向朱文佳汇报的算法和技术负责人开始转向吴永辉汇报。简单来说,就是...

31页组织效能提升模型,经营管理团队搭建框架与权责定位

分享职场干货,提升能力!为职场精英打造个人知识体系,升职加薪!31页组织效能提升模型如何拿到分享的源文件:请您关注本头条号,然后私信本头条号“文米”2个字,按照操作流程,专人负责发送源文件给您。...

异形柱结构(异形柱结构技术规程)

下列关于混凝土异形柱结构设计的说法,其中何项正确?(A)混凝土异形柱框架结构可用于所有非抗震和抗震设防地区的一般居住建筑。(B)抗震设防烈度为6度时,对标准设防类(丙类)采用异形柱结构的建筑可不进行地...

职场干货:金字塔原理(金字塔原理实战篇)

金字塔原理的适用范围:金字塔原理适用于所有需要构建清晰逻辑框架的文章。第一篇:表达的逻辑。如何利用金字塔原理构建基本的金字塔结构受众(包括读者、听众、观众或学员)最容易理解的顺序:先了解主要的、抽象的...

底部剪力法(底部剪力法的基本原理)

某四层钢筋混凝土框架结构,计算简图如图1所示。抗震设防类别为丙类,抗震设防烈度为8度(0.2g),Ⅱ类场地,设计地震分组为第一组,第一自振周期T1=0.55s。一至四层的楼层侧向刚度依次为:K1=1...

结构等效重力荷载代表值(等效重力荷载系数)

某五层钢筋混凝土框架结构办公楼,房屋高度25.45m。抗震设防烈度8度,设防类别丙类,设计基本地震加速度0.2g,设计地震分组第二组,场地类别为Ⅱ类,混凝土强度等级C30。该结构平面和竖向均规则。假定...

体系结构已成昭告后世善莫大焉(体系构架是什么意思)

实践先行也理论已初步完成框架结构留余后人后世子孙俗话说前人栽树后人乘凉在夏商周大明大清民国共和前人栽树下吾之辈已完成结构体系又俗话说青出于蓝而胜于蓝各个时期任务不同吾辈探索框架结构体系经历有限肯定发展...

框架柱抗震构造要求(框架柱抗震设计)

某现浇钢筋混凝土框架-剪力墙结构高层办公楼,抗震设防烈度为8度(0.2g),场地类别为Ⅱ类,抗震等级:框架二级,剪力墙一级,混凝土强度等级:框架柱及剪力墙C50,框架梁及楼板C35,纵向钢筋及箍筋均采...

梁的刚度、挠度控制(钢梁挠度过大会引起什么原因)

某办公楼为现浇钢筋混凝土框架结构,r0=1.0,混凝土强度等级C35,纵向钢筋采用HRB400,箍筋采用HPB300。其二层(中间楼层)的局部平面图和次梁L-1的计算简图如图1~3(Z)所示,其中,K...

死要面子!有钱做大玻璃窗,却没有钱做“柱和梁”,不怕房塌吗?

活久见,有钱做2层落地大玻璃窗,却没有钱做“柱子和圈梁”,这样的农村自建房,安全吗?最近刷到个魔幻施工现场,如下图,这栋5开间的农村自建房,居然做了2个全景落地窗仔细观察,这2个落地窗还是飘窗,为了追...

不是承重墙,物业也不让拆?话说装修就一定要拆墙才行么

最近发现好多朋友装修时总想拆墙“爆改”空间,别以为只要避开承重墙就能随便砸!我家楼上邻居去年装修,拆了阳台矮墙想扩客厅,结果物业直接上门叫停。后来才知道,这种配重墙拆了会让阳台承重失衡,整栋楼都可能变...

取消回复欢迎 发表评论: