Flutter渲染机制:Widget、Elment和RenderObject_董小虫的博客-CSDN博客

来源: Flutter渲染机制:Widget、Elment和RenderObject_董小虫的博客-CSDN博客

Widget、Elment和RenderObject
本文也发布于本人的知乎专栏:https://zhuanlan.zhihu.com/p/394568668

引子
在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。
其中涉及到了Widget、Element和Render,都属于Flutter渲染机制。本文将对Flutter渲染机制进行分析。

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

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

}

这个类的作用是桥接RenderObject和Element树,其中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框架来说,主要关注的就是Widget、Element、RenderObject。下面我们来分析一下这三者的特点和关系。

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。
StatefullWidget和StatelessWidget都是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拥有一个可变的状态。这个状态State在Widget建立时可以同步地被读取,而在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方法,来实现一次性初始化。这个初始化依赖于BuildContext或Widget,即分别对应于context和widget属性。
框架调用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继承自ProxyWidget。ProxyWidget会有子Widget提供给它,而不需要新创建一个。

RenderObjectWidget
RenderObjectWidget为RenderObjectElement提供配置。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) { }
}

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

Widget小结
Widget构成了Flutter UI的最上层,直接面对开发者。开发者在开发Flutter应用时,都是通过Widget来实现应用的UI。
Widget类继承关系如图所示:

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相同的runtimeType和key。如果父节点希望改变runtimeType或key,可以通过卸载该Element并填充新的Widget来实现。
还有些时候,一个祖先节点可能会通过调用自身的deactivateChild方法将当前Element从树中移除。停用间接祖先会导致将那个Element的RenderObject从渲染树中移除,并且将当前Element添加到owner的非活动Element列表中,最终引起框架对Element调用deactivate方法。
此时,可以认为Element是“inactive”的,且不再在屏幕上出现。一个Element可以在当前动画帧结束前保持在“inactive”状态。在动画帧结束时,所有仍然保持在“inactive”状态的Element会被卸载(unmount)。
如果这个Element重新合并入树中(如该Element或其祖先节点有一个global key,且重用时),框架会将这个Element从owner的非活跃Element列表中移除,然后将该Element的RenderObject重新附着到渲染树中。此时,这个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是一个抽象类,其子类主要分为两种,ComponentElement和RenderObjectElement。下面分别看一下各自的情况。

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

RenderObjectElement
RenderObjectElement对象有一个关联的渲染树中的RenderObject。RenderObject实际执行布局、绘制、碰撞检测等操作。
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则为SingleChildRenderObjectElement。Align重载了createRenderObject方法,创建的RenderObject为RenderPositionedBox。
接着看一下RenderPositionedBox类:

class RenderPositionedBox extends RenderAligningShiftedBox {

}

abstract class RenderAligningShiftedBox extends RenderShiftedBox {

}

abstract class RenderShiftedBox extends RenderBox with RenderObjectWithChildMixin<RenderBox> {

}

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

则对于Center来说,三者的关系如图所示:

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

class Text extends StatelessWidget {

@override
Widget build(BuildContext context) {

Widget result = RichText(

text: TextSpan(

),
);

return result;
}

}

Text类继承于StatelessWidget,对应StatelessElement。Text实现了build方法,创建了一个RichText。RichText也是一个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、Element和RenderObject做了简要讲解。这三个类系统是Flutter框架的核心。Widget负责UI部分,与开发者直接交互;Element负责在指定位置实例化Widget,并维护树结构;RenderObject则是渲染的核心,负责包括布局、测量、绘制等工作。
三棵树之间有一定的对应关系。一个Widget可能会对应多个Element,而一个Element则仅对应一个Widget;只有继承于RenderObjectElement的Element会维护RenderObject;而RenderObject的创建入口则是在RenderObjectWidget中。
后续文章中,会基于这三者的概念之上,详细分析Flutter的渲染管线。
————————————————
版权声明:本文为CSDN博主「董小虫」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/dongzhong1990/article/details/107042295

赞(0) 打赏
分享到: 更多 (0)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏