原文传送门:
RenderObject
之前讲过三棵树的绘制,它们最后都是为RenderObject树服务的,RenderObject是确定节点位置、大小,处理子父位置关系的类,当构建过程完成之后,就生成了一棵RenderObject
树,然后进入布局及绘制阶段。
RenderObject
中包含parent
、parentData
、constraints
等属性,layout
及paint
等抽象方法,不过其中没有定义size、offset等具体大小和位置信息,为了更好的扩展性,其大小是通过RenderBox
这个继承至RenderObject
的类来实现的,parentData
会保存位置等信息,在绘制时父节点再实时计算传给paint
。具体实现细节,我们后面来慢慢讲解。
接收通知
Flutter在什么时候开始渲染流程呢?为了让渲染更加流畅,渲染只能被设计成异步,因为你不知道开发者会在复杂的业务中同时发起多少次渲染,渲染本身的耗时是昂贵的,如果每次都会经历渲染,那界面一定会卡顿。
如果只是简单的通过Flutter的Future来进行异步渲染,同上也会有性能问题,因为业务中也会存在多个异步。
所以Android
和iOS
都有一个叫Vsync
的机制。Vsync(垂直同步)
是VerticalSynchronization的简写,让AppUI和SurfaceFlinger可以按硬件产生的VSync节奏进行工作,以此来达到界面的刷新和渲染保持在60FPS以内,让人类视觉上感觉到不卡顿。
上一篇文章讲了三棵树的构建完成后会发送一个通知,然后会等待Vsync
信号的到来,在Flutter中,SchedulerBinding
中有一个addPersistentFrameCallback
方法来注册回调监听
/// 注册回调监听
void addPersistentFrameCallback(FrameCallback callback) {
_persistentCallbacks.add(callback);
}
2
3
4
当Vsync发送到平台端时,会调用window.onDrawFrame
方法,后面会触发到handleDrawFrame
void handleDrawFrame() {
assert(_schedulerPhase == SchedulerPhase.midFrameMicrotasks);
try {
// 处理addPersistentFrameCallback回调
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (final FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
_schedulerPhase = SchedulerPhase.postFrameCallbacks;
// 处理addPersistentFrameCallback回调 end
// 处理addPostFrameCallback添加的回调,此回调会在添加监听的下一时刻被调用且只会被调用一次
final List<FrameCallback> localPostFrameCallbacks =
List<FrameCallback>.from(_postFrameCallbacks);
_postFrameCallbacks.clear();
for (final FrameCallback callback in localPostFrameCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
// 处理addPostFrameCallback添加的回调 end
} finally {
_schedulerPhase = SchedulerPhase.idle;
//...
_currentFrameTimeStamp = null;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
上面的方法会循环遍历回调队列并执行,其中还会执行通过SchedulerBinding.instance.addPostFrameCallback
添加的回调。
虽然正常的流程是在树构建完成后发送window.scheduleFrame
事件,然后通过window.onDrawFrame
来接收下一侦开始信号,不过在首次渲染时,为了让界面更快的显示,runApp方法中也会在构建完成后第一时间调用scheduleWarmUpFrame
先进行渲染(后面也会调用handleDrawFrame
)。
[-> packages/flutter/lib/src/widgets/binding.dart:WidgetsBinding]
void drawFrame() {
// ...
try {
if (renderViewElement != null)
buildOwner!.buildScope(renderViewElement!);
super.drawFrame();
buildOwner!.finalizeTree();
} finally {
//...
}
//...
}
2
3
4
5
6
7
8
9
10
11
12
在应用启动的时候,_handlePersistentFrameCallback
方法就会被注册到_persistentCallbacks
中,上面的handleDrawFrame
就会触发上面这个drawFrame
方法,它做了三件事情
- 调用
BuildOwner.buildScope
,重新构建脏节点 - 调用
super.drawFrame
方法,开始渲染流程 - 调用
buildOwner.finalizeTree
,开发模式中做一些全局检查(Key重复使用)
先看看渲染流程图
其对应的源码也非常简洁
[-> packages/flutter/lib/src/rendering/binding.dart:RenderBinding]
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout(); // 布局
pipelineOwner.flushCompositingBits(); // 更新所有节点,计算待绘制区域数据
pipelineOwner.flushPaint(); // 绘制
if (sendFramesToEngine) {
renderView.compositeFrame(); // 将绘制数据提交到GPU线程
pipelineOwner.flushSemantics(); // 更新语义化,给一些视力障碍人士提供UI的的语义
_firstFrameSent = true;
}
}
2
3
4
5
6
7
8
9
10
11
12
PipelineOwner
就是渲染流水线俗称渲染管线
,它通过持有根节点的RenderObject
及所有子节点会持有它来对节点的布局绘制的控制。
布局过程
layout
几乎是所有现代前端技术必不可少的流程,它是通过一系列复杂的计算来确定每个节点具体占据多少的位置,处理父子布局关系及显示位置的计算。
我们先看看布局的大概流程图
setSize表示设置盒子的大小
flushLayout
当PipelineOwner
调用了flushLayout
之后,会开始布局流程,在flushLayout
中
[-> packages/flutter/lib/src/rendering/object.dart:PipelineOwner]
void flushLayout() {
// ...
try {
while (_nodesNeedingLayout.isNotEmpty) {
// 取出需要重新布局的所有`RenderObject`节点
final List<RenderObject> dirtyNodes = _nodesNeedingLayout;
_nodesNeedingLayout = <RenderObject>[];
for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => a.depth - b.depth)) {
if (node._needsLayout && node.owner == this)
// 调用节点的_layoutWithoutResize开始布局
node._layoutWithoutResize();
}
}
} finally {
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
_nodesNeedingLayout
是一个需要重新构建的列表,它会存储所有需要重新布局的节点,一般在节点构建过程中会通过markNeedsLayout
将自己添加到待重新layout列表中。有一点比较特殊,在RendererBinding
初始化时,也会提前将根节点RenderView
添加进列表中。
[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]
void _layoutWithoutResize() {
RenderObject? debugPreviousActiveLayout;
// ...
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
// ...
}
_needsLayout = false;
markNeedsPaint();
}
2
3
4
5
6
7
8
9
10
11
12
flushLayout
中会存储所有需要布局的节点,然后调用每个节点的_layoutWithoutResize
,_layoutWithoutResize
中会调用performLayout
进行布局。
performLayout
performLayout
在RenderObject
是一个抽象方法,需要由子类实现。我们来看看在Flutter使用最多的RenderObject
子类RenderProxyBox
,它的mixin——RenderProxyBoxMixin
中是这样实现performLayout
的
[-> packages/flutter/lib/src/rendering/proxy_box.dart:RenderProxyBoxMixin]
void performLayout() {
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
} else {
size = computeSizeForNoChild(constraints);
}
}
2
3
4
5
6
7
8
9
其逻辑很简单,如果当前节点存在孩子节点,则调用孩子节点的layout
,然后将size设置为子节点的size,如果不存在子节点,则调用computeSizeForNoChild
对size进行赋值。
layout、performResize
layout
存在于RenderObject
类中,它不参与真正的布局,所以一般也不要重写此方法。它的作用是父节点通过调用child.layout
来对子节点的布局。
[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]
void layout(Constraints constraints, { bool parentUsesSize = false }) {
// ...
RenderObject? relayoutBoundary;
// 确定当前的relayoutBoundary,一般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;
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
visitChildren(_cleanChildRelayoutBoundary);
}
_relayoutBoundary = relayoutBoundary;
// ...
if (sizedByParent) {
assert(() {
_debugDoingThisResize = true;
return true;
}());
try {
performResize();
// ...
} catch (e, stack) {
_debugReportException('performResize', e, stack);
}
}
// ...
try {
performLayout();
markNeedsSemanticsUpdate();
assert(() {
debugAssertDoesMeetConstraints();
return true;
}());
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
}
// ...
_needsLayout = false;
markNeedsPaint();
}
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
为了layout
的性能考虑,layout
过程中会使用_relayoutBoundary
来优化性能。它根据下面的四个条件满足其一即可让_relayoutBoundary
等于自己:
parentUsesSize 为false
,表示子节点的布局不会影响父节点,父节点不会根据子节点的大小来调整自身sizedByParent 为true
,表示如果父节点传给子节点的约束(constraints)不变,那么子节点不会重新计算盒子大小,子节点的孩子节点的布局变化也不会影响子节点的大小,如该节点始终充满父节点。constraints.isTight 为true
,表示约束(constraints)确定后,盒子大小就唯一确定。比如当盒子的最大高度和最小高度一样,同时最大宽度和最小宽度一样时,那么盒子大小就确定了parent不是RenderObject
时,parent是AbstractNode
类型,所以还存在parent
不是RenderObject
的情况,比如SemanticsNode
(语义辅助节点)
否则_relayoutBoundary
会指向父节点的_relayoutBoundary
。在当前类调用markNeedsLayout
的时候,它会从当前向父节点遍历,直到找到节点为_relayoutBoundary
时停止,并会标记所有遍历的节点为_needsLayout=true
,如果当前类的_relayoutBoundary
节点离自己越近越好,最好就是自己。
当sizedByParent
为true时,才会调用performResize
,此时其大小在performResize
中就确定了,在后面的performLayout
方法中将不会再被修改了,这种情况下performLayout
只负责布局子节点。
RenderBox
前面说过,RenderObject
仅仅控制绘制流程,并没有具体定义盒子的size,size都是通过RenderBox
类来定义的
RenderBox
有一些比较重要的属性及方法
Size size
: 定义盒子大小BoxConstraints constraints
: 盒子约束,其中保存了盒子的最大最小高度宽度限制,由父盒子传递Size computeDryLayout()
: 此方法在Flutter2.0中被定义,用于当sizedByParent 为true
时用来计算盒子的大小,不能在它内部进行size赋值,只需要返回其计算的大小即可。无法计算盒子大小时,返回Size.zero
。如果我们自定义的RenderObject
类中sizedByParent=true
,只需要继承实现此方法来计算布局大小.bool hitTest(BoxHitTestResult result,{required Offset position})
: 用于事件的命中测试,它会遍历当前及子节点,如果事件在当前节点中发生,它会将当前节点添加到命中测试结果列表中。
确定位置及parentData
之前说layout阶段会确定盒子大小和位置,那么位置是如何保存的呢,答案就在这个parentData
里。
在盒子初始化时,父节点会调用setupParentData
来初始化子节点的parentData
[-> packages/flutter/lib/src/rendering/object.dart:RenderObject]
void setupParentData(covariant RenderObject child) {
if (child.parentData is! ParentData)
child.parentData = ParentData();
}
2
3
4
ParentData
只是一个简单的空方法,一般需要继承它来定义自己的信息,比如我们最常用的BoxParentData
,它的作用就是当我们子节点只有一个的时候,存储子节点的位置信息
[-> packages/flutter/lib/src/rendering/box.dart:BoxParentData]
class BoxParentData extends ParentData {
Offset offset = Offset.zero;
String toString() => 'offset=$offset';
}
2
3
4
5
6
我用一个我们经常使用的Center
组件的例子来看看是如何确定位置的。
Center
组件是继承的Align
组件,Align
组件是通过RenderPositionedBox
类来创建RenderObject
class Center extends Align {
const Center({ Key? key, double? widthFactor, double? heightFactor, Widget? child })
: super(key: key, widthFactor: widthFactor, heightFactor: heightFactor, child: child);
}
//...
class Align extends SingleChildRenderObjectWidget {
const Align({
Key? key,
this.alignment = Alignment.center,
this.widthFactor,
this.heightFactor,
Widget? child,
});
// ...
RenderPositionedBox createRenderObject(BuildContext context) {
return RenderPositionedBox(
alignment: alignment,
widthFactor: widthFactor,
heightFactor: heightFactor,
textDirection: Directionality.maybeOf(context),
);
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Align的构造函数中alignment
属性默认为Alignment.center
,我们看看RenderPositionedBox
是如何确定大小及位置的
[-> packages/flutter/lib/src/rendering/shifted_box.dart:RenderPositionedBox]
void performLayout() {
final BoxConstraints constraints = this.constraints;
// _widthFactor表示容器大小是子盒子的倍数,如果_widthFactor不为空或者 盒子的最大长度为最长时为true(当constraints.maxWidth == double.infinity且_widthFactor为空时,盒子宽度为子盒子实际宽度)
final bool shrinkWrapWidth = _widthFactor != null || constraints.maxWidth == double.infinity;
final bool shrinkWrapHeight = _heightFactor != null || constraints.maxHeight == double.infinity;
if (child != null) {
// 布局子节点
child!.layout(constraints.loosen(), parentUsesSize: true);
// 设置盒子大小
size = constraints.constrain(Size(shrinkWrapWidth ? child!.size.width * (_widthFactor ?? 1.0) : double.infinity,
shrinkWrapHeight ? child!.size.height * (_heightFactor ?? 1.0) : double.infinity));
// 设置子节点位置
alignChild();
} else {
size = constraints.constrain(Size(shrinkWrapWidth ? 0.0 : double.infinity,
shrinkWrapHeight ? 0.0 : double.infinity));
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
我们看到在它performLayout
阶段结束后,会调用alignChild
来设置子盒子的位置
[-> packages/flutter/lib/src/rendering/shifted_box.dart:RenderAligningShiftedBox]
void alignChild() {
_resolve();
// ...
final BoxParentData childParentData = child!.parentData! as BoxParentData;
childParentData.offset = _resolvedAlignment!.alongOffset(size - child!.size as Offset);
}
2
3
4
5
6
7
在这里,它先拿到子节点的parentData,然后对其的offset赋值,size-child.size
表示当前盒子大小减去子盒子的大小,此时只需要将Size的长宽除以2,就可以知道子盒子相对于父盒子的Offset
,然后将这个Offset
赋值给子节点的parentData
。现在,子节点的parentData
中存储了自己相对于父节点的相对位置信息了。
总结
这一篇讲了Flutter绘制流程的启动及布局过程,handleDrawFrame
会启动绘制流程,drawFrame
方法中使用PipelineOwner
来进行布局、合成、绘制、提交等流程,performLayout
是每个节点都会调用的方法,我们一般在它下面进行设置盒子大小(size
)及调用子节点的layout
方法,layout
方法也会调用performLayout
。布局是比较复杂的,子节点会影响父节点,父节点的大小又会影响子节点,所以其中引入了sizedByParent
、parentUsesSize
等属性,还引入了_relayoutBoundary
来优化布局的性能。然后通过parentData
进行存储节点位置信息,给绘制时使用。
这一步,我们的layout
过程已经完成,接下来就可以绘制了。绘制过程又做了哪些事情,又有哪些优化呢,后面我会持续更新。