Flutter——浅析Widget原理

Widget渲染过程

在进行App开发的时候,我们往往会关注的一个问题是:如何结构化地组织视图数据,提供渲染引擎,最终完成界面显示。

通常情况下,不同UI框架都会以不同方式去处理这一个问题,但无一例外都会用到视图树(View Tree)的概念,而Flutter将视图树的概念进行了扩展,把视图树的组织和渲染抽象为三部分,即Widget,Element和RenderObject。

widget,element和renderObject

Widget

很多人理解为Widget为一个组件其实并不是完全正确的,Widget只是一个数据配置信息,它是组件实现的基本逻辑单位,它里面存储了有关视图渲染的配置信息,比如布局,渲染属性,事件相应信息等等。

当视图渲染信息发生变化的时候,Flutter会选择重建Widget树的方式进行数据更新。

但,你可想到的是——当Widget树层次很深的时候,只要视图渲染信息发生变化的时候会涉及到大量的Widget的销毁和重建,这样会对垃圾回收造成一定的压力。但是Widget它只是一个数据配置对象,它是一个轻量级的数据结构,不涉及实际渲染位图,重建的成本很低。

另外,由于Widget的不可变性,可以以较低成本进行渲染节点复用,因此在一个真实的渲染树中可能存在不同的Widget对应同一个渲染节点的情况,这无疑又降低了重建UI的成本。

Element

Element是Widget的一个实例化对象,它是配置信息到最终模型数据成型的桥梁。

Flutter渲染过程,可以分为这三步:

  • 首先通过Widget树生成对应的Element树

  • 然后,创建相应的RenderObject并关联到Element.renderObject属性上

  • 最后,构建RenderObject树,来完成最终的渲染

    可以看到其实Widget和Element都不负责视图的渲染,最终干活的只是RenderObject。Widget和Element只是负责发号施令,但是既然都是发号施令,为什么不直接通过Widget去控制RenderObject的渲染呢?其实本来三者就可以结合在一起,分为两者是为了数据信息和处理信息的解耦,而分为三者也是为了解耦。

    因为Widget具有不可变性,每次改变都需要重建,如果Widget直接关联RenderObject,那么就意味着Widget的重建必然会导致RenderObject的重建,而RenderObject是负责渲染过程的,所以RenderObject的重建会非常影响性能,Element就是为了解决这个问题的,为了解决Widget和RenderObject的耦合问题,Element充当了中间的抽象层,它会和Widget中的数据进行同步,只将需要修改的部分同步到真实的RenderObject树中,最大程度降低对真实渲染视图的修改,提高渲染效率,而不是销毁整个渲染视图树。

    这,就是Element存在的意义。

RenderObject

RenderObject主要负责实现视图渲染的对象。

渲染对象树在Flutter的展示过程分为四个阶段——布局,绘制,合成和渲染。其中,布局和绘制在RenderObject中完成,Flutter采用深度优先机制遍历渲染对象树,确定树中各个对象的位置和尺寸,并把他们绘制到不同图层。绘制完毕后,合成和渲染的工作则交给Skia搞定。

我们可以先了解一下图像的绘制原理,图像绘制需要三大主角——CPU,GPU,显示器

CPU负责图像数据计算,GPU负责图像数据渲染,显示器负责最终图像显示。

整个过程就是CPU将计算好的模型数据交给GPU,GPU完成渲染之后放入帧缓冲区,随后视频控制器根据垂直同步信号(vsync)以每秒60次的速度,从帧缓冲区读取帧数据交给显示器完成图像显示

Flutter图像显示原理

在图中CPU线程所做的事情其实就是RenderObject需要做的事情,我们上文所说渲染对象树在Flutter的展示过程分为布局,绘制,合成和渲染。其中合成和渲染其实就是上图中的GPU线程合成数据交给Skia然后Skia交个自身图像渲染引擎OpenGL来完成渲染。前面的布局和绘制则是RenderObject完成的。

上文我们可以知道Widget负责存储控件信息如位置,大小,渲染属性,事件回应信息等,其实位置和大小就是布局,而渲染属性和等等其他信息其实就是绘制这一层了,RenderObject通过Element抽象层来连接到Widget中的数据,然后对Widget数的数据创建渲染对象树,然后创建真正的渲染模型对象数据交给GPU线程去完成真正的视图渲染。

RenderObjectWidget

在Flutter中有两个Widget,StatelessWidget和StatefulWidget,这两个Widget都是继承了Widget,但其实它们只是用来组装空间的容器并不负责组件最后的布局和绘制。在Flutter中,布局和绘制实际上是在Widget的另一个子类RenderObjectWidget内完成的。

我们来看一下RenderObjectWidget的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
abstract class RenderObjectWidget extends Widget {

const RenderObjectWidget({ Key key }) : super(key: key);

@override
RenderObjectElement createElement();

@protected
RenderObject createRenderObject(BuildContext context);

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

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

我们可以看到这个类有创建Element和RenderObject两个方法,还有更新和didUnmountRenderObject方法

对于Element对象的创建,Flutter会在遍历Widget树时,调用createElement去同步Widget自身配置从而生成对应节点的Element对象,而对于RenderObject的创建和更新其实是在RenderObjectElement类中完成的

RenderObjectElement

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
abstract class RenderObjectElement extends Element {
/// Creates an element that uses the given widget as its configuration.
RenderObjectElement(RenderObjectWidget widget) : super(widget);

@override
RenderObjectWidget get widget => super.widget;

/// The underlying [RenderObject] for this element.
@override
RenderObject get renderObject => _renderObject;
RenderObject _renderObject;

@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;
}

@override
void update(covariant RenderObjectWidget newWidget) {
super.update(newWidget);
assert(widget == newWidget);
assert(() { _debugUpdateRenderObjectOwner(); return true; }());
widget.updateRenderObject(this, renderObject);
_dirty = false;
}

.............
}

以上是部分RenderObjectElement类的源码,这个类继承了Element类,它同时拥有了RenderObjectWidget和RenderObject对象,并在其中的mount方法中调用了widget的createRenderObject方法然后进行渲染树的插入工作,并且在update方法中调用了widget的updateRenderObject方法。

我们还可以查看一些子类对于createRenderObject和updateRenderObject方法的实现

这里我拿Align演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@override
RenderPositionedBox createRenderObject(BuildContext context) {
return RenderPositionedBox(
alignment: alignment,
widthFactor: widthFactor,
heightFactor: heightFactor,
textDirection: Directionality.of(context),
);
}

@override
void updateRenderObject(BuildContext context, RenderPositionedBox renderObject) {
renderObject
..alignment = alignment
..widthFactor = widthFactor
..heightFactor = heightFactor
..textDirection = Directionality.of(context);
}

我们可以看到最终的图像渲染——布局和绘制最终都是交给RenderObject去实现的,比如create是通过RenderPositionedBox去实例化,而这个类正是继承了RenderObject。比如update中最终还是对renderObject对象进行级联操作。

我们可以看一下RenderObject的源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
void paint(PaintingContext context, Offset offset) { }

void layout(Constraints constraints, { bool parentUsesSize = false }) {
assert(constraints != null);
assert(constraints.debugAssertIsValid(
isAppliedConstraint: true,
informationCollector: (StringBuffer information) {
final List<String> stack = StackTrace.current.toString().split('\n');
int targetFrame;
final Pattern layoutFramePattern = RegExp(r'^#[0-9]+ +RenderObject.layout \(');
for (int i = 0; i < stack.length; i += 1) {
if (layoutFramePattern.matchAsPrefix(stack[i]) != null) {
targetFrame = i + 1;
break;
}
}
if (targetFrame != null && targetFrame < stack.length) {
information.writeln(
'These invalid constraints were provided to $runtimeType\'s layout() '
'function by the following function, which probably computed the '
'invalid constraints in question:'
);
final Pattern targetFramePattern = RegExp(r'^#[0-9]+ +(.+)$');
final Match targetFrameMatch = targetFramePattern.matchAsPrefix(stack[targetFrame]);
if (targetFrameMatch != null && targetFrameMatch.groupCount > 0) {
information.writeln(' ${targetFrameMatch.group(1)}');
} else {
information.writeln(stack[targetFrame]);
}
}
},
));
assert(!_debugDoingThisResize);
assert(!_debugDoingThisLayout);
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
assert(() {
_debugCanParentUseSize = parentUsesSize;
return true;
}());
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
assert(() {
// in case parentUsesSize changed since the last invocation, set size
// to itself, so it has the right internal debug values.
_debugDoingThisResize = sizedByParent;
_debugDoingThisLayout = !sizedByParent;
final RenderObject debugPreviousActiveLayout = _debugActiveLayout;
_debugActiveLayout = this;
debugResetSize();
_debugActiveLayout = debugPreviousActiveLayout;
_debugDoingThisLayout = false;
_debugDoingThisResize = false;
return true;
}());
return;
}
_constraints = constraints;
_relayoutBoundary = relayoutBoundary;
assert(!_debugMutationsLocked);
assert(!_doingThisLayoutWithCallback);
assert(() {
_debugMutationsLocked = true;
if (debugPrintLayouts)
debugPrint('Laying out (${sizedByParent ? "with separate resize" : "with resize allowed"}) $this');
return true;
}());
if (sizedByParent) {
assert(() { _debugDoingThisResize = true; return true; }());
try {
performResize();
assert(() { debugAssertDoesMeetConstraints(); return true; }());
} catch (e, stack) {
_debugReportException('performResize', e, stack);
}
assert(() { _debugDoingThisResize = false; return true; }());
}
RenderObject debugPreviousActiveLayout;
assert(() {
_debugDoingThisLayout = true;
debugPreviousActiveLayout = _debugActiveLayout;
_debugActiveLayout = this;
return true;
}());
try {
performLayout();
markNeedsSemanticsUpdate();
assert(() { debugAssertDoesMeetConstraints(); return true; }());
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
}
assert(() {
_debugActiveLayout = debugPreviousActiveLayout;
_debugDoingThisLayout = false;
_debugMutationsLocked = false;
return true;
}());
_needsLayout = false;
markNeedsPaint();
}

在这里我们就可以画出结构图

三者结构关系

三者结构关系

-------------本文结束感谢阅读-------------