RenderObject及其他
RenderObject
还是老样子,我们看看注释 这个RenderObject的注释也多我随便翻译了点 剩下的老哥可以自己去看看
/// 渲染树中的一个对象
///
/// RenderObject类结构是渲染库的核心。
///
/// RenderObject 有父节点,还有一个parentData代表其父节点给子节点的特殊数据,如子节点在父节点中
/// 的位置 RenderObject 也实现了基本的布局和画笔协议
///
/// RenderObject类没有定义子节点的模型,如有可以存几个子节点等 同时也没有定义坐标系统或者详细的布
/// 局协议
///
/// 它的子类RenderBox布局使用笛卡尔坐标系
///
/// ## 编写RenderObject子类
///
/// 大多数情况下 RenderObject过于抽象了,建议直接从RenderBox继承,但如果你不想用笛卡尔坐标系,那
/// 就要继承RenderObject了。这允许它通过使用[Constraints]的新子类而不是使用[BoxConstraints]
/// 来定义自己的布局协议,并且可能使用一组全新的对象和值来表示输出的结果而不仅仅是[Size]。提高灵活
/// 性的代价是不能依赖[RenderBox]的功能。例如,[RenderBox]实现了一个内在的大小调整协议,允许您在
/// 不完全布局的情况下测量子项,这样如果该子项改变大小,父项将再次布局(考虑新的维度)的孩子)。这是
/// 一个微妙且容易出错的功能。
///
/// ### 布局
///
/// 一个布局协议要实现Constraints 查看Constraints的注释了解如何实现
总的来说,就是用于渲染的对象,但是很多方法并没有实现,建议直接从他的子类开始继承,不过还是比较重要,下面我们分块分析一下它的代码,比较多,会省略一些
先看几个比较基本的
void reassemble() {//更新整个节点及其子树节点,一般用在热重载时,不推荐正常开发使用
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsPaint();
markNeedsSemanticsUpdate();
visitChildren((RenderObject child) {
child.reassemble();
});
}
@override
void adoptChild(RenderObject child) {//添加一个child
assert(_debugCanPerformMutations);
assert(child != null);
setupParentData(child);//设置parentdata
markNeedsLayout();//需要重新layout
markNeedsCompositingBitsUpdate();//需要更新bit
markNeedsSemanticsUpdate();//更新辅助功能
super.adoptChild(child);//加入child节点
}
@override
void dropChild(RenderObject child) {
assert(_debugCanPerformMutations);
assert(child != null);
assert(child.parentData != null);
child._cleanRelayoutBoundary();
child.parentData.detach();
child.parentData = null;
super.dropChild(child);
markNeedsLayout();
markNeedsCompositingBitsUpdate();
markNeedsSemanticsUpdate();
}
void visitChildren(RenderObjectVisitor visitor) { }//遍历child,需要子类实现
下面就按照整个更新的顺序来分析一下各种方法,首先就是布局相关的
Layout
ParentData parentData;
/// 重写这个方法给child设置data 我感觉有点像是layoutparams之类
void setupParentData(covariant RenderObject child) {
assert(_debugCanPerformMutations);
if (child.parentData is! ParentData)
child.parentData = ParentData();
}
//此方法标记需要重新布局
void markNeedsLayout() {
assert(_debugCanPerformMutations);
if (_needsLayout) {//已经标记过了
assert(_debugSubtreeRelayoutRootAlreadyMarkedNeedsLayout());
return;
}
assert(_relayoutBoundary != null);
if (_relayoutBoundary != this) {//如果自己不是重布局的边界
markParentNeedsLayout();//让父布局标记,就是调用了parent的markNeedsLayout方法
} else {
_needsLayout = true;//添加标记
if (owner != null) {
assert(() {
if (debugPrintMarkNeedsLayoutStacks)
debugPrintStack(label: 'markNeedsLayout() called for $this');
return true;
}());
owner._nodesNeedingLayout.add(this);//把自己添加到owner的needinglayout列表
owner.requestVisualUpdate();//请求重绘
}
}
}
//注释中说明,如果sizedByParent属性变化引起重布局,要调用markNeedsLayoutForSizedByParentChange方法
void markNeedsLayoutForSizedByParentChange() {
markNeedsLayout();//就是自己和父布局都需要标记重布局
markParentNeedsLayout();
}
//这个sizedByParent属性是什么呢,等一下会讲到,先接着往下看
void _cleanRelayoutBoundary() {//清除重布局的区域
if (_relayoutBoundary != this) {
_relayoutBoundary = null;
_needsLayout = true;
visitChildren((RenderObject child) {
child._cleanRelayoutBoundary();
});
}
}
void scheduleInitialLayout() {//初始化布局,初始一些属性
assert(attached);
assert(parent is! RenderObject);
assert(!owner._debugDoingLayout);
assert(_relayoutBoundary == null);
_relayoutBoundary = this;
assert(() {
_debugCanParentUseSize = false;
return true;
}());
owner._nodesNeedingLayout.add(this);
}
下面我们再把关注点放回layout这里,首先看一下Layout方法,这个这个方法有点长,我们先看一下方法注释
/// 计算渲染对象的布局
/// 此方法是父节点请求子节点更新布局信息的主入口,父节点传入用于通知子节点允许如何布局的constraints对象,子节点需要遵守此constraints的描述
/// 如果父节点需要读取子级布局期间计算的信息,则父节点必须为“parentUsesSize”传递true。在这种情况下,只要子节点被标记为需要布局,父节点就会被标记为需要布局,因为父节点的布局信息取决于子节点的布局信息。如果使用默认值false,则子节点可以更改其布局信息(受限于给定的约束),而不通知父节点。
/// 子类不应该直接重写layout方法,而是重写performResize或performLayout,layout方法会把真正的布局工作委托给这两个方法处理
/// 父亲节点的performLayout方法应无条件地调用其所有子节点的layout。如果孩子不需要做任何工作来更新其布局信息,那么layout方法可以提前返回。
注释中提到了一个抽象类,constraints,我们先简单关注它的两个属性
/// Whether there is exactly one size possible given these constraints
bool get isTight;//官方注释说的很清楚,名字我觉得也挺恰当,紧的。。。
/// Whether the constraint is expressed in a consistent manner.
bool get isNormalized;
然后就是我们上面提到的sizedByParent,我们看看注释是怎么说的
/// constarints 是否是size计算算法的唯一输入
/// 返回false没有问题,但如果返回true则在计算size是更加高效,因为我们在constarints没有改变的时候会直接返回
/// 一般情况子类会返回的值不变。如果这个值变了,子类需要调用markNeedsLayoutForSizedByParentChange方法
@protected
bool get sizedByParent => false;
下面我们看一下方法实现代码
void layout(Constraints constraints, { bool parentUsesSize = false }) {
···省略一些断言
RenderObject relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;//这种情况布局边界就是自己,更新也不需要通知父节点
} else {
final RenderObject parent = this.parent;//布局边界依赖父节点的布局边界
relayoutBoundary = parent._relayoutBoundary;//
}
···
if (!_needsLayout && constraints == _constraints && relayoutBoundary == _relayoutBoundary) {//如果_needsLayout=false并且布局约束和布局边界都没有变
···
return;//那就什么都不需要处理
}
_constraints = constraints;//设置布局约束和布局边界
_relayoutBoundary = relayoutBoundary;
···
if (sizedByParent) {//如果constarints是size计算算法的唯一输入
···
performResize();//调用performResize
···
}
RenderObject debugPreviousActiveLayout;
···
performLayout();//调用performLayout
markNeedsSemanticsUpdate();//标记需要更新语意
···
_needsLayout = false;
markNeedsPaint();//标记需要绘制
}
而performResize/performLayout是两个空方法
可见在layout中只是标记范围以及判断是否真的需要重布局等,布局的功能并没有实现,等待子类实现
另外还有一个方法
void _layoutWithoutResize() {//调用顺序和layout一样,在不resize的情况下计算布局
···
try {
performLayout();
markNeedsSemanticsUpdate();
} catch (e, stack) {
_debugReportException('performLayout', e, stack);
}
···
_needsLayout = false;
markNeedsPaint();
}
那么上面两个方法有什么区别呢?查看源码发现在父布局performlayout计算layout时会使用layout对子节点进行布局计算,而在drawframe中,会对被标记需要重布局的节点调用_layoutWithoutResize计算,具体原因还不是很清楚
到此为止,layout相关的流程就走完了,下面我们看第二部分
painting
看这一部分要先了解其他的几个类,我们依次看一下
首先就是
Layer及Scene
还记得之前看的drawframe流程么,有一个方法用于发送数据给GPU
renderView.compositeFrame();//this sends the bits to the GPU
这个方法是renderview独有的,先点进去看一下
void compositeFrame() {
```
final ui.SceneBuilder builder = ui.SceneBuilder();//通过ui方法获取一个sceneBuilder
final ui.Scene scene = layer.buildScene(builder);//通过当前RenderView的图层构建场景
if (automaticSystemUiAdjustment)
_updateSystemChrome();
_window.render(scene);//window渲染场景
scene.dispose();
```
}
可以看到,实际上是通过Renderview的layer构建一个场景给window渲染的,具体暂时就不深入了
下面看一下layer如何构建的scene
OffsetLayer:://支持偏移的layer
ui.Scene buildScene(ui.SceneBuilder builder) {
···
updateSubtreeNeedsAddToScene();//更新需要添加的子树
addToScene(builder);//添加到场景
final ui.Scene scene = builder.build();//构建场景
···
return scene;
}
=====
ContainerLayer:://容器layer
@override
void updateSubtreeNeedsAddToScene() {//递归判断设置需要添加的标记位
super.updateSubtreeNeedsAddToScene();
Layer child = firstChild;
while (child != null) {
child.updateSubtreeNeedsAddToScene();
_subtreeNeedsAddToScene = _subtreeNeedsAddToScene || child._subtreeNeedsAddToScene;
child = child.nextSibling;//实际上layer会组成树的形式
}
}
Layer:://Layer基类
@visibleForTesting
void updateSubtreeNeedsAddToScene() {
_subtreeNeedsAddToScene = _needsAddToScene || alwaysNeedsAddToScene;
}
=====
OffsetLayer::
@override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
final ui.EngineLayer engineLayer = builder.pushOffset(layerOffset.dx + offset.dx, layerOffset.dy + offset.dy);//底层layer偏移
addChildrenToScene(builder);
builder.pop();
return engineLayer;
}
ContainerLayer::
void addChildrenToScene(ui.SceneBuilder builder, [ Offset childOffset = Offset.zero ]) {
Layer child = firstChild;
while (child != null) {//遍历child,一次添加
if (childOffset == Offset.zero) {
child._addToSceneWithRetainedRendering(builder);//todo 这个我不是很清楚 sorry 有没有大佬说一下TT
} else {
child.addToScene(builder, childOffset);
}
child = child.nextSibling;
}
}
Layer ::
@protected //基类的addToScene并没有实现,我们等一下就知道了
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]);
看到这里我们知道了,实际上是通过吧Layer树构建成scene传递给window进行渲染的。那么我们的Layer又是怎么来的呢,刚才看到的基类的addToScene并没有实现,一般情况下又是如何实现的?我们看看下面的概念
PaintingContext及ClipContext
首先是ClipContext 看名字就知道,它提供了剪切方法,实际也只提供了这么一个方法=。=有几种方案
[none], 默认,如果内容不溢出小部件边界,不支付剪辑的任何性能成本。
[hardEdge],这是最快的剪辑,但保真度较低。
[antiAlias],比[hardEdge]慢一点,但边缘平滑。
[antiAliasWithSaveLayer],比[antiAlias]慢得多,应该很少使用。
而PaintContext内容相对比较多,暂时先简单了解一下,看一下注释
///一个绘制的地方。
///
///RenderObject不是直接持有canvas,而是使用PaintContext进行绘制。PaintContext有一个Canvas,它接收单独的绘制操作,还具有绘制子RenderObject的功能。
///
///绘制子RenderObject时,PaintContext持有的canvas可以更改,因为绘制子节点之前和之后发出的绘制操作可能会记录在单独的合成图层中。 因此,不要在可能绘制子节点的操作之间保持对画布的引用。
///
///使用[PaintingContext.repaintCompositedChild]和[pushLayer]时会自动创建新的[PaintingContext]对象。
简单来说就是用来绘制的类
那我们首先来看看它的构造方法和一些基本属性
@protected
PaintingContext(this._containerLayer, this.estimatedBounds)//构造方法,需要传一个容器图层和预估范围进来
: assert(_containerLayer != null),
assert(estimatedBounds != null);
PictureLayer _currentLayer;//当前正在绘制的图层
ui.PictureRecorder _recorder;//引擎方面的recorder
Canvas _canvas;//画布
要绘制的话肯定要先拿到画布,我们先看一组方法
@override
Canvas get canvas {
if (_canvas == null)
_startRecording();//获取canvas,如果没有则开启记录模式生成画布
return _canvas;
}
void _startRecording() {//开启记录模式
assert(!_isRecording);
_currentLayer = PictureLayer(estimatedBounds);//新生成一个estimatedBounds范围的Picture图层
_recorder = ui.PictureRecorder();//通过flutterui引擎获取一个PictureRecorder
_canvas = Canvas(_recorder);//生成持有recorder的canvas
_containerLayer.append(_currentLayer);//把当前图层添加如容器图层
}//然后我们就可以操作canvas了
//这个类的内部会调用此方法停止记录,保存记录,置空属性。不允许你自己调用。如果子类自定义了对canvas的录制,那这个方法也应该被重写来保存录制的结果。
@protected
@mustCallSuper
void stopRecordingIfNeeded() {
···
_currentLayer.picture = _recorder.endRecording();//在这里可以看到,我们把绘制记录的结果传给了当前图层的picture
_currentLayer = null;
_recorder = null;
_canvas = null;
}
//我们在看一下,PictureLayer的addToScene是怎么实现的呢?
@override
ui.EngineLayer addToScene(ui.SceneBuilder builder, [ Offset layerOffset = Offset.zero ]) {
builder.addPicture(layerOffset, picture, isComplexHint: isComplexHint, willChangeHint: willChangeHint);//直接把picture添加给了builder,用于构建scene
return null;
}
到这里实际上就比较明朗了我们就暂时看到这里,先回归主题,等一下遇到了再看
RenderObject中的painting部分
首先还是看一下相关的属性
/// 在子类中重写以表明是否与其父节点分开重新绘制。如重绘的对象可能想要重新绘制自己而不需要父节点重绘
/// 如果此getter返回true,则[paintBounds]将应用于此对象和所有后代。
/// 警告:此getter不得在此对象的生命周期内更改值。
bool get isRepaintBoundary => false;
/// 是否总是需要合成
/// 重写这个来指示你的paint函数总是至少创建一个合成图层。例如视频使用硬件解码器,则应返回true。
/// 此项改变时必须调用markNeedsCompositingBitsUpdate。(这个方法在[adoptChild]或者[dropChild]中调用了)
@protected
bool get alwaysNeedsCompositing => false;
OffsetLayer _layer;//这个就是我们当前的图层
bool _needsCompositingBitsUpdate = false;//当有子节点添加时,设置true
bool _needsCompositing; // 构造函数中初始化了
bool _needsPaint = true; //需要重绘
Rect get paintBounds;//绘制的预估边界
了解了它的属性,我们看一下它的方法
首先就是之前就看到过的markNeedsPaint();当发生改变需要重绘就会调用这个方法
void markNeedsPaint() {
···
_needsPaint = true;//标记true
if (isRepaintBoundary) {//如果只需要重绘自己
···
if (owner != null) {
owner._nodesNeedingPaint.add(this);//把自己加入重绘队列等待
owner.requestVisualUpdate();//请求更新
}
} else if (parent is RenderObject) {
···
final RenderObject parent = this.parent;
parent.markNeedsPaint();//请求重绘父节点
assert(parent == this.parent);
} else {
···//有可能是跟节点,直接刷新
if (owner != null)
owner.requestVisualUpdate();
}
}
然后就是因为子节点添加或移除,需要重新合成而调用的markNeedsCompositingBitsUpdate
void markNeedsCompositingBitsUpdate() {
if (_needsCompositingBitsUpdate)
return;
_needsCompositingBitsUpdate = true;//设置标记位
if (parent is RenderObject) {
final RenderObject parent = this.parent;//如果父节点也已经标记了,就不用管了
if (parent._needsCompositingBitsUpdate)
return;
if (!isRepaintBoundary && !parent.isRepaintBoundary) {//如果父节点还没标记,但是当前节点和父节点都不是独立渲染的
parent.markNeedsCompositingBitsUpdate();//通知标记父节点更新
return;
}
}
···
// parent is fine (or there isn't one), but we are dirty
if (owner != null)
owner._nodesNeedingCompositingBitsUpdate.add(this);//添加owner的dirtylist中
}
然后就是当drawFrame的时候
void drawFrame() {
assert(renderView != null);
pipelineOwner.flushLayout();//刷新布局,之前看到过了,我们这里先着重看下接下来的三个方法
pipelineOwner.flushCompositingBits();
pipelineOwner.flushPaint();
renderView.compositeFrame(); // this sends the bits to the GPU
pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
}
1.PipelineOwner.flushCompositingBits 此方法会依次调用其需要重新合成的_nodesNeedingCompositingBitsUpdate 中的RenderObject对象,调用其_updateCompositingBits方法
void _updateCompositingBits() {
if (!_needsCompositingBitsUpdate)
return;
final bool oldNeedsCompositing = _needsCompositing;
_needsCompositing = false;
visitChildren((RenderObject child) {
child._updateCompositingBits();//对child依次调用此方法
if (child.needsCompositing)
_needsCompositing = true;
});
if (isRepaintBoundary || alwaysNeedsCompositing)
_needsCompositing = true;
if (oldNeedsCompositing != _needsCompositing)//实际上是修改了_needsCompositing变量并调用markNeedsPaint
markNeedsPaint();
_needsCompositingBitsUpdate = false;
}
2.pipelineOwner.flushPaint();
此方法在遍历存储的dirty对象时,会先做一个判断
if (node._layer.attached) {//判断这个节点的图层是不是已经被加入图层树了
PaintingContext.repaintCompositedChild(node);
} else {
node._skippedPaintingOnLayer();
}
//如果已经被移除了
void _skippedPaintingOnLayer() {
···
AbstractNode ancestor = parent;
while (ancestor is RenderObject) {
final RenderObject node = ancestor;
if (node.isRepaintBoundary) {
if (node._layer == null)
break;
if (node._layer.attached)
break;
node._needsPaint = true;//逐级对它的父节点标记需要重绘
}
ancestor = node.parent;
}
}
//对加入图层树的节点,直接调用PaintingContext.repaintCompositedChild
static void repaintCompositedChild(RenderObject child, { bool debugAlsoPaintedParent = false }) {
···
_repaintCompositedChild(
child,
debugAlsoPaintedParent: debugAlsoPaintedParent,
);
}
static void _repaintCompositedChild(
RenderObject child, {
bool debugAlsoPaintedParent = false,
PaintingContext childContext,
}) {
assert(child.isRepaintBoundary);
···
if (child._layer == null) {
child._layer = OffsetLayer();//如果没有图层就新建一个图层给节点
} else {
child._layer.removeAllChildren();//如果有,移除所有的子图层
}
childContext ??= PaintingContext(child._layer, child.paintBounds);
child._paintWithContext(childContext, Offset.zero);
childContext.stopRecordingIfNeeded();//绘制完毕,停止记录,把绘制的记录保存到了当前图层
}
//下面回到RenderObject中 可以看到除去一些状态判断和断言,就是直接调用了paint方法,而这个方法是需要子类实现的,真正的绘制方法
void _paintWithContext(PaintingContext context, Offset offset) {
···
_needsPaint = false;
try {
paint(context, offset);
assert(!_needsLayout); // check that the paint() method didn't mark us dirty again
assert(!_needsPaint); // check that the paint() method didn't mark us dirty again
}
···
}
3.renderView.compositeFrame()
这个方法上面已经讲过了,忘记的同学可以上去搜一下,实际上就是把图层树中的数据组装成scene交给引擎渲染,我们就不再看了
到此为止,painting相关的内容就看完了,实际上RenderObject中还有一些其他的方法,有兴趣的可以自己去看
下面我们看下一个部分
Semantics
辅助功能相关,同样以树的形式出现
非常抱歉因为这一块对我想要了解的整体流程而言不是十分重要,目前就没有深入分析。有兴趣的老哥可以去看一下。过一段时间我应该会对语义模块整体分析一下
我们先看下一部分
Events
这一块很简单,只有一个空方法等待实现
/// Override this method to handle pointer events that hit this render object.
@override
void handleEvent(PointerEvent event, covariant HitTestEntry entry) { }
你可以在这里处理事件。事件的分发则由GestureBinding进行
到这里,RenderObject我们就分析完了,那么问题来了,我们写代码的时候写的一般都是Widget,那么渲染用的RenderObject是如何得到的呢?我们看一下第二个类
RenderObjectWidget
这个类继承自Widget,代码比较少,我们看一下
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) { }
}
可以看到,这个类只有四个等待实现的方法,我们结合RenderObjectElement一起看
RenderObjectElement
我们还是按照之前分析Element时的生命周期来分析
首先就是mount 挂载方法
@override
void mount(Element parent, dynamic newSlot) {
super.mount(parent, newSlot);
_renderObject = widget.createRenderObject(this);//这里我们通过widget获取了要显示到屏幕上的RenderObject
···
attachRenderObject(newSlot);
_dirty = false;
}
@override
void attachRenderObject(dynamic newSlot) {
assert(_ancestorRenderObjectElement == null);
_slot = newSlot;
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();//找到上级 RenderObjectElement
_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);//插入renderobject
final ParentDataElement<RenderObjectWidget> parentDataElement = _findAncestorParentDataElement();//找到被添加的parentdata
if (parentDataElement != null)
_updateParentData(parentDataElement.widget);//更新
}
我们看一下上面调用的一些方法
RenderObjectElement _findAncestorRenderObjectElement() {
Element ancestor = _parent;
while (ancestor != null && ancestor is! RenderObjectElement)//逐级查找最近的RenderObjectElement父节点并返回
ancestor = ancestor._parent;
return ancestor;
}
首先上面这个方法,我们知道element是树形结构的,而并不是所有的element都是RenderObjectElement,当前得父节点有可能是其他传递数据或者控制布局的元素,比如我们看到的ParentDataElement,所以我们逐级向上查找最近的RenderObjectElement,这个就是当前节点的父布局,它持有当前RenderObject的父RenderObject
/// Insert the given child into [renderObject] at the given slot.
///
/// The semantics of `slot` are determined by this element. For example, if
/// this element has a single child, the slot should always be null. If this
/// element has a list of children, the previous sibling is a convenient value
/// for the slot.
@protected
void insertChildRenderObject(covariant RenderObject child, covariant dynamic slot);//把RenderObject插入父布局的RenderObject中,此方法需要子类实现
下面我们再获取它的ParentDataElement
ParentDataElement<RenderObjectWidget> _findAncestorParentDataElement() {
Element ancestor = _parent;
while (ancestor != null && ancestor is! RenderObjectElement) {
if (ancestor is ParentDataElement<RenderObjectWidget>)//和之前一样,逐级查找
return ancestor;
ancestor = ancestor._parent;
}
return null;
}
void _updateParentData(ParentDataWidget<RenderObjectWidget> parentData) {
parentData.applyParentData(renderObject);//应用此parentData
}
这个ParentData到底是啥呢,我们下一篇再说
同理,在unmount时
@override
void detachRenderObject() {
if (_ancestorRenderObjectElement != null) {
_ancestorRenderObjectElement.removeChildRenderObject(renderObject);//移除renderobject
_ancestorRenderObjectElement = null;
}
_slot = null;
}
其他类似方法有兴趣的老哥可以自己去看一下
最后,在之前看element时有一个更新child的方法,updatechild,而在RenderObjectElement中又提供了一个批量更新的方法,updateChildren,我们先看看注释
/// 使用提供的新widgets更新指定的旧的子节点,移除过时的元素根据需要引入新的元素,最后返回新的子节点列表
/// 在方法执行过程中 oldChildren 列表不可改变。如果调用者想要在函数运行期间移除其中的元素,可以提供一个forgottenChildren参数。
/// 当函数从oldchildren中读取元素时会先检查是否在forgottenChildren中,如果是的话,会忽略该元素
/// 这个函数是updateChild的一个包装,可以更新每个child。调用updateChild时,使用前一个元素作为`newSlot`参数。
感觉我翻译的不是很好理解,我们看一下代码理解一下它是如何批量更新的 代码比较多,会有部分省略
List<Element> updateChildren(List<Element> oldChildren, List<Widget> newWidgets, { Set<Element> forgottenChildren }) {
···
Element replaceWithNullIfForgotten(Element child) {//检查该child是否在forgottenChildren中,在的话替换成null
return forgottenChildren != null && forgottenChildren.contains(child) ? null : child;
}
//注释我省略了,主要是说明此方法用于优化一些情况以及此方法的步骤
int newChildrenTop = 0;
int oldChildrenTop = 0;
int newChildrenBottom = newWidgets.length - 1;
int oldChildrenBottom = oldChildren.length - 1;
final List<Element> newChildren = oldChildren.length == newWidgets.length ?
oldChildren : List<Element>(newWidgets.length);//获取新children的容器,为了节省空间,如果新旧一样长就直接用旧的
Element previousChild;//前一个child
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
final Widget newWidget = newWidgets[newChildrenTop];
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
final Element newChild = updateChild(oldChild, newWidget, previousChild);
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}
//上面这一段代码就是 从新旧的队列头开始比对,如果不能直接update或者有需要忽略的oldchild就停下来,否则直接update并继续下一个直到新的或者旧的没有了
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenBottom]);
final Widget newWidget = newWidgets[newChildrenBottom];
if (oldChild == null || !Widget.canUpdate(oldChild.widget, newWidget))
break;
oldChildrenBottom -= 1;
newChildrenBottom -= 1;
}
//相同的逻辑从队尾开始比对,只不过这次即使可以update也先不update
final bool haveOldChildren = oldChildrenTop <= oldChildrenBottom;
Map<Key, Element> oldKeyedChildren;
if (haveOldChildren) {//如果此时还有不能直接update或者需要忽略的oldchildren
oldKeyedChildren = <Key, Element>{};
while (oldChildrenTop <= oldChildrenBottom) {//遍历剩下的old
final Element oldChild = replaceWithNullIfForgotten(oldChildren[oldChildrenTop]);
if (oldChild != null) {
if (oldChild.widget.key != null)//如果oldwidget设置了key
oldKeyedChildren[oldChild.widget.key] = oldChild;//根据设置的key缓存起来
else//否则直接移除
deactivateChild(oldChild);
}
oldChildrenTop += 1;
}
}
while (newChildrenTop <= newChildrenBottom) {//遍历剩下不能直接update的newwidget
Element oldChild;
final Widget newWidget = newWidgets[newChildrenTop];//还是按顺序获取
if (haveOldChildren) {//如果还有待处理的旧节点
final Key key = newWidget.key;
if (key != null) {
oldChild = oldKeyedChildren[key];//查看有没有和新widget的key值相同的旧节点
if (oldChild != null) {
if (Widget.canUpdate(oldChild.widget, newWidget)) {//有并且能直接update的话
oldKeyedChildren.remove(key);//从缓存中移除
} else {
oldChild = null;//如果不能直接update,没啥用,假装没看到
}
}
}
}
final Element newChild = updateChild(oldChild, newWidget, previousChild);//调用updateChild更新。这里如果oldchild为NUll,就会执行插入操作
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
}
//更改bottom指针,以便把刚才倒着遍历没有处理的数据处理了
newChildrenBottom = newWidgets.length - 1;
oldChildrenBottom = oldChildren.length - 1;
// 继续顺序遍历之前倒叙没处理的
while ((oldChildrenTop <= oldChildrenBottom) && (newChildrenTop <= newChildrenBottom)) {
final Element oldChild = oldChildren[oldChildrenTop];
final Widget newWidget = newWidgets[newChildrenTop];
final Element newChild = updateChild(oldChild, newWidget, previousChild);//update更新
newChildren[newChildrenTop] = newChild;
previousChild = newChild;
newChildrenTop += 1;
oldChildrenTop += 1;
}
if (haveOldChildren && oldKeyedChildren.isNotEmpty) {
for (Element oldChild in oldKeyedChildren.values) {
if (forgottenChildren == null || !forgottenChildren.contains(oldChild))
deactivateChild(oldChild);//移除旧的剩余节点
}
}
return newChildren;
}
到这里为止就基本看完了。那么回到刚开始的问题,明明写的是widget类,最后是如何通过renderObject渲染到屏幕上的呢?我们从main方法开始再做一次简单梳理
首先在runapp中初始化了我们的Binding,并把设置写好的root widget,最后开启第一帧
WidgetsFlutterBinding.ensureInitialized()
..attachRootWidget(app)//这里的app就是我们的传进来的根widget
..scheduleWarmUpFrame();
void attachRootWidget(Widget rootWidget) {
_renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
container: renderView,
debugShortDescription: '[root]',
child: rootWidget,
).attachToRenderTree(buildOwner, renderViewElement);
}
1.在attachRootWidget中获取了一个RenderObjectToWidgetAdapter实例,它实际上就是一个RenderObjectWidget,相应的,也就有了对应的RenderObjectElement,而它的createRenderObject方法返回的正是我们以前看到过的已经添加到pipelineOwner上的RenderView。
2.调用attachToRenderTree方法
RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
if (element == null) {
owner.lockState(() {
element = createElement();//生成element
assert(element != null);
element.assignOwner(owner);
});
owner.buildScope(element, () {
element.mount(null, null);//挂载上去
});
} else {
element._newWidget = this;
element.markNeedsBuild();
}
return element;
}
在element的挂载过程中,相应的renderObject树也就生成了
最后调用scheduleWarmUpFrame显示出来