1. 前言
在Flutter框架分析(四)-RenderObject一文中,我们简单介绍了RenderObject中一个重要成员变量:RelayoutBoundary。下面我们简单回顾下RelayoutBoundary的主要作用。 当一个组件的大小被改变时,其parent的大小可能也会被影响,因此需要通知其父节点。如果这样迭代上去,需要通知整棵RenderObject Tree重新布局,必然会影响布局效率。因此,Flutter通过RelayoutBoundary将RenderObject Tree分段,如果遇到了RelayoutBoundary,则不去通知其父节点重新布局,因为其大小不会影响父节点的大小。这样就只需要对RenderObject Tree中的一段重新布局,提高了布局效率。 那么,RelayoutBoundary是怎么实现将RenderObject Tree分段的呢?本文将通过源码来剖析RelayoutBoundary的工作原理。
2. 源码解析
在Flutter中,如果Widget有更新,需要重新布局,Framework会将需要布局的RenderObject加入PipelineOwner的_nodesNeedingLayout中,然后当下一个VSync信号来临时,Framework会遍历_nodesNeedingLayout,对其中的每一个RenderObject重新进行布局,遍历_nodesNeedingLayout的函数源码如下:
void flushLayout() {
try {
// TODO(ianh): assert that we're not allowing previously dirty nodes to redirty themselves
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 {
}
}
其中,_layoutWithoutResize会调用RenderObject的performLayout函数,实现该RenderObject的重新布局。 以上流程的示意图如下:
由上述逻辑可知,当Widget有更新,需要重新布局时,加入_nodesNeedingLayout的元素的多少直接关系到需要重新布局元素的多少,如果能将尽可能少的RenderObject加入_layoutWithoutResize,即可尽可能提高布局效率。这就是设计RelayoutBoundary的核心思路。 下面我们来看什么时候会将RenderObject添加进_nodesNeedingLayout。从源码可以看到,添加进_nodesNeedingLayout有两个地方:
void scheduleInitialLayout() {
_relayoutBoundary = this;
owner._nodesNeedingLayout.add(this);
}
本函数只在Flutter初始化的时候调用一次。
- RenderObject标记自己需要重新布局的时候,源码如下:
void markNeedsLayout() {
if (_needsLayout) {
return;
}
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
_needsLayout = true;
if (owner != null) {
owner._nodesNeedingLayout.add(this);
owner.requestVisualUpdate();
}
}
}
那本函数的调用时机是什么呢?主要有以下几种:
- 子节点变动,例如attach或detach。
- 自身布局变化,例如Size变化。
当Flutter初始化进行第一次布局,每个RenderObject均需要布局,因此无优化空间,本文主要关注对重新布局的优化,即对markNeedsLayout的调用。接下来我们分析markNeedsLayout的调用链。其流程图如下:
可见,在一个RenderObject调用markNeedsLayout函数后,如果其本身不是_relayoutBoundary,则会通过markParentNeedsLayout函数调用到parent的markNeedsLayout函数,从而形成递归调用,直到找到最近的一个是_relayoutBoundary的上级节点,才会停止递归,并将该节点加入_nodesNeedingLayout。因此,通过_relayoutBoundary,Flutter将RenderObject Tree划分成了数段,当位于某段的RenderObject需要重新布局时,只会更新该段及其下的RenderObject,而不是整个RenderObject Tree。示意图如下:
那么,什么时候会将RenderObject设置为RelayoutBoundary呢?满足以下4种情况之一时,会将自身设置为RelayoutBoundary。
- parentUsesSize = false:父节点的布局不依赖当前节点的大小。
- sizedByParent = true:当前节点大小由父节点决定。
- constraints.isTight:大小为确定的值,即宽高的最大值等于最小值。
- parent is not RenderObject:如果父节点不是RenderObject,子节点layout变化不需要通知父节点更新。
以上条件很好理解,例如parentUsesSize = false,此时父节点的布局不依赖当前节点的大小,那当前节点布局更新自然不需要通知父节点,因此可以将其作为一个RelayoutBoundary。
3. 小结
本文首先介绍了RelayoutBoundary的作用,然后结合源码分析了RelayoutBoundary的作用原理,其重点如下:
- RelayoutBoundary通过减少待布局节点列表数量(加入_nodesNeedingLayout)的方式优化节点更新时的布局效率。
- RelayoutBoundary的设置条件包括以下4种:
- parentUsesSize = false
- sizedByParent = true
- constraints.isTight
- parent is not RenderObjec
4. 参考文档
如何在Flutter上实现高性能的动态模板渲染
5. 相关文章
Flutter框架分析(一)–架构总览 Flutter框架分析(二)-- Widget Flutter框架分析(三)-- Element Flutter框架分析(四)-RenderObject Flutter框架分析(五)-Widget,Element,RenderObject树 Flutter框架分析(六)-Constraint Flutter框架分析- Parent Data Flutter框架分析 -InheritedWidget
|