我们前面介绍了StatelessWidget和StatefulWidget,它们只是对其他Widget进行组合,不具备自定义绘制的能力。在需要绘制内容的场景下,我们要使用RenderObjectWidget,因为RenderObjectWidget创建的RenderObject负责布局和绘制的功能。
本文将以RenderObject为起点,梳理下Flutter的布局和绘制流程的逻辑。
RenderObject
RenderObject是渲染树Render Tree的一个节点,主要负责布局和绘制。
Flutter设计了重要的三棵树Widget Tree - Element Tree - RenderObject Tree。示例如下:
图片引用来源
RenderObjectWidget实例化的RenderObjectElement会创建 RenderObject, 所有的RenderObject会组成一颗RenderObject Tree。
abstract class RenderObject extends AbstractNode implements HitTestTarget {}
RenderObject继承自AbstractNode, AbstractNode是对树的节点的抽象:
class AbstractNode {
// 1
int get depth => _depth;
int _depth = 0;
void redepthChild(AbstractNode child) {}
// 2
Object? get owner => _owner;
Object? _owner;
void attach(covariant Object owner) {
_owner = owner;
}
void detach() {
_owner = null;
}
// 3
AbstractNode? get parent => _parent;
AbstractNode? _parent;
void adoptChild(covariant AbstractNode child) {
child._parent = this;
if (attached)
child.attach(_owner!);
redepthChild(child);
}
void dropChild(covariant AbstractNode child) {
child._parent = null;
if (attached)
child.detach();
}
}
- AbstractNode提供了三个属性和几个重要的方法:
- 节点深度
depth
属性和计算节点深度redepthChild()
方法; -
owner
和对应的关联attach()
和取消关联detach()
方法; -
parent
父节点; - 挂载子节点
adoptChild()
和卸载子节点dropChild()
方法。
abstract class RenderObject extends AbstractNode implements HitTestTarget {
// 1
ParentData? parentData;
// 2
Constraints _constraints;
// 3
RenderObject? _relayoutBoundary;
// 众多方法...
}
- RenderObject自身也有几个重要的属性:
-
parentData
父节点的插槽,父节点的一些信息可以放置在这里面供子节点使用; -
_constraints
为父节点提供的约束; -
_relayoutBoundary
是需要重新布局的边界。
- RenderObject的方法和Android的View非常类似:
功能 | RenderObject | View |
---|---|---|
布局 | performLayout() |
measure()/measure() |
绘制 | paint() |
draw() |
请求布局 | markNeedsLayout() |
requestLayout() |
请求绘制 | markNeedsPaint() |
invalidate() |
父节点/View | parent |
getParent() |
添加子节点/View | adoptChild() |
addView() |
移除子节点/View | dropChild() |
removeView() |
关联owner/Window | attach() |
onAttachedToWindow() |
取消关联owner/Window | detach() |
onDetachedFromWindow() |
事件 | hitTest() |
onTouch() |
屏幕旋转 | rotate() |
onConfigurationChanged() |
参数 | parentData |
mLayoutParams |
- RenderObject还有一个特点 --- 它定义了布局/绘制协议,但并没定义具体布局/绘制模型。
定义了布局/绘制协议就是指继承RenderObject的子类必须要实现一些方法,譬如performLayout
、paint
等;没定义具体布局/绘制模型是指没有限定使用什么坐标系,子节点可以有0个、1个还是多个等。
- Flutter提供了RenderBox和RenderSlive两个子类,他们分别对应简单的2D笛卡尔坐标模型和滚动模型。
RenderObject子类 | Constraints | ParentData |
---|---|---|
RenderBox | BoxConstraints | BoxParentData |
RenderSlive | SliverConstraints | SliverLogicalParentData |
一般情况下我们不需要直接使用RenderObject,使用RenderBox和RenderSlive这两个子类就能满足需求。
SchedulerBinding.handleDrawFrame()
我们介绍这个方法是为了介绍每次刷新的工作流程,这样有助于我们更好的理解RenderObject的相关内容。
Flutter启动流程分析那篇文章中,我们提到过window.scheduleFrame()
向Native platform发起一个刷新视图的请求后,Flutter Engine会在适当的时候调用SchedulerBinding的_handleDrawFrame
方法。
void handleDrawFrame() {
try {
// PERSISTENT FRAME CALLBACKS
_schedulerPhase = SchedulerPhase.persistentCallbacks;
for (final FrameCallback callback in _persistentCallbacks)
_invokeFrameCallback(callback, _currentFrameTimeStamp!);
} finally {
}
}
handleDrawFrame
中执行了回调函数数组persistentCallbacks中所有的回调函数。其中就包括RendererBinding中的_handlePersistentFrameCallback
方法:
<!-- RendererBinding -->
void _handlePersistentFrameCallback(Duration timeStamp) {
drawFrame();
_scheduleMouseTrackerUpdate();
}
这里的drawFrame
方法是调用的父类WidgetsBinding的方法:
<!-- WidgetsBinding -->
void drawFrame() {
try {
// 1
if (renderViewElement != null)
buildOwner!.buildScope(renderViewElement!);
// 2
super.drawFrame();
// 3
buildOwner!.finalizeTree();
} finally {
}
}
此方法代表的含义:
-
buildOwner!.buildScope(renderViewElement!)
执行的是Widget的build任务,这其中就包括StatelessWidget和StatefulWidget和RenderObjectWidget; - 调用WidgetsBinding的
drawFrame
方法; - 卸载非激活状态的Element。
WidgetsBinding的drawFrame
方法中则执行了布局和绘制等操作。
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout();
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
if (sendFramesToEngine) {
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
_firstFrameSent = true;
}
}
上面的buid/layout/paint等都和RenderObject息息相关,我们将在接下来的章节中详细介绍。
Build
接下来我们就来看看buildScope
方法触发的RenderObjectWidget的构建过程。
Element inflateWidget(Widget newWidget, dynamic newSlot) {
// 1
final Element newChild = newWidget.createElement();
// 2
newChild.mount(this, newSlot);
return newChild;
}
inflateWidget
方法主要作用:
- 先通过
createElement
方法根据Widget创建对应的Element; - 然后新建的Element调用
mount
方法,将自己挂载到Element Tree上,位置是父Element的newSlot
这个插槽。
createElement
abstract class RenderObjectWidget extends Widget {
@factory
RenderObjectElement createElement();
}
RenderObjectWidget的createElement
方法是工厂方法,真正的实现方法在子类里面。
RenderObjectWidget的子类对应的Element总结:
分类 | Widget | Element |
---|---|---|
根节点 | RenderObjectToWidgetAdapter | RootRenderObjectElement |
具有多个子节点 | MultiChildRenderObjectWidget | MultiChildRenderObjectElement |
具有一个子节点点 | SingleChildRenderObjectWidget | SingleChildRenderObjectElement |
叶子节点 | LeafRenderObjectWidget | LeafRenderObjectElement |
代码如下:
abstract class LeafRenderObjectWidget extends RenderObjectWidget {
@override
LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}
abstract class SingleChildRenderObjectWidget extends RenderObjectWidget {
@override
SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this);
}
abstract class MultiChildRenderObjectWidget extends RenderObjectWidget {
@override
MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this);
}
class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {
@override
RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);
@override
RenderObjectWithChildMixin<T> createRenderObject(BuildContext context) => container;
}
mount
我们来看RenderObjectElement的mount
方法实现:
void mount(Element? parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);
attachRenderObject(newSlot);
_dirty = false;
}
-
super.mount
的作用主要是记录下parent
,slot
和depth
等值; -
widget.createRenderObject
创建了一个renderObject; -
attachRenderObject
就是将这个parentData挂载到RenderObject Tree上,并且更新RenderObject的parentData。
void attachRenderObject(dynamic newSlot) {
_slot = newSlot;
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertRenderObjectChild(renderObject, newSlot);
final ParentDataElement<ParentData>? parentDataElement = _findAncestorParentDataElement();
if (parentDataElement != null)
_updateParentData(parentDataElement.widget);
}
insertRenderObjectChild
renderObject
通过insertRenderObjectChild
方法挂载到RenderObject Tree上,那具体的实现是如何实现的呢?
能实现挂载RenderObject的只能是SingleChildRenderObjectElement和MultiChildRenderObjectElement。我们分别来看看:
SingleChildRenderObjectElement
void insertRenderObjectChild(RenderObject child, dynamic slot) {
final RenderObjectWithChildMixin<RenderObject> renderObject = this.renderObject as RenderObjectWithChildMixin<RenderObject>;
renderObject.child = child;
}
对child
进行赋值:
set child(ChildType? value) {
if (_child != null)
dropChild(_child!);
_child = value;
if (_child != null)
adoptChild(_child!);
}
如果已经有_child
先将其从卸载,然后将新的Child挂载上。
void dropChild(RenderObject child) {
child._cleanRelayoutBoundary();
child.parentData!.detach();
child.parentData = null;
super.dropChild(child);
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
}
void adoptChild(RenderObject child) {
setupParentData(child);
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
super.adoptChild(child);
}
这两个方法主要是对_child
和parentData
重新赋值,然后通过markNeedsLayout
,markNeedsCompositingBitsUpdate
和markNeedsSemanticsUpdate
标记需要重新布局,需要合成和语义的更新。
MultiChildRenderObjectElement
void insertRenderObjectChild(RenderObject child, IndexedSlot<Element?> slot) {
final ContainerRenderObjectMixin<RenderObject, ContainerParentDataMixin<RenderObject>> renderObject = this.renderObject;
renderObject.insert(child, after: slot.value?.renderObject);
}
void insert(ChildType child, { ChildType? after }) {
adoptChild(child);
_insertIntoChildList(child, after: after);
}
MultiChildRenderObjectElement中的实现方式类似,只是这次不是简单的赋值,而是将child
添加到Render Tree中去,然后进行各种标记。
_insertIntoChildList
方法添加的逻辑如下:
- 依附的兄弟节点为空,插入在第一个子节点;
- 依附的兄弟节点没有相关联的下一个兄弟节点,插入在兄弟节点队尾;
- 依附的兄弟节点有相关联的下一个兄弟节点,插入在兄弟节点中间。
inflateWidget递归
由于SingleChildRenderObjectWidget和MultiChildRenderObjectWidget含有子节点,所以需要对子Widget进行构建。
<!-- SingleChildRenderObjectWidget -->
void mount(Element? parent, dynamic newSlot) {
super.mount(parent, newSlot);
_child = updateChild(_child, widget.child, null);
}
Element? updateChild(Element? child, Widget? newWidget, dynamic newSlot) {
final Element newChild;
// ... 省略Widget更新的逻辑
newChild = inflateWidget(newWidget, newSlot);
return newChild;
}
<!-- MultiChildRenderObjectElement -->
void mount(Element? parent, dynamic newSlot) {
super.mount(parent, newSlot);
final List<Element> children = List<Element>.filled(widget.children.length, _NullElement.instance, growable: false);
Element? previousChild;
for (int i = 0; i < children.length; i += 1) {
final Element newChild = inflateWidget(widget.children[i], IndexedSlot<Element?>(i, previousChild));
children[i] = newChild;
previousChild = newChild;
}
_children = children;
}
这样,接下来的操作就进入了递归流程了,和上面介绍的流程内容一模一样了。
流程示意图:
markNeedsLayout
我们上面看到了,adoptChild
和adoptChild
的方法中都调用了markNeedsLayout
的相关内容:
abstract class RenderObject extends AbstractNode with DiagnosticableTreeMixin implements HitTestTarget {
bool _needsLayout = true;
RenderObject? _relayoutBoundary;
void markNeedsLayout() {
// 1
if (_needsLayout) {
return;
}
// 2
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
// 3
_needsLayout = true;
if (owner != null) {
owner!._nodesNeedingLayout.add(this);
owner!.requestVisualUpdate();
}
}
}
}
RenderObject有_needsLayout
属性,标记是否需要重新布局,还有一个_relayoutBoundary
布局边界属性,表示开始重新布局的节点,这样就不需要每次整个渲染树的节点都进行重新布局。
markNeedsLayout
代表的含义:
- 如果已经标记
_needsLayout
,直接返回; - 如果
_relayoutBoundary
布局边界不是自身,让父节点递归调用markNeedsLayout
方法; - 如果
_relayoutBoundary
布局边界是自身,标记_needsLayout
, 并将自身加入到PipelineOwner的_nodesNeedingLayout
列表中,等待PipelineOwner进行重新布局; - 请求PipelineOwner进行更新。
您可能会有疑问_relayoutBoundary
是在什么时候赋值的?有两个地方赋值:
- 第一次布局的时候,
_relayoutBoundary
会被标记为RenderView,即自身,然后从根节点进行布局;
void scheduleInitialLayout() {
_relayoutBoundary = this;
owner!._nodesNeedingLayout.add(this);
}
-
layout()
方法中RenderObject也会重新标记_relayoutBoundary
,一般情况下也是自身。
void layout(Constraints constraints, { bool parentUsesSize = false }) {
// ...
_relayoutBoundary = relayoutBoundary;
}
markNeedsCompositingBitsUpdate
bool _needsCompositingBitsUpdate = false;
void markNeedsCompositingBitsUpdate() {
if (_needsCompositingBitsUpdate)
return;
_needsCompositingBitsUpdate = true;
if (parent is RenderObject) {
final RenderObject parent = this.parent! as RenderObject;
if (parent._needsCompositingBitsUpdate)
return;
if (!isRepaintBoundary && !parent.isRepaintBoundary) {
parent.markNeedsCompositingBitsUpdate();
return;
}
}
if (owner != null)
owner!._nodesNeedingCompositingBitsUpdate.add(this);
}
RenderObject有_needsCompositingBitsUpdate
属性,标记是否需要合成。
markNeedsCompositingBitsUpdate
的逻辑如下:
- 如果已经标记
_needsCompositingBitsUpdate
,直接返回; - 如果未标记
_needsCompositingBitsUpdate
先标记,然后标记父节点或者向父类递归调用markNeedsCompositingBitsUpdate
直到标记成功为止; - 将自身加入到PipelineOwner的
_nodesNeedingCompositingBitsUpdate
列表中。
结果就是将isRepaintBoundary
这个节点下的所有节点都标记为_needsCompositingBitsUpdate
,然后加入到PipelineOwner的_nodesNeedingCompositingBitsUpdate
列表中。
flushLayout
前面所有的逻辑只能算是buildScope
方法触发的Build阶段。接下来我们就进入了Layout阶段了。
<!-- PipelineOwner -->
void flushLayout() {
try {
while (_nodesNeedingLayout.isNotEmpty) {
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)
node._layoutWithoutResize();
}
}
} finally {
}
}
PipelineOwner的flushLayout
其实很简单,让_nodesNeedingLayout
中的所有RenderObject按照广度优先遍历调用_layoutWithoutResize
方法。
<!-- RenderObject -->
void _layoutWithoutResize() {
try {
performLayout();
} catch (e, stack) {
}
_needsLayout = false;
markNeedsPaint();
}
我们来看看_ScaffoldLayout
中的实现:
void performLayout() {
size = _getSize(constraints);
delegate._callPerformLayout(size, firstChild);
}
void _callPerformLayout(Size size, RenderBox? firstChild) {
performLayout(size);
}
void performLayout(Size size) {
layoutChild(_ScaffoldSlot.body, bodyConstraints);
positionChild(_ScaffoldSlot.body, Offset(0.0, contentTop));
}
Size layoutChild(Object childId, BoxConstraints constraints) {
child!.layout(constraints, parentUsesSize: true);
return child.size;
}
void positionChild(Object childId, Offset offset) {
final MultiChildLayoutParentData childParentData = child!.parentData! as MultiChildLayoutParentData;
childParentData.offset = offset;
}
根据一系列调用,生成一个BoxConstraints传递给每个子节点,子节点调用layout()
进行测量和布局。
void layout(Constraints constraints, { bool parentUsesSize = false }) {
// 1
RenderObject? relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
}
// 2
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {
return;
}
// 3
_constraints = constraints;
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
visitChildren(_cleanChildRelayoutBoundary);
}
_relayoutBoundary = relayoutBoundary;
// 4
if (sizedByParent) {
try {
performResize();
} catch (e, stack) {
}
}
try {
// MultiChildLayoutDelegate --- performLayout & _callPerformLayout & performLayout & child!.layout
// 5
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
}
_needsLayout = false;
markNeedsPaint();
}
- 首先根据
!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject
的条件进行_relayoutBoundary
的计算,一般情况下会指向自身;
parentUsesSize表示是否父节点的大小依赖子节点,sizedByParent表示大小由父类决定,constraints.isTight表示大小是固定的。
- 根据
!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary
确实定是否需要重新布局,不需要直接返回; - 记录下
_constraints
; - 如果依赖父节点的大小,则根据
_constraints
计算出size
尺寸, ; -
performLayout
根据根据_constraints
计算出size
尺寸,然后调用子类的layout
方法。
总结:
performLayout
的逻辑就是通过layout
方法将Constraints逐步往下传递,得到Size逐步向上传递,然后父节点通过给parentData赋值确定对子节点的位置摆放。
flushCompositingBits
void flushCompositingBits() {
_nodesNeedingCompositingBitsUpdate.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
for (final RenderObject node in _nodesNeedingCompositingBitsUpdate) {
if (node._needsCompositingBitsUpdate && node.owner == this)
node._updateCompositingBits();
}
_nodesNeedingCompositingBitsUpdate.clear();
}
遍历_nodesNeedingCompositingBitsUpdate
中的每个RenderObject,然后调用_updateCompositingBits
方法。
void _updateCompositingBits() {
if (!_needsCompositingBitsUpdate)
return;
final bool oldNeedsCompositing = _needsCompositing;
_needsCompositing = false;
visitChildren((RenderObject child) {
child._updateCompositingBits();
if (child.needsCompositing)
_needsCompositing = true;
});
if (isRepaintBoundary || alwaysNeedsCompositing)
_needsCompositing = true;
if (oldNeedsCompositing != _needsCompositing)
markNeedsPaint();
_needsCompositingBitsUpdate = false;
}
这个方法就是找到isRepaintBoundary为true的节点及其父节点,将它们的_needsCompositing为true
设置为true;
isRepaintBoundary
上面提到的isRepaintBoundary是RenderObject的一个属性,默认是false。表示的是否需要独立渲染。
<!-- RenderObject -->
bool get isRepaintBoundary => false;
如果需要独立渲染则需要覆盖这个值为true,例如RenderView的值就为true。
<!-- RenderView -->
@override
bool get isRepaintBoundary => true;
flushPaint
按照逻辑flushPaint
之前应该先会调用markNeedsPaint
,我们回过头来看看发现确实如此,有很多地方都频繁的调用的markNeedsPaint
,譬如_layoutWithoutResize
,layout
,_updateCompositingBits
等方法中都有出现,只是前面我们特意忽略了这个逻辑。
void markNeedsPaint() {
if (_needsPaint)
return;
_needsPaint = true;
if (isRepaintBoundary) {
if (owner != null) {
owner!._nodesNeedingPaint.add(this);
owner!.requestVisualUpdate();
}
} else if (parent is RenderObject) {
final RenderObject parent = this.parent! as RenderObject;
parent.markNeedsPaint();
} else {
if (owner != null)
owner!.requestVisualUpdate();
}
}
- 如果isRepaintBoundary为true, 则加入到
_nodesNeedingPaint
数组中,然后请求界面更新; - 如果isRepaintBoundary为false,则向父节点遍历;
- 如果到了根节点,就直接请求界面更新;
我们接下来看看flushPaint的代码:
void flushPaint() {
try {
final List<RenderObject> dirtyNodes = _nodesNeedingPaint;
_nodesNeedingPaint = <RenderObject>[];
for (final RenderObject node in dirtyNodes..sort((RenderObject a, RenderObject b) => b.depth - a.depth)) {
if (node._needsPaint && node.owner == this) {
if (node._layer!.attached) {
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
}
}
} finally {
}
}
从下往上遍历_nodesNeedingPaint
数组,然后从上往下进行绘制。
接下来我们看看是如何绘制的:
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
);
}
static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext? childContext,
}) {
OffsetLayer? childLayer = child._layer as OffsetLayer?;
if (childLayer == null) {
child._layer = childLayer = OffsetLayer();
} else {
childLayer.removeAllChildren();
}
childContext ??= PaintingContext(child._layer!, child.paintBounds);
// 重点
child._paintWithContext(childContext, Offset.zero);
childContext.stopRecordingIfNeeded();
}
PaintingContext的类方法repaintCompositedChild
接收了RenderObject对象,最后结果是这个RenderObject对象调用_paintWithContext
方法,参数是PaintingContext对象和偏移量Offset。
void _paintWithContext(PaintingContext context, Offset offset) {
if (_needsLayout)
return;
_needsPaint = false;
try {
paint(context, offset);
} catch (e, stack) {
}
}
<!-- PaintingContext -->
Canvas? _canvas;
_paintWithContext
方法调用的是RenderObject子类对象的paint(PaintingContext context, Offset offset)
进行绘制,绘制在PaintingContext的Canvas上。
总结
本文主要分析了RendObject、Build、Layout、Paint等相关内容,后续继续分析其他相关内容。