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。开发时常用到的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之间的关系如下图所示。

如图:

  • 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,并递归创建子节点。

  • 更新

由父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的主要职责如下:

  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 == nullnewWidget != 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之间的关系如下图所示:

如图:

  • 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

  • 更新

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的一致,此处只列举不一样的职责,职责如下:

  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分类

如上图所示,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 RenderObjectAndroid View
绘制paint()draw()/onDraw()
布局performLayout()/layout()measure()/onMeasure(), layout()/onLayout()
布局约束ConstraintsMeasureSpec
布局协议1performLayout() 的 Constraints 参数表示父节点对子节点的布局限制measure() 的两个参数表示父节点对子节点的布局限制
布局协议2performLayout() 应调用各子节点的 layout()onLayout() 应调用各子节点的 layout()
布局参数parentDatamLayoutParams
请求布局markNeedsLayout()requestLayout()
请求绘制markNeedsPaint()invalidate()
添加 childadoptChild()addView()
移除 childdropChild()removeView()
关联到窗口/树attach()onAttachedToWindow()
从窗口/树取消关联detach()onDetachedFromWindow()
获取 parentparentgetParent()
触摸事件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进行渲染,完成界面的展示。

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

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

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

3.1. Widget

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

继承关系如下:

Padding -> SingleChildRenderObjectWidget -> RenderObjectWidget -> Widget

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

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

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

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

  • 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-渲染原理&三棵树详解的主要内容,如果未能解决你的问题,请参考以下文章

Flutter UI 渲染原理概览

已开源!Flutter 基于分帧渲染的流畅度优化组件 Keframe

详解Flutter中各种Binding

详解Flutter中各种Binding

Flutter渲染完全解读系列:启动与三棵树的构建

Flutter渲染完全解读系列:启动与三棵树的构建