Flutter-渲染原理&三棵树详解
Posted 一叶飘舟
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter-渲染原理&三棵树详解相关的知识,希望对你有一定的参考价值。
一. 渲染原理
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
Element从功能上看,可以分为两大类:
- ComponentElement
组合类Element。这类Element主要用来组合其他更基础的Element,得到功能更加复杂的Element。开发时常用到的StatelessWidget和StatefulWidget相对应的Element:StatelessElement和StatefulElement,即属于ComponentElement。
- RenderObjectElement
渲染类Element,对应Renderer Widget,是框架最核心的Element。RenderObjectElement主要包括LeafRenderObjectElement,SingleChildRenderObjectElement,和MultiChildRenderObjectElement。其中,LeafRenderObjectElement对应的Widget是LeafRenderObjectWidget,没有子节点;SingleChildRenderObjectElement对应的Widget是SingleChildRenderObjectWidget,有一个子节点;MultiChildRenderObjectElement对应的Widget是MultiChildRenderObjecWidget,有多个子节点。
2.1 ComponentElement
2.1.1 与核心元素关系
如上文所述,ComponentElement分为StatelessElement和StatefulElement,这两种Element同核心元素Widget以及State之间的关系如下图所示。
如图:
- ComponentElement持有Parent Element及Child Element,由此构成Element Tree.
- ComponentElement持有其对应的Widget,对于StatefulElement,其还持有对应的State,以此实现Element和Widget之间的绑定。
- State是被StatefulElement持有,而不是被StatefulWidget持有,便于State的复用。事实上,State和StatefulElement是一一对应的,只有在初始化StatefulElement时,才会初始化对应的State并将其绑定到StatefulElement上。
2.1.2 ComponentElement核心流程
一个Element的核心操作流程有,创建、更新、销毁三种,下面将分别介绍这三个流程。
- 创建
ComponentElement的创建起源与父Widget调用inflateWidget,然后通过mount将该Element挂载至Element Tree,并递归创建子节点。
- 更新
由父Element执行更新子节点的操作(updateChild),由于新旧Widget的类型和Key均未发生变化,因此触发了Element的更新操作,并通过performRebuild将更新操作传递下去。其核心函数updateChild之后会详细介绍。
- 销毁
由父Element或更上级的节点执行更新子节点的操作(updateChild),由于新旧Widget的类型或者Key发生变化,或者新Widget被移除,因此导致该Element被转为未激活状态,并被加入未激活列表,并在下一帧被失效。
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的主要职责如下:
- 判断新Widget是否有GlobalKey,如果有GlobalKey,则从Inactive Elements列表中找到对应的Element并进行复用。
- 无可复用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的时候,该方法被调用。其主要职责如下:
- 将给Element加入Element Tree,更新_parent,_slot等树相关的属性。
- 如果新Widget有GlobalKey,将该Element注册进GlobalKey中,其作用下文会详细分析。
- ComponentElement的mount函数会调用_firstBuild函数,触发子Widget的创建和更新。
- performRebuild
@override
void performRebuild()
//调用build函数,生成子Widget
Widget built;
built = build();
//根据新的子Widget更新子Element
_child = updateChild(_child, built, slot);
performRebuild的主要职责如下:
- 调用build函数,生成子Widget。
- 根据新的子Widget更新子Element。
- update
@mustCallSuper
void update(covariant Widget newWidget)
_widget = newWidget;
此函数主要职责为:
- 将对应的Widget更新为新的Widget。
- 在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,新旧子Widget的Key和RuntimeType等都相同,则调用update方法更新子Element并返回之。
- 如果newWidget不为null,旧Child不为null,新旧子Widget的Key和RuntimeType等不完全相同,则说明Widget Tree有变动,此时移除旧的子Element,并创建新的子Element,并返回之。
2.2 RenderObjectElement
2.2.1 RenderObjectElement与核心元素关系
RenderObjectElement同核心元素Widget及RenderObject之间的关系如下图所示:
如图:
- RenderObjectElement持有Parent Element,但是不一定持有Child Element,有可能无Child Element,有可能持有一个Child Element(Child),有可能持有多个Child Element(Children)。
- RenderObjectElement持有对应的Widget和RenderObject,将Widget、RenderObject串联起来,实现了Widget、Element、RenderObject之间的绑定。
2.2.2 RenderObjectElement核心流程
如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。
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的一致,此处只列举不一样的职责,职责如下:
- 调用createRenderObject创建RenderObject,并使用attachRenderObject将RenderObject关联到Element上。
- 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的主要职责如下:
- 将对应的Widget更新为新的Widget。
- 调用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;
该函数的主要职责如下:
- 复用能复用的子节点,并调用updateChild对子节点进行更新。
- 对不能更新的子节点,调用deactivateChild对该子节点进行失效。
其步骤如下:
- 从顶部向下更新子Element。
- 从底部向上扫描子Element。
- 扫描旧的子Element列表里面中间的子Element,保存Widget有Key的Element到oldKeyChildren,其他的失效。
- 对于新的子Element列表,如果其对应的Widget的Key和oldKeyChildren中的Key相同,更新oldKeyChildren中的Element。
- 从下到上更新底部的Element。
- 清除旧子Element列表中其他所有剩余Element。
2.1.3 Element小结
本文主要介绍了Element相关知识,重点介绍了其分类,生命周期,和核心函数。重点如下:
- 维护Element Tree,根据Widget Tree的变化来更新Element Tree,包括:节点的插入、更新、删除、移动等;并起到纽带的作用,将Widget以及RenderObject关联到Element Tree上。
- Element分为ComponentElement和RenderObjectElement,前者负责组合子Element,后者负责渲染。
- Element的主要复用和更新逻辑由其核心函数updateChild实现,具体逻辑见上文。
3. RenderObject
通过上篇文章介绍的Element Tree,Flutter 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分类
如上图所示,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,该函数主要作用是通过上级节点传过来的Constraints和parentUsesSize等控制参数,对本节点和其子节点进行布局。Constraints是对于节点布局的约束,其原则是,Constraints向下,Sizes向上,父节点设置本节点的位置。即:
- 一个Widget从它的父节点获取Constraints,并将其传递给子节点。
- 该Widget对其子节点进行布局。
- 最终,该节点告诉其父节点它的Sizes。
在接下来的文章中,我们将对该流程进行详细介绍,当前我们只需要记住该原则。
当本节点的布局依赖于其子节点的布局时,parentUsesSize的值是true,此时,子节点被标记为需要布局时,本节点也将被标记为需要布局。这样当下一帧绘制时本节点和子节点都将被重新布局。反之,如果parentUsesSize的值是false,子节点被重新布局时不需要通知本节点。
RenderObject的子类不应该直接重写RenderObject的layout函数,而是重写performResize和performLayout函数,这两个函数才是真正负责具体布局的函数。
RenderObject中layout函数的源码如下:
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();
从源码可以看到,relayoutBoundary是layout函数中一个重要参数。当一个组件的大小被改变时,其parent的大小可能也会被影响,因此需要通知其父节点。如果这样迭代上去,需要通知整棵RenderObject Tree重新布局,必然会影响布局效率。因此,Flutter通过relayoutBoundary将RenderObject Tree分段,如果遇到了relayoutBoundary,则不去通知其父节点重新布局,因为其大小不会影响父节点的大小。这样就只需要对RenderObject Tree中的一段重新布局,提高了布局效率。关于relayoutBoundary将在之后的文章中详细讲解,目前只需要了解relayoutBoundary会将RenderObject Tree分段,提高布局效率。
- 绘制
绘制对应的函数是paint,其主要作用是将本RenderObject和子RenderObject绘制在Canvas上。RenderObject的子类应该重写这个函数,在该函数中添加绘制的逻辑。
RenderObject的子类RenderFlex的paint函数源码如下:
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的根节点RenderView的hitTest函数。下面是该函数的源码:
bool hitTest(HitTestResult result, Offset position )
if (child != null)
child.hitTest(BoxHitTestResult.wrap(result), position: position);
result.add(HitTestEntry(this));
return true;
从RenderView的构造函数可以看出,child是RenderBox类,因此我们再看RenderBox的hitTest函数。
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() |
可见,RenderObject和Android 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进行渲染,完成界面的展示。
三. 对象的创建过程【参考内容】
上面已经介绍了三棵树的运作流程,这部分为参考内容(视频教程里边学的)
我们这里以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插入到树中具体的位置:
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
等方法
以上是关于Flutter-渲染原理&三棵树详解的主要内容,如果未能解决你的问题,请参考以下文章