在Widget
篇中,提到了RenderObject
,那么RenderObject
到底是个啥?咱们来盘一盘!
官方解释
An object in the render tree.
render树上的一个对象。
Flutter门前有四棵树,一棵是Widget
树,一棵是Element
树,一棵是Render
树,还有一棵是layer
树。
如果Widget
树是一张图纸,那么Render
树就是这张图纸对应的流水线。RenderObject
就是这条流水线上的操作员工。
我们在顶层配置好Widget
树后,最终render
树负责在手机屏幕上表现出你想要的画面。
阅读源码
美好的一天应当从阅读源码开始!
abstract class RenderObject extends AbstractNode
with DiagnosticableTreeMixin
implements HitTestTarget {}
可以看到RenderObject
也是链表结构,混入了DiagnosticableTreeMixin
树状结构的特性,并且实现了命中测试抽象类。
去掉debug和assert的代码,RenderObject
的结构功能清晰可见。
Layout 模块
abstract class RenderObject {
// LAYOUT 模块
// 传递信息给child的存储容器 通常为偏移量Offset
ParentData parentData;
// 此方法,在child加入到child列表之前,把parentData传递给child
void setupParentData(covariant RenderObject child) {
if (child.parentData is! ParentData)
child.parentData = ParentData();
}
// 添加一个render object 作为自己的child
@override
void adoptChild(RenderObject child) {
// 设置parentData
setupParentData(child);
// 标记更新
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
super.adoptChild(child);
}
// 删除一个render object 作为自己的child
@override
void dropChild(RenderObject child) {
// 清除边界
child._cleanRelayoutBoundary();
// 失去parentdata访问权限
child.parentData.detach();
child.parentData = null;
super.dropChild(child);
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
}
// 遍历children
void visitChildren(RenderObjectVisitor visitor) { }
// render tree的管理者
@override
PipelineOwner get owner => super.owner;
// 告诉其owner将其插入render tree 并初次标记需要计算layout并且重绘
@override
void attach(PipelineOwner owner) {
super.attach(owner);
if (_needsLayout && _relayoutBoundary != null) {
_needsLayout = false;
markNeedsLayout();
}
if (_needsCompositingBitsUpdate) {
_needsCompositingBitsUpdate = false;
markNeedsCompositingBitsUpdate();
}
if (_needsPaint && _layer != null) {
_needsPaint = false;
markNeedsPaint();
}
if (_needsSemanticsUpdate && _semanticsConfiguration.isSemanticBoundary) {
_needsSemanticsUpdate = false;
markNeedsSemanticsUpdate();
}
}
// 是否需要布局
bool _needsLayout = true;
// 记录布局边界在哪
RenderObject _relayoutBoundary;
// parent的约束
@protected
Constraints get constraints => _constraints;
Constraints _constraints;
//标记需要重新layout
void markNeedsLayout() {
// 如果自己不是边界,则让parent.markNeedsLayout处理 一直推到边界
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
// owner 把自己加入layout的脏表 请求刷新
_needsLayout = true;
if (owner != null) {
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
}
}
}
// 遍历清除非边界child relayout边界记录 当边界发生变化时,会调用此方法清除边界缓存
void _cleanRelayoutBoundary() {
if (_relayoutBoundary != this) {
_relayoutBoundary = null;
_needsLayout = true;
visitChildren((RenderObject child) {
child._cleanRelayoutBoundary();
});
}
}
// 仅layout 不重新测量大小
void _layoutWithoutResize() {
performLayout();
markNeedsSemanticsUpdate();
_needsLayout = false;
markNeedsPaint();
}
void layout(Constraints constraints, { bool parentUsesSize = false }) {
RenderObject relayoutBoundary;
// 确定边界
// 满足以下四项条件 中的一项 则自己为边界
// parentUsesSize parent是否关心自己的大小
// sizedByParent 由parent确认大小
// constraints.isTight 受严格约束
// parent 不为 RenderObject 即自己为root节点
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
// 否则使用parent的边界
final RenderObject parent = this.parent;
relayoutBoundary = parent._relayoutBoundary;
}
_constraints = constraints;
// 如果边界发生变化 则遍历清空所有已记录的边界 重新设置
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
visitChildren((RenderObject child) {
child._cleanRelayoutBoundary();
});
}
_relayoutBoundary = relayoutBoundary;
// sizedByParent 为true 时 才会调用performResize 确定大小
// 否则在performLayout中确定size
if (sizedByParent) {
performResize();
}
// 计算layout
performLayout();
markNeedsSemanticsUpdate();
_needsLayout = false;
markNeedsPaint();
}
// size是否只由parent的约束决定 默认为false 永远不会错
// 当为true时,确定size的工作要在performResize中计算
@protected
bool get sizedByParent => false;
// 子类重写此方法 根据约束 计算自身的size
// 不能直接调用此方法 而是调用layout方法间接调用 只有sizedByParent为true时才会执行
@protected
void performResize();
// 子类重写此方法
// 不能直接调用此方法 而是调用layout方法间接调用
// 作用1:当sizedByParent为false时 根据适应child计算自身size
// 作用2:遍历调用child.layout 确定child的size和offsets
@protected
void performLayout();
}
划一下重点:
RenderObject
作为Render Tree
上的一个对象,但是自己却不能直接改变这棵大树。一举一动都需要传达给PipelineOwner
这位管理者去执行。PipelineOwner
管理着几个污污的(dirty)小本本,记录了需要变化的节点,根据这些小本本去实际更新Render Tree
。当RenderObject
需要发生改变时,通知owner把自己写到小本本上(标记为dirty)。可以理解为军训过程中,有什么问题报告给教官,教官统一安排。parent
通过setupParentData
方法,传递parentData
,通常为layout offset
。performResize
方法需要子类重写。计算自身的大小performLayout
方法需要子类重写。
- 当sizedByParent为false时,计算size。
- 2.遍历调用child.layout,确定child的size和offsets。
- 核心方法是
layout
。
确定`_relayoutBoundary`布局边界
->调用`performResize`和`performLayout`方法计算大小和位置
->重绘
由于这个流程满足大多数场景,因此当我们真正开发时,只关心重写performResize
和performLayout
的实现,而不会去重写layout
方法。
Paint 模块
abstract class RenderObject {
// PAINTING 模块
RenderObject() {
// 是否需要混合图层 = 是重绘边界 || 总是混合图层
_needsCompositing = isRepaintBoundary || alwaysNeedsCompositing;
}
// 当前是否为重绘边界
bool get isRepaintBoundary => false;
// 是否总是新建图层然后合并到原图层
@protected
bool get alwaysNeedsCompositing => false;
// 缓存的layer
ContainerLayer _layer;
// 是否_needsCompositing的值需要设置
bool _needsCompositingBitsUpdate = false;
// 标记_needsCompositing的值需要重新设置
void markNeedsCompositingBitsUpdate() {
if (owner != null)
owner._nodesNeedingCompositingBitsUpdate.add(this);
}
// 是否在一个新的图层绘制然后合并到祖先图层
// true:在新图层绘制 但是新图层会优先使用缓存图层 以提高性能
// false:不使用新图层 此时缓存图层一定要置null
// 当前为repaintBoundary时 _needsCompositing=true 并且会自动给缓存layer赋值为新的OffsetLayer 在此layer上绘制后 合并到祖先图层
bool _needsCompositing;
// 遍历设置需要使用图层混合
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;
}
// 是否需要绘制
bool _needsPaint = true;
// 标记需要重绘
void markNeedsPaint() {
_needsPaint = true;
// 如果当前为repaintBoundary 则通知owner需要重绘
if (isRepaintBoundary) {
if (owner != null) {
owner._nodesNeedingPaint.add(this);
owner.requestVisualUpdate();
}
}
// 如果当前不为root节点 则让parent判断
else if (parent is RenderObject) {
final RenderObject parent = this.parent;
parent.markNeedsPaint();
}
// 若当前为root节点 则直接通知owner重绘
else {
if (owner != null)
owner.requestVisualUpdate();
}
}
// 根节点初始化 见[RenderView]
void scheduleInitialPaint(ContainerLayer rootLayer) {
_layer = rootLayer;
owner._nodesNeedingPaint.add(this);
}
// 替换rootLayer 只有root节点才会调用
// 当设备的像素比device pixel ratio变化时 可能会调用此方法
void replaceRootLayer(OffsetLayer rootLayer) {
_layer.detach();
_layer = rootLayer;
markNeedsPaint();
}
// 子类重写此方法 在对应的offset完成真正的绘制操作
// 不可直接调用此方法 而是用markNeedsPaint标记 让owner去处理
// 如果只想绘制一个child 则用PaintingContext.paintChild 接口的方式去操作 避免直接操作render object
void paint(PaintingContext context, Offset offset) { }
}
本模块中最重要的一点就是needsCompositing
这个变量。这个变量决定是否在新的layer
上绘制。在构造器中可以看到,它由isRepaintBoundary
和alwaysNeedsCompositing
决定。isRepaintBoundary
这个可以通过RepaintBoundary
包裹修改其为true
。alwaysNeedsCompositing
可以由子类修改。
因此我们可以通过使用RepaintBoundary
这个控件达到局部重绘的目的,以提高性能。
SEMANTICS 语义化模块 和 HIT TESTING 命中测试模块
语义化即Semantics,主要是提供给读屏软件的接口,也是实现辅助功能的基础,通过语义化接口可以让机器理解页面上的内容,对于有视力障碍用户可以使用读屏软件来理解UI内容。除非有特殊的需求,一般接触不到。
通过以下方法,处理命中测试结果。
// 重写此方法处理事件
@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) { }
RenderObject
中还有两个比较重要的方法,也提一下。
// 是否重装 只在debug模式下生效 也就是开发中的hot reload效果
void reassemble() {
// 标记为需要重新layout
markNeedsLayout();
// 标记重新设置是否使用新图层
markNeedsCompositingBitsUpdate();
// 标记需要重绘
markNeedsPaint();
// 标记语义化需要更新
markNeedsSemanticsUpdate();
// 遍历child的reassemble方法 全部标记一遍
visitChildren((RenderObject child) {
child.reassemble();
});
}
// 是否显示在屏幕上 viewport中会使用到
void showOnScreen({
RenderObject descendant,
Rect rect,
Duration duration = Duration.zero,
Curve curve = Curves.ease,
}) {
if (parent is RenderObject) {
final RenderObject renderParent = parent;
renderParent.showOnScreen(
descendant: descendant ?? this,
rect: rect,
duration: duration,
curve: curve,
);
}
}
PipelineOwner
The pipeline owner manages the rendering pipeline.
整个渲染流程的管理者,具体表现在如下几个方法:
- flushLayout
遍历需要relayout的render object节点,调用_layoutWithoutResize()
重写计算布局
- flushCompositingBits
遍历需要CompositingBitsUpdate的节点,调用_updateCompositingBits()
方法更新needsCompositing
。通常在flushLayout
和flushPaint
之间执行。
- flushPaint
遍历需要repaint的节点,通过PaintingContext
调用子render object
的_paintWithContext
方法触发paint
绘制。
- flushSemantics
更新语义化
RenderBox
RenderBox
是RenderObject
的子类,是在2D笛卡尔坐标系下对RenderObject
的进一步封装。它主要封装了如下几个功能点:
- parentdata是
BoxParentData
只有offset
属性 默认为Offset.zero
- 使用
BoxConstraints
作为其约束 - 使用
size
记录其大小 - 测量自身最大最小宽高
- 测量基线
- 实现默认的命中测试方案
- 混入
RenderObjectWithChildMixin
单个child的实现->SingleChildRenderObjectWidget
- 混入
ContainerRenderObjectMixin
多个children的实现->MultiChildRenderObjectWidget
开发App在笛卡尔坐标系上进行绘制,所以继承RenderBox
就可以满足大部分场景了。
总结
本节主要记录了RenderObject
主要的功能和方法,理解这些内容可以帮助我们更好的理解Flutter UI底层原理,对我们实现自定义的控件也有帮助。至于具体如何根据布局绘制到屏幕上,后文会使用实例分析。
搞懂原理真是伤头发啊!