在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底层原理,对我们实现自定义的控件也有帮助。至于具体如何根据布局绘制到屏幕上,后文会使用实例分析。
搞懂原理真是伤头发啊!