Flutter-渲染原理&三棵树详解
  xx2YH4ad7R0N 2023年11月02日 57 0


一. 渲染原理

WidgetTree:存放渲染内容、它只是一个配置数据结构,创建是非常轻量的,在页面刷新的过程中随时会重建

Element 是分离 WidgetTree 和真正的渲染对象的中间层, WidgetTree 用来描述对应的Element 属性,同时持有Widget和RenderObject,存放上下文信息,通过它来遍历视图树,支撑UI结构。

RenderObject (渲染树)用于应用界面的布局和绘制,负责真正的渲染,保存了元素的大小,布局等信息,实例化一个 RenderObject 是非常耗能的

当应用启动时 Flutter 会遍历并创建所有的 Widget 形成 Widget Tree,通过调用 Widget 上的 createElement() 方法创建每个 Element 对象,形成 Element Tree。最后调用 Element 的 createRenderObject() 方法创建每个渲染对象,形成一个 Render Tree。

那么,flutter为什么要设计成这样呢?为什么要弄成复杂的三层结构?

答案是性能优化。如果每一点细微的操作就去完全重绘一遍UI,将带来极大的性能开销。flutter的三棵树型模式设计可以有效地带来性能提升。

widget的重建开销非常小,所以可以随意的重建,因为它不一会导致页面重绘,并且它也不一定会常常变化。 而renderObject如果频繁创建和销毁成本就很高了,对性能的影响比较大,因此它会缓存所有页面元素,只是当这些元素有变化时才去重绘页面。

而判断页面有无变化就依靠element了,每次widget变化时element会比较前后两个widget,只有当某一个位置的Widget和新Widget不一致,才会重新创建Element和widget;其他时候则只会修改renderObject的配置而不会进行耗费性能的RenderObject的实例化工作了。

课题笔记

Widget的渲染原理

  • 所有的Widget都会创建一个Element对象
  • 并不是所有的Widget都会被独立渲染!只有继承RenderObjectWidget的才会创建RenderObject对象!(Container就不会创建RenderObject、column和padding这些可以创建RenderObject)
  • 在Flutter渲染的流程中,有三颗重要的树!Flutter引擎是针对Render树进行渲染!
  • Widget树、Element树、Render树
  • 每一个Widget都会创建一个Element对象
  • 隐式调用createElement方法。Element加入Element树中,它会创建RenderElement、ComponentElement(又分为StatefulElement和StatelessElement)。
  • RenderElement主要是创建RenderObject对象, 继承RenderObjectWidget的Widget会创建RenderElement
  • 创建RanderElement
  • Flutter会调用mount方法,调用createRanderObject方法
  • StatefulElement继承ComponentElement,StatefulWidget会创建StatefulElement
  • 调用createState方法,创建State
  • 将Widget赋值给state
  • 调用state的build方法 并且将自己(Element)传出去,build里面的context 就是Widget的Element !
  • StatelessElement继承ComponentElement,StatelessWidget会创建StatelessElement
  • mount方法 -> firstBuild -> rebuild -> performBuild -> build -> _widget.build

-主要就是调用build方法 并且将自己(Element)传出去

1. widget

Widget从功能上看,可以分为三大类:

  • Component Widget

组合类Widget。这类Widget主要用来组合其他更基础的Widget,得到功能更加复杂的Widget。平常的业务开发一般用的就是此类Widget

  • Render Widget

渲染类Widget,这类Widget是框架最核心的Widget,会参与后面的布局和渲染流程;只有这种类型的Widget会绘制到屏幕上。

  • Proxy Widget

代理类Widget,其本身并不涉及Widget内部逻辑,只是为子Widget提供一些附加的中间功能。例如:InheritedWidget用于将一些状态信息传递给子孙Widget

2.Element

Flutter-渲染原理&三棵树详解_ui

Element从功能上看,可以分为两大类:

  • ComponentElement

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

  • RenderObjectElement

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

2.1 ComponentElement

2.1.1 与核心元素关系

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

Flutter-渲染原理&三棵树详解_ui_02

如图:

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

2.1.2 ComponentElement核心流程

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

  • 创建

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

Flutter-渲染原理&三棵树详解_flutter_03

  • 更新

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

Flutter-渲染原理&三棵树详解_子节点_04

  • 销毁

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

Flutter-渲染原理&三棵树详解_flutter_05

2.1.3 ComponentElement核心函数

下面对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,并返回之。

2.2 RenderObjectElement

2.2.1 RenderObjectElement与核心元素关系

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

Flutter-渲染原理&三棵树详解_flutter_06

如图:

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

2.2.2 RenderObjectElement核心流程

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

  • 创建

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

Flutter-渲染原理&三棵树详解_子节点_07

  • 更新

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

Flutter-渲染原理&三棵树详解_ui_08

  • 销毁

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

Flutter-渲染原理&三棵树详解_子节点_09

2.2.3 RenderObjectElement核心函数

下面对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

2.1.3 Element小结

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

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

3. RenderObject

通过上篇文章介绍的Element TreeFlutter Framework会生成一棵RenderObject Tree. 其主要功能如下:

  • 布局,从RenderBox开始,对RenderObject Tree从上至下进行布局。
  • 绘制,通过Canvas对象,RenderObject可以绘制自身以及其在RenderObject Tree中的子节点。
  • 点击测试,RenderObject从上至下传递点击事件,并通过其位置和behavior来控制是否响应点击事件。

RenderObject Tree是底层的布局和绘制系统。大多数Flutter开发者并不需要直接和RenderObject Tree交互,而是使用Widget,然后Flutter Framework会自动构建RenderObject Tree

3.1 RenderObject分类

Flutter-渲染原理&三棵树详解_flutter_10

如上图所示,RenderObject主要分为四类:

  • RenderView

RenderView是整个RenderObject Tree的根节点,代表了整个输出界面。

  • RenderAbstractViewport

RenderAbstractViewport是一类接口,此类接口为只展示其部分内容的RenderObject设计。

  • RenderSliver

RenderSliver是所有实现了滑动效果的RenderObject基类,其常用子类有RenderSliverSingleBoxAdapter等。

  • RenderBox

RenderBox是一个采用2D笛卡尔坐标系的RenderObject的基类,一般的RenderOBject都是继承自RenderBox,例如RenderStack等,它也是一般自定义RenderObject的基类。

3.2 RenderObject核心流程

RenderObject主要负责布局,绘制,及命中测试,下面会对这几个核心流程分别进行讲解。

  • 布局

布局对应的函数是layout,该函数主要作用是通过上级节点传过来的ConstraintsparentUsesSize等控制参数,对本节点和其子节点进行布局。Constraints是对于节点布局的约束,其原则是,Constraints向下,Sizes向上,父节点设置本节点的位置。即:

  1. 一个Widget从它的父节点获取Constraints,并将其传递给子节点。
  2. Widget对其子节点进行布局。
  3. 最终,该节点告诉其父节点它的Sizes

在接下来的文章中,我们将对该流程进行详细介绍,当前我们只需要记住该原则。

当本节点的布局依赖于其子节点的布局时,parentUsesSize的值是true,此时,子节点被标记为需要布局时,本节点也将被标记为需要布局。这样当下一帧绘制时本节点和子节点都将被重新布局。反之,如果parentUsesSize的值是false,子节点被重新布局时不需要通知本节点。

RenderObject的子类不应该直接重写RenderObject的layout函数,而是重写performResizeperformLayout函数,这两个函数才是真正负责具体布局的函数。

RenderObjectlayout函数的源码如下:

void layout(Constraints constraints, { bool parentUsesSize = false }) {
//1. 根据relayoutBoundary判断是否需要重新布局
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
relayoutBoundary = (parent as RenderObject)._relayoutBoundary;
}
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
_constraints = constraints;
//2. 更新子节点的relayout boundary
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
// The local relayout boundary has changed, must notify children in case
// they also need updating. Otherwise, they will be confused about what
// their actual relayout boundary is later.
visitChildren(_cleanChildRelayoutBoundary);
}
_relayoutBoundary = relayoutBoundary;
//3. 重新计算大小,重新布局
if (sizedByParent) {
try {
performResize();
} catch (e, stack) {
_debugReportException('performResize', e, stack);
}
}
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
}
_needsLayout = false;
markNeedsPaint();
}

从源码可以看到,relayoutBoundarylayout函数中一个重要参数。当一个组件的大小被改变时,其parent的大小可能也会被影响,因此需要通知其父节点。如果这样迭代上去,需要通知整棵RenderObject Tree重新布局,必然会影响布局效率。因此,Flutter通过relayoutBoundaryRenderObject Tree分段,如果遇到了relayoutBoundary,则不去通知其父节点重新布局,因为其大小不会影响父节点的大小。这样就只需要对RenderObject Tree中的一段重新布局,提高了布局效率。关于relayoutBoundary将在之后的文章中详细讲解,目前只需要了解relayoutBoundary会将RenderObject Tree分段,提高布局效率。

  • 绘制

绘制对应的函数是paint,其主要作用是将本RenderObject和子RenderObject绘制在Canvas上。RenderObject的子类应该重写这个函数,在该函数中添加绘制的逻辑。

RenderObject的子类RenderFlexpaint函数源码如下:

void paint(PaintingContext context, Offset offset) {
//1. 未溢出,直接绘制
if (!_hasOverflow) {
defaultPaint(context, offset);
return;
}

//2. 空的,不需要绘制
// There's no point in drawing the children if we're empty.
if (size.isEmpty)
return;

//3. 根据clipBehavior判断是否需要对溢出边界部分进行裁剪
if (clipBehavior == Clip.none) {
defaultPaint(context, offset);
} else {
// We have overflow and the clipBehavior isn't none. Clip it.
context.pushClipRect(needsCompositing, offset, Offset.zero & size, defaultPaint, clipBehavior: clipBehavior);
}

//4. 绘制溢出错误提示
assert(() {
// Only set this if it's null to save work. It gets reset to null if the
// _direction changes.
final List<DiagnosticsNode> debugOverflowHints = <DiagnosticsNode>[
ErrorDescription(
'The overflowing $runtimeType has an orientation of $_direction.'
),
ErrorDescription(
'The edge of the $runtimeType that is overflowing has been marked '
'in the rendering with a yellow and black striped pattern. This is '
'usually caused by the contents being too big for the $runtimeType.'
),
ErrorHint(
'Consider applying a flex factor (e.g. using an Expanded widget) to '
'force the children of the $runtimeType to fit within the available '
'space instead of being sized to their natural size.'
),
ErrorHint(
'This is considered an error condition because it indicates that there '
'is content that cannot be seen. If the content is legitimately bigger '
'than the available space, consider clipping it with a ClipRect widget '
'before putting it in the flex, or using a scrollable container rather '
'than a Flex, like a ListView.'
),
];

// Simulate a child rect that overflows by the right amount. This child
// rect is never used for drawing, just for determining the overflow
// location and amount.
Rect overflowChildRect;
switch (_direction) {
case Axis.horizontal:
overflowChildRect = Rect.fromLTWH(0.0, 0.0, size.width + _overflow, 0.0);
break;
case Axis.vertical:
overflowChildRect = Rect.fromLTWH(0.0, 0.0, 0.0, size.height + _overflow);
break;
}
paintOverflowIndicator(context, offset, Offset.zero & size, overflowChildRect, overflowHints: debugOverflowHints);
return true;
}());
}

这部分代码逻辑为,先判断是否溢出,没有溢出则调用defaultPaint完成绘制,再看是否为空,size是空的话直接返回,最后绘制溢出信息。

其中defaultPaint的源码如下:

void defaultPaint(PaintingContext context, Offset offset) {
ChildType child = firstChild;
while (child != null) {
final ParentDataType childParentData = child.parentData as ParentDataType;
context.paintChild(child, childParentData.offset + offset);
child = childParentData.nextSibling;
}
}

可见defaultPaint会调用paintChild绘制子节点,而如果子节点还有子节点,则paintChild最终又会调用到其paint然后调用到defaultPaint,从而形成循环递归调用,绘制整棵RenderObject Tree

  • 命中测试

命中测试是为了判断某个组件是否需要响应一个点击事件,其入口是RenderObject Tree的根节点RenderViewhitTest函数。下面是该函数的源码:

bool hitTest(HitTestResult result, { Offset position }) {
if (child != null)
child.hitTest(BoxHitTestResult.wrap(result), position: position);
result.add(HitTestEntry(this));
return true;
}

RenderView的构造函数可以看出,childRenderBox类,因此我们再看RenderBoxhitTest函数。

bool hitTest(BoxHitTestResult result, { @required Offset position }) {
if (_size.contains(position)) {
if (hitTestChildren(result, position: position) || hitTestSelf(position)) {
result.add(BoxHitTestEntry(this, position));
return true;
}
}
return false;
}

代码逻辑很简单,如果点击事件位置处于RenderObject之内,如果在其内,并且hitTestSelf或者hitTestChildren返回true,则表示该RenderObject通过了命中测试,需要响应事件,此时需要将被点击的RenderObject加入BoxHitTestResult列表,同时点击事件不再向下传递。否则认为没有通过命中测试,事件继续向下传递。其中,hitTestSelf函数表示本节点是否通过命中测试,hitTestChildren表示子节点是否通过命中测试。

3.3 RenderObject核心函数

RenderObject的核心函数有很多,难以一一列举,在核心流程中已经详细讲解了RenderObject三个核心函数。为了便于理解各个核心函数的作用,这里将RenderObject的核心函数和Android View的核心函数进行比较。以下是比较的表格。

作用

Flutter RenderObject

Android View

绘制

paint()

draw()/onDraw()

布局

performLayout()/layout()

measure()/onMeasure(), layout()/onLayout()

布局约束

Constraints

MeasureSpec

布局协议1

performLayout() 的 Constraints 参数表示父节点对子节点的布局限制

measure() 的两个参数表示父节点对子节点的布局限制

布局协议2

performLayout() 应调用各子节点的 layout()

onLayout() 应调用各子节点的 layout()

布局参数

parentData

mLayoutParams

请求布局

markNeedsLayout()

requestLayout()

请求绘制

markNeedsPaint()

invalidate()

添加 child

adoptChild()

addView()

移除 child

dropChild()

removeView()

关联到窗口/树

attach()

onAttachedToWindow()

从窗口/树取消关联

detach()

onDetachedFromWindow()

获取 parent

parent

getParent()

触摸事件

hitTest()

onTouch()

用户输入事件

handleEvent()

onKey()

旋转事件

rotate()

onConfigurationChanged()

可见,RenderObjectAndroid View有很多函数是对应起来的,RenderObject相对于将Android View中的布局渲染等功能单独拆了出来,简化了View的逻辑。

3.4. 小结

本文主要介绍了RenderObject相关知识,重点介绍了其分类,核心流程,和核心函数。重点如下:

  • RenderObject主要负责绘制,布局,命中测试等。
  • RenderObject布局的原则是,Constraints向下,Sizes向上,父节点设置本节点的位置。
  • RenderView是整个RenderObject Tree的根节点,其child是一个RenderBox类型的RenderObject

二、Flutter绘制流程及原理

系统启动时,runApp方法会被调用,flutter会从最外层的widget去遍历创建一颗widget树;每一个widget创建后会调用createElement()创建相应的element,形成一颗element树;element创建后会通过createRenderObject()创建相应的renderObject树,如此就形成了三棵树。

在渲染树种完成布局排列和绘制。最后合并层级,通过Skia引擎渲染为GPU数据,然后GPU接着将数据交给显示器显示。

而渲染对象树在Flutter的展示过程分为三个阶段:布局、绘制、合成和渲染。

布局:

Flutter采用深度优先机制遍历渲染对象树,决定渲染对象树中各渲染对象在屏幕上的位置和尺寸。在布局过程中,渲染对象树中的每个渲染对象都会接收父对象的布局约束参数,决定自己的大小,然后父对象按照控件逻辑决定各个子对象的位置,完成布局过程。

绘制:

布局完成后,渲染对象树中的每个节点都有了明确的尺寸和位置。Flutter会把所有的渲染对象绘制到不同的图层上。与布局过程一样,绘制过程也是深度优先遍历,而且总是先绘制自身,再绘制子节点。

图层合成:

终端设备的页面越来越复杂,因此Flutter的渲染树层级通常很多,直接交付给渲染引擎进行多图层渲染,可能会出现大量渲染内容的重复绘制,所以还需要先进行一次图层合成,即将所有的图层根据大小、层级、透明度等规则计算出最终的显示效果,将相同的图层归类合并,简化渲染树,提高渲染效率。

合并完成后,Flutter会将几何图层数据交由Skia引擎加工成二维图像数据,最终交由GPU进行渲染,完成界面的展示。

Flutter-渲染原理&三棵树详解_ui_11

Flutter-渲染原理&三棵树详解_ui_12

三. 对象的创建过程【参考内容】

上面已经介绍了三棵树的运作流程,这部分为参考内容(视频教程里边学的)

我们这里以Padding为例,Padding用来设置内边距

3.1. Widget

Padding是一个Widget,并且继承自SingleChildRenderObjectWidget

继承关系如下:

Padding -> SingleChildRenderObjectWidget -> RenderObjectWidget -> Widget

我们之前在创建Widget时,经常使用StatelessWidget和StatefulWidget,这种Widget只是将其他的Widget在build方法中组装起来,并不是一个真正可以渲染的Widget(在之前的课程中其实有提到)。

在Padding的类中,我们找不到任何和渲染相关的代码,这是因为Padding仅仅作为一个配置信息,这个配置信息会随着我们设置的属性不同,频繁的销毁和创建。

问题:频繁的销毁和创建会不会影响Flutter的性能呢?

  • 并不会,答案在我的另一篇文章中;
  • mp.weixin.qq.com/s/J4XoXJHJS…

那么真正的渲染相关的代码在哪里执行呢?

  • RenderObject

3.2. RenderObject

我们来看Padding里面的代码,有一个非常重要的方法:

  • 这个方法其实是来自RenderObjectWidget的类,在这个类中它是一个抽象方法;
  • 抽象方法是必须被子类实现的,但是它的子类SingleChildRenderObjectWidget也是一个抽象类,所以可以不实现父类的抽象方法
  • 但是Padding不是一个抽象类,必须在这里实现对应的抽象方法,而它的实现就是下面的实现
@override
RenderPadding createRenderObject(BuildContext context) {
return RenderPadding(
padding: padding,
textDirection: Directionality.of(context),
);
}

上面的代码创建了什么呢?RenderPadding

RenderPadding的继承关系是什么呢?

RenderPadding -> RenderShiftedBox -> RenderBox -> RenderObject

我们来具体查看一下RenderPadding的源代码:

  • 如果传入的_padding和原来保存的value一样,那么直接return;
  • 如果不一致,调用_markNeedResolution,而_markNeedResolution内部调用了markNeedsLayout;
  • 而markNeedsLayout的目的就是标记在下一帧绘制时,需要重新布局performLayout;
  • 如果我们找的是Opacity,那么RenderOpacity是调用markNeedsPaint,RenderOpacity中是有一个paint方法的;
  set padding(EdgeInsetsGeometry value) {
assert(value != null);
assert(value.isNonNegative);
if (_padding == value)
return;
_padding = value;
_markNeedResolution();
}

3.3. Element

我们来思考一个问题:

  • 之前我们写的大量的Widget在树结构中存在引用关系,但是Widget会被不断的销毁和重建,那么意味着这棵树非常不稳定;
  • 那么由谁来维系整个Flutter应用程序的树形结构的稳定呢?
  • 答案就是Element。
  • 官方的描述:Element是一个Widget的实例,在树中详细的位置。

Element什么时候创建?

在每一次创建Widget的时候,会创建一个对应的Element,然后将该元素插入树中。

  • Element保存着对Widget的引用;

在SingleChildRenderObjectWidget中,我们可以找到如下代码:

  • 在Widget中,Element被创建,并且在创建时,将this(Widget)传入了;
  • Element就保存了对Widget的应用;
  @override
SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);

在创建完一个Element之后,Framework会调用mount方法来将Element插入到树中具体的位置:

Flutter-渲染原理&三棵树详解_flutter_13

mount方法

在调用mount方法时,会同时使用Widget来创建RenderObject,并且保持对RenderObject的引用:

  • _renderObject = widget.createRenderObject(this);
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
assert(() {
_debugUpdateRenderObjectOwner();
return true;
}());
assert(_slot == newSlot);
attachRenderObject(newSlot);
_dirty = false;
}

但是,如果你去看类似于Text这种组合类的Widget,它也会执行mount方法,但是mount方法中并没有调用createRenderObject这样的方法。

  • 我们发现ComponentElement最主要的目的是挂载之后,调用_firstBuild方法
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
assert(_child == null);
assert(_active);
_firstBuild();
assert(_child != null);
}

void _firstBuild() {
rebuild();
}

如果是一个StatefulWidget,则创建出来的是一个StatefulElement

我们来看一下StatefulElement的构造器:

  • 调用widget的createState()
  • 所以StatefulElement对创建出来的State是有一个引用的
  • 而_state又对widget有一个引用
  StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
....省略代码
_state._widget = widget;

而调用build的时候,本质上调用的是_state中的build方法:

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

3.4. build的context是什么

在StatelessElement中,我们发现是将this传入,所以本质上BuildContext就是当前的Element

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

我们来看一下继承关系图:

  • Element是实现了BuildContext类(隐式接口)
abstract class Element extends DiagnosticableTree implements BuildContext

在StatefulElement中,build方法也是类似,调用state的build方式时,传入的是this

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

3.5. 创建过程小结

Widget只是描述了配置信息:

  • 其中包含createElement方法用于创建Element
  • 也包含createRenderObject,但是不是自己在调用

Element是真正保存树结构的对象:

  • 创建出来后会由framework调用mount方法;
  • 在mount方法中会调用widget的createRenderObject对象;
  • 并且Element对widget和RenderObject都有引用;

RenderObject是真正渲染的对象:

  • 其中有​​markNeedsLayout​​​​performLayout​​​​markNeedsPaint​​​​paint​​等方法
【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月08日 0

暂无评论

推荐阅读
  b1UHV4WKBb2S   2023年11月13日   34   0   0 裁剪ideflutter
  b1UHV4WKBb2S   2023年11月13日   27   0   0 flutterDart
xx2YH4ad7R0N