Flutter渲染机制:Widget、Elment和RenderObject

Widget、Elment和RenderObject


引子

在Flutter源码阅读分析:Framework层的启动中,我们分析了Framework层的启动流程,其中讲到了在runApp方法中,调用到了attchRootWidget方法:

// ./packages/flutter/lib/src/widgets/binding.dart
void attachRootWidget(Widget rootWidget) {
  _readyToProduceFrames = true;
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget,
  ).attachToRenderTree(buildOwner, renderViewElement as RenderObjectToWidgetElement<RenderBox>);
}

这个方法获取一个Widget并将其附到renderViewElement上,在必要的时候创建这个renderViewElement
其中涉及到了WidgetElementRender,都属于Flutter渲染机制。本文将对Flutter渲染机制进行分析。

首先看一下RenderObjectToWidgetAdapter这个类和其构造方法:

class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
  RenderObjectToWidgetAdapter({
    this.child,
    this.container,
    this.debugShortDescription,
  }) : super(key: GlobalObjectKey(container));
  ...
}

这个类的作用是桥接RenderObjectElement树,其中container就是RenderObject,而Element树则插入在其中。类型参数T是一种RenderObject,是container期望其孩子的类型。
再看一下RenderObjectToWidgetAdapter类的attachToRenderTree方法:

RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
  if (element == null) {
    owner.lockState(() {
      element = createElement();
      assert(element != null);
      element.assignOwner(owner);
    });
    owner.buildScope(element, () {
      element.mount(null, null);
    });
    // This is most likely the first time the framework is ready to produce
    // a frame. Ensure that we are asked for one.
    SchedulerBinding.instance.ensureVisualUpdate();
  } else {
    element._newWidget = this;
    element.markNeedsBuild();
  }
  return element;
}

这个方法填充了widget,并且将结果RenderObject设置为container的孩子。如果element为空,那么则会创建一个新的element,否则的话,给定的element会调度一个更新,使得与当前widget关联。

从上面可以看出,对于Flutter框架来说,主要关注的就是WidgetElementRenderObject。下面我们来分析一下这三者的特点和关系。


Widget

在上文中的例子中,rootWidget是用户开发的Flutter应用的根节点,是一个Widget类。
Widget类的注释中,官方给出的定位是用于描述Element的配置。Widget是Flutter框架的中心类结构。一个Widget是UI中一个固定不变的部分。可以被填充成Element,而Element又管理底层的渲染树。
Widget本身是没有可变状态的,所有的成员变量都是final的。如果需要和一个Widget关联的可变状态,可以使用StatefulWidget,这个类会创建一个StatefulWidget,而它又会在填充成element和合并到树中的时候创建一个State对象。
一个给定的Widget可以被包含到树中0次或更多次。特别是Widget可以被多次放置到树中。每次Widget被放置到树中,都会填充成一个Element。看一下Widget基类的方法声明:

@immutable
abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });

  final Key key;

  @protected
  Element createElement();

  ...

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    ...
  }

  static int _debugConcreteSubtype(Widget widget) {
    ...
  }
}

分别介绍一下这几个方法和成员变量。
首先是key这个成员变量,它用于控制在树中一个Widget如何替换另一个。主要有以下几种方式:更新Element、替换Element以及换位置。通常情况下,如果一个Widget是另一个的唯一孩子,那么不需要明确的key
createElement方法用于将配置填充为一个具体的实例。
canUpdate方法用于判断newWidget能否用于更新当前以oldWidget为配置的Element
_debugConcreteSubtype方法返回一个编码值,用于指示Widget的实际子类型,1表示StatefulWidget,2表示StatelessWidget
StatefullWidgetStatelessWidget都是Widget的抽象子类,下面看一下这两个子类的具体情况。

StatelessWidget

StatelessWidget用于不需要可变状态的情况。一个无状态Widget通过建立一些列其他更完整描述UI的Widget的方式,来描述部分UI。这个构建过程是一个递归的过程,直到这个描述已经被完全的实现。
当部分UI依赖的只有其自身配置信息和BuildContext时,StatelessWidget就非常有用了。

abstract class StatelessWidget extends Widget {
  ...
  @protected
  Widget build(BuildContext context);
}

build方法会在当前Widget被插入到给定BuildContext内的树中时被调用。框架会用这个方法返回的Widget更新当前Widget的子树,可能是更新现有子树,也可能是移除子树。然后根据返回的Widget填充一个新的子树。
通常情况下,这个方法的实现,会返回一个新建的Widget系列,构建信息是根据从当前Widget构造函数和给定BuildContext中传递进来的信息来配置。

StatefulWidget

StatefulWidget拥有一个可变的状态。这个状态StateWidget建立时可以同步地被读取,而在Widget的整个声明周期中可能会改变。
StatefulWidget可用于可动态变化的用户节目口描述。比如说,依赖于一些系统状态或者时钟驱动的情况。

abstract class StatefulWidget extends Widget {
  ...
  @protected
  State createState();
}

StatefulWIdget实例本身时不可变的,但是其动态信息会保存在一切辅助类对象里,比如通过createState方法创建的State对象,或者是State订阅的对象。
框架会在填充一个StatefulWidget时调用createState方法。这意味着,当一个StatefulWidget在树中不同位置插入时,可能会有多个State对象与这个StatefulWidget关联。类似的,如果一个StatefulWidget先从树中移除,之后又重新插入到树中,那么框架会再次调用createState去创建一个新的State对象,便于简化State对象的声明周期。

可以看出,对于StatefulWidget来说,State类是关键辅助类。下面再看一下State类的详情。

State

State类用于表示StatefulWidget的逻辑和内部状态。
State对象有以下的声明周期:

  • 框架通过调用StatefulWidget.createState方法创建State对象;
  • 新创建的State对象与一个BuildContext关联。这个关联是不变的,也就是说,State对象不会改变它的BuildContext。不过,BuildContext本身是可以在沿着子树移动。这种情况下,State对象可以认为是mounted
  • 框架调用initState方法。State的子类都需要重载initState方法,来实现一次性初始化。这个初始化依赖于BuildContextWidget,即分别对应于contextwidget属性。
  • 框架调用didChangeDependencies方法。State的子类需要重写该方法,来实现包括InderitedWidget在内的初始化。如果调用了BuildContext.dependOnInheritedWIdgetOfExactType方法,那么在后续InheritedWidget改变或当前Widget在树中移动时,didChangeDependencies方法会再次被调用。
  • 此时State对象已经完全初始化,框架可能会调用任意次数的build方法来获取一个子树UI的描述。State对象会自发的通过调用setState方法来请求重建其子树。这个方法以为着其部分内部状态发生了改变,这可能会影响到子树中的UI。
  • 在这期间,一个父Widget可能会重建和请求当前位置显式一个新的Widget。当这些发生时,框架会将widget属性更新为新的Widget,并且调用didUpdateWidget方法,将之前的Widget作为一个参数传入。State对象应该重载didUpdateWidget方法来应对其关联的Widget的变化。框架也会在didUpdateWidget方法之后调用build方法,这意味着在didUpdateWidget方法内调用setState方法是多余的。
  • 在开发过程中,如果发生了重载,则会调用reassemble方法。这会使得在iniState方法中准备好的数据重新初始化。
  • 如果包含State的子树从树中移除,框架会调用deactivate方法。子类需要重载这个方法,来清理当前对象和树中其他element的连接。
  • 此时,框架可能会重新将该子树插入到树的另一个地方。框架会确保调用了build方法使得State对象适配新位置。以上操作会在子树移动所在的动画帧结束前完成,这意味着State对象可以延迟释放大部分资源,直到框架调用他们的dispose方法。
  • 如果框架没有在动画帧结束前重新插入子树,那么框架会调用dispose方法,表示这个State对象不会再创建。子类需要重载这个方法来释放这个对象持有的资源。
  • 在框架调用dispose之后,State对象可以认为是未安装状态,其mounted属性置为false。此时不能调用setState方法。生命周期终止:在State对象被处理后,将不再有机会重新挂载。
@optionalTypeArgs
abstract class State<T extends StatefulWidget> with Diagnosticable {
  ...
  T _widget;
  ...
  StatefulElement _element;
  bool get mounted => _element != null;

  @protected
  @mustCallSuper
  void initState() {
    assert(_debugLifecycleState == _StateLifecycle.created);
  }

  @mustCallSuper
  @protected
  void didUpdateWidget(covariant T oldWidget) { }

  @protected
  @mustCallSuper
  void reassemble() { }

  @protected
  void setState(VoidCallback fn) {
    ...
  }

  @protected
  @mustCallSuper
  void deactivate() { }

  @protected
  @mustCallSuper
  void dispose() {
    ...
  }

  @protected
  Widget build(BuildContext context);

  @protected
  @mustCallSuper
  void didChangeDependencies() { }
  ...
}

InheritedWidget

InheritedWIdget可用于向下传播信息的Widget的基类。为了从BuildContext中获取最近的特定类型InheritedWidget实例,需要使用BuildContext.dependOnInheritedWidgetOfExactType。如果使用这种方式引用了InheritedWidget,那么在其状态发生改变时,会引发消费者重建。

abstract class InheritedWidget extends ProxyWidget {
  ...
  @override
  InheritedElement createElement() => InheritedElement(this);

  @protected
  bool updateShouldNotify(covariant InheritedWidget oldWidget);
}

InheritedWIdget继承自ProxyWidgetProxyWidget会有子Widget提供给它,而不需要新创建一个。

RenderObjectWidget

RenderObjectWidgetRenderObjectElement提供配置。RenderObjectElement用于包装RenderObject。而RenderObject则是提供了应用实际渲染。

abstract class RenderObjectWidget extends Widget {
  ...
  @override
  RenderObjectElement createElement();

  @protected
  RenderObject createRenderObject(BuildContext context);

  @protected
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }

  @protected
  void didUnmountRenderObject(covariant RenderObject renderObject) { }
}

这个类有三个重要子类,分别是LeafRenderObjectWidgetSingleChildRenderObjectWidgetMultiChildRenderObjectWidget,分别用于无子节点、有单个子节点和有多个子节点的RenderObjectWidget

Widget小结

Widget构成了Flutter UI的最上层,直接面对开发者。开发者在开发Flutter应用时,都是通过Widget来实现应用的UI。
Widget类继承关系如图所示:
Flutter渲染机制:Widget、Elment和RenderObject_第1张图片


Element

上文讲过了Widget的作用是为Element的配置提供描述的。反过来讲,那就是Element其实可以说是Widget在树中特定位置的实例。
Widget用来描述如何配置一个子树,而且同一个Widget可以用来同时配置多个子树,因为Widget是不可变的。经过一段时间后,与给定Element相关联的Widget可能会发生改变。例如,当父节点Widget重建后,为当前位置创建了一个新Widget
Element会形成一棵树。大部分的Element拥有单独的一个子节点,不过部分Widget(如RenderObjectElement的子类)会拥有多个子节点。
Element有如下的生命周期:

  • 框架通过调用Widget.createElement方法创建一个Element。这个Widget被用来当作Element的初始化配置。
  • 框架调用mount方法来将一个新创建的Element添加到树中给定父节点的指定槽中。mount方法负责填充所有的子Widget,以及在必要的时候调用attachRenderObject将关联的RenderObject附着到render树上。
  • 此时,可以认为Element是“active”,可以出现在屏幕上了。
  • 有些时候,父节点可能会变更该Element使用的配置Widget。在这种情况下,框架会调用update方法来更新Widget。新的Widget通常会有与老的Widget相同的runtimeTypekey。如果父节点希望改变runtimeTypekey,可以通过卸载该Element并填充新的Widget来实现。
  • 还有些时候,一个祖先节点可能会通过调用自身的deactivateChild方法将当前Element从树中移除。停用间接祖先会导致将那个ElementRenderObject从渲染树中移除,并且将当前Element添加到owner的非活动Element列表中,最终引起框架对Element调用deactivate方法。
  • 此时,可以认为Element是“inactive”的,且不再在屏幕上出现。一个Element可以在当前动画帧结束前保持在“inactive”状态。在动画帧结束时,所有仍然保持在“inactive”状态的Element会被卸载(unmount)。
  • 如果这个Element重新合并入树中(如该Element或其祖先节点有一个global key,且重用时),框架会将这个Elementowner的非活跃Element列表中移除,然后将该ElementRenderObject重新附着到渲染树中。此时,这个Element重新被视为“active”,且可以出现在屏幕上。
  • 如果这个Element在当前动画帧结束时没有重新合并到树中,框架就会对该Element调用unmount方法。
  • 此时,这个Element可以认为是“defunct”状态,且不再会重新合并入树中。
abstract class Element extends DiagnosticableTree implements BuildContext {
  ...
  @mustCallSuper
  @protected
  void reassemble() {
    ...
  }
  ...
  void visitChildren(ElementVisitor visitor) { }
  ...
  @protected
  Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    ...
  }

  @mustCallSuper
  void mount(Element parent, dynamic newSlot) {
    ...
  }

  @mustCallSuper
  void update(covariant Widget newWidget) {
    ...
  }
  ...
  void detachRenderObject() {
    ...
  }

  void attachRenderObject(dynamic newSlot) {
    ...
  }
  ...
  @protected
  Element inflateWidget(Widget newWidget, dynamic newSlot) {
    ...
  }
  ...
  @protected
  void deactivateChild(Element child) {
    ...
  }
  ...
  @mustCallSuper
  void activate() {
    ...
  }

  @mustCallSuper
  void deactivate() {
    ...
  }
  ...
  @mustCallSuper
  void unmount() {
    ...
  }
  ...
  void markNeedsBuild() {
    ...
  }

  void rebuild() {
    ...
  }

  @protected
  void performRebuild();
}

可以看到,Element类继承于BuildContext类,也就是说Element就是在Widget小节里经常提到的BuildContext
Element类的方法中,updateChild方法是Widget系统的核心。这个方法的作用是使用给定的新配置来更新指定的子节点。每当基于更新的配置来添加、更新、移除一个子节点时,都会调用这个方法。updateChild方法通过比较子节点和给定新配置,来判断如何处理。可由下表来表示其逻辑。

newWidget == null newWidget != null
child == null Returns null. Returns new [Element].
child != null Old child is removed, returns null. Old child updated if possible, returns child or new [Element].

Element是一个抽象类,其子类主要分为两种,ComponentElementRenderObjectElement。下面分别看一下各自的情况。

ComponentElement

ComponentElement是组合其他ElementElement。其本身不直接创造RenderObject,但是会通过创造其他Element的方式间接地创建RenderObject
ComponentElement的子类StatelessElementStatefulElement分别是对应于StatelessWidgetStatefullWidgetElement。同样,InheritedElement也是ComponentElement的一种,对应于InheritedWidget

RenderObjectElement

RenderObjectElement对象有一个关联的渲染树中的RenderObjectRenderObject实际执行布局、绘制、碰撞检测等操作。
RenderObject有三种子节点模型:

  • 叶节点RenderObject,无子节点:LeafRenderObjectElement类处理这种情况
  • 单独子节点:SingleChildRenderObjectElement类处理这种情况
  • 多个子节点的链表:MultiChildRenderObjectElement类处理这种情况
    有的时候,RenderObject的子节点模型会更复杂。可续会有一个二维数组的子节点,可能仅在需要的时候创建子节点,也可能形成多列表的形式。在这些情况发生时,就需要相应的新的RenderObjectElement子类。这样的子类需要能够管理子节点,特别时这个对象的Element子节点,以及其对应RenderObject的子节点。

RenderObjectElement还有一个特殊的子类RootRenderObjectElement,用于表示树的根节点。只有根节点Element可以显式的设置BuildOwner,其他的Element都只能继承父节点的BuildOwner


RenderObject

RenderObject是渲染库的核心。每个RenderObject都有一个父节点,且有一个叫parentData的槽位用于供父RenderObject保存子节点相关数据,例如子节点位置等。RenderObject类还实现了基本的布局和绘制协议。
RenderObject类没有定义子节点模型(如一个节点有零个、一个还是多个子节点),也没有定义坐标系(如是在直角坐标系还是极坐标系中),同样也没有定义特定的布局协议。
RenderBox子类采用了直角坐标系布局系统。通常情况下,直接继承RenderObject类有点过度了,继承RenderBox类可能会更好一些。当然,如果实现的子类不想使用直角坐标系的话,那就得继承RenderObject类了。

abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
  ...
  void reassemble() {
    ...
  }

  // LAYOUT
  ...
  @override
  void adoptChild(RenderObject child) {
    ...
  }

  @override
  void dropChild(RenderObject child) { 
    ...
  }

  void visitChildren(RenderObjectVisitor visitor) { }
  ...
  @override
  void attach(PipelineOwner owner) {
    ...
  }
  ...
  void markNeedsLayout() {
    ...
  }
  ...
  void scheduleInitialLayout() {
    ...
  }
  ...
  void layout(Constraints constraints, { bool parentUsesSize = false }) {
    ...
  }
  ...
  // PAINTING
  ...
  void markNeedsCompositingBitsUpdate() {
    ...
  }
  ...
  void markNeedsPaint() {
    ...
  }
  ...
  void scheduleInitialPaint(ContainerLayer rootLayer) {
    ...
  }
  ...
  void paint(PaintingContext context, Offset offset) { }

  void applyPaintTransform(covariant RenderObject child, Matrix4 transform) {
    ...
  }
  ...
  // SEMANTICS
  void scheduleInitialSemantics() {
    ...
  }
  ...
  void markNeedsSemanticsUpdate() {
    ...
  }
  ...
  void assembleSemanticsNode(
    SemanticsNode node,
    SemanticsConfiguration config,
    Iterable<SemanticsNode> children,
  ) {
    ...
  }

  // EVENTS
  @override
  void handleEvent(PointerEvent event, covariant HitTestEntry entry) { }
  ...
}

下面来看一下Flutter提供的实现子类RenderBox

RenderBox

RenderBox是在二维直角坐标系内的RenderObject
对于RenderBox来说,size表达为宽和高。每个RenderBox都有自己的坐标系,这个坐标系左上角坐标为(0, 0),右下角坐标为(width, height)RenderBox中的点包含了左上角,但是不包含右下角。
盒布局通过向下传递BoxConstraints对象来实现布局。BoxContraints为子节点的宽高提供了最大值和最小值约束。子节点在确定自身尺寸时,必须遵守父节点给定的约束。
以上协议足够表达一系列普通盒布局数据流。例如,为了实现width-in-height-out数据流,在调用子节点layout方法时,传递有紧凑的宽度数值的盒约束。在子节点确定了其高度后,使用子节点的高度来确定自身的尺寸。

RenderView

RenderView是渲染树的根节点,其直接继承于RenderObject
RenderVIew表示的是渲染树的整体输出surface。它也处理整个渲染管线的启动工作。RenderView只有一个单独的子节点,这个子节点是RenderBox类,它负责填满整个输出surface。

三者关系

我们仍然以Framework层的启动中的启动的app为例。

const Center( // [2]
  child:
    Text('Hello, world!',
      key: Key('title'),
      textDirection: TextDirection.ltr
    )
)

Center

首先看一下Center类。Center是将子节点置于其中心的Widget

class Center extends Align {
  ...
}

Center类继承自Align类:

class Align extends SingleChildRenderObjectWidget {
  ...
  @override
  RenderPositionedBox createRenderObject(BuildContext context) {
    return RenderPositionedBox(
      alignment: alignment,
      widthFactor: widthFactor,
      heightFactor: heightFactor,
      textDirection: Directionality.of(context),
    );
  }
  ...
}

Align类继承自SingleChildRenderObjectWidget,对应的Element则为SingleChildRenderObjectElementAlign重载了createRenderObject方法,创建的RenderObjectRenderPositionedBox
接着看一下RenderPositionedBox类:

class RenderPositionedBox extends RenderAligningShiftedBox {
  ...
}

abstract class RenderAligningShiftedBox extends RenderShiftedBox {
  ...
}

abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
  ...
}

RenderPositionedBox类最终继承自RenderBox。通过使用一个AlignmentGeometry来对子节点进行定位。

则对于Center来说,三者的关系如图所示:
Flutter渲染机制:Widget、Elment和RenderObject_第2张图片

Text

再来看一下Text类。该类用于表示一连串相同样式的文字。

class Text extends StatelessWidget {
  ...
  @override
  Widget build(BuildContext context) {
    ...
    Widget result = RichText(
      ...
      text: TextSpan(
        ...
      ),
    );
    ...
    return result;
  }
  ...
}

Text类继承于StatelessWidget,对应StatelessElementText实现了build方法,创建了一个RichTextRichText也是一个Widget,继承关系如下:

class RichText extends MultiChildRenderObjectWidget {
  ...
  @override
  RenderParagraph createRenderObject(BuildContext context) {
    ...
  }
  ...
}

RichText继承于MultiChildRenderObjectWidget,用于表示一个富文本段落。RichText可能有多个SizedBox类子节点,但这种类型的子节点是通过Text.rich方法创建的,该例子内不涉及,也就是说children属性是一个长度为0的列表。
RichText重载了createRenderObject,创建一个RenderParagraph

class RenderParagraph extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, TextParentData>,
             RenderBoxContainerDefaultsMixin<RenderBox, TextParentData>,
                  RelayoutWhenSystemFontsChangeMixin {
  ...
}

RenderParagraph继承自RenderBox,是用于展示文字段落的RenderObject
对于Text来说,三者关系如下图:
Flutter渲染机制:Widget、Elment和RenderObject_第3张图片

三棵树

根据上面的分析,可以得到例子的三棵树关系图。
Flutter渲染机制:Widget、Elment和RenderObject_第4张图片

总结

本文对Flutter框架中的WidgetElementRenderObject做了简要讲解。这三个类系统是Flutter框架的核心。Widget负责UI部分,与开发者直接交互;Element负责在指定位置实例化Widget,并维护树结构;RenderObject则是渲染的核心,负责包括布局、测量、绘制等工作。
三棵树之间有一定的对应关系。一个Widget可能会对应多个Element,而一个Element则仅对应一个Widget;只有继承于RenderObjectElementElement会维护RenderObject;而RenderObject的创建入口则是在RenderObjectWidget中。
后续文章中,会基于这三者的概念之上,详细分析Flutter的渲染管线。

你可能感兴趣的:(Flutter源码阅读分析)