大家有没有想过,当子widget属性变化后,通过setState通知底层引擎刷新,那么当前页面上的所有Widget都会刷新吗,准确说应该是RenderObject Tree会整个刷新吗?
聪明的你肯定能想的到,当然不会把整个RenderObject Tree刷新
当一个组件的大小被改变时,其parent的大小可能也会被影响,因此需要通知其父节点。如果这样迭代上去,需要通知整棵RenderObject Tree重新布局,必然会影响布局效率。因此,Flutter通过RelayoutBoundary将RenderObject Tree分段,如果遇到了RelayoutBoundary,则不去通知其父节点重新布局,因为其大小不会影响父节点的大小。这样就只需要对RenderObject Tree中的一段重新布局,提高了布局效率。
我们举例说明
import 'dart:async';
import 'dart:math';
import 'dart:ui';
import 'package:flutter/material.dart';
void main() {
runApp(DemoWidget());
}
class DemoWidget extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return DemoState();
}
}
class DemoState extends State {
int randomIndex = 0;
List<Color> colorArray = [Colors.white, Colors.yellowAccent, Colors.green, Colors.lightBlue, Colors.pink];
List<Size> sizeArray = [
const Size(50, 50),
const Size(60, 60),
const Size(65, 65),
const Size(70, 70),
const Size(80, 80)
];
@override
void initState() {
Timer.periodic(Duration(seconds: 10), (timer) {
setState(() {
randomIndex = Random().nextInt(5);
});
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: Container(
width: sizeArray.elementAt(randomIndex).width,
height: sizeArray.elementAt(randomIndex).height,
color: colorArray.elementAt(randomIndex),
),
);
}
}
我们使用上面demo测试验证,间隔10s随机修改修改Container 的width和height用来分析relayoutBoundary的使用
使用Flutter DevTools 工具查看RenderObject Tree 为 RenderView-->RenderPositionedBox-->RenderConstrainedBox-->_RenderColoredBox
结合前面几篇文章的梳理,我们知道在Flutter中,如果Widget有更新,需要重新布局,Framework会将需要布局的RenderObject加入PipelineOwner的_nodesNeedingLayout中,然后当下一个VSync信号来临时,Framework会遍历_nodesNeedingLayout,对其中的每一个RenderObject重新进行布局,遍历_nodesNeedingLayout的函数源码如下:
/// Update the layout information for all dirty render objects.
///
/// This function is one of the core stages of the rendering pipeline. Layout
/// information is cleaned prior to painting so that render objects will
/// appear on screen in their up-to-date locations.
///
/// See [RendererBinding] for an example of how this function is used.
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 {
...
}
}
layout 的过程就是从 _nodesNeedingLayout 列表里面轮询调用node的_layoutWithoutResize() 方法。为了达到高效_nodesNeedingLayout 列表里面的节点越少也就越高效,也就性能越高
那么_nodesNeedingLayout 列表是如何被添加进来,是不是所有的dirty node都会被加进来呢。
下面我们就追踪该列表如何被添加
通过源码,我们看到markNeedsLayout方法
void markNeedsLayout() {
...
if (_needsLayout) {
assert(_debugSubtreeRelayoutRootAlreadyMarkedNeedsLayout());
return;
}
...
if (_relayoutBoundary != this) {
markParentNeedsLayout();
} else {
_needsLayout = true;
if (owner != null) {
...
owner!._nodesNeedingLayout.add(this);
owner!.requestVisualUpdate();
}
}
}
可见,在一个RenderObject调用markNeedsLayout函数后,如果其本身不是_relayoutBoundary,则会通过markParentNeedsLayout函数调用到parent的markNeedsLayout函数,从而形成递归调用,直到找到最近的一个是_relayoutBoundary的上级节点,才会停止递归,并将该节点加入_nodesNeedingLayout。因此,通过_relayoutBoundary,Flutter将RenderObject Tree划分成了数段,当位于某段的RenderObject需要重新布局时,只会更新该段及其下的RenderObject,而不是整个RenderObject Tree。
@protected
void markParentNeedsLayout() {
_needsLayout = true;
assert(this.parent != null);
final RenderObject parent = this.parent! as RenderObject;
if (!_doingThisLayoutWithCallback) {
parent.markNeedsLayout();
} else {
assert(parent._debugDoingThisLayout);
}
assert(parent == this.parent);
}
我们注意到markParentNeedsLayout()方法,在递归调用父RenderObject 的makeNeedsLayout 时同时也将自己_needsLayout标记为true
那么当前_relayoutBoundary是怎么确定的呢。
@pragma('vm:notify-debugger-on-exception')
void layout(Constraints constraints, { bool parentUsesSize = false }) {
...
RenderObject? relayoutBoundary;
if (!parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject) {
relayoutBoundary = this;
} else {
relayoutBoundary = (parent! as RenderObject)._relayoutBoundary;
}
...
_constraints = constraints;
if (_relayoutBoundary != null && relayoutBoundary != _relayoutBoundary) {
// The local relayout boundary has changed, must notify children in case
// they also need updating. Otherwise, they will be confused about what
// their actual relayout boundary is later.
visitChildren(_cleanChildRelayoutBoundary);
}
_relayoutBoundary = relayoutBoundary;
...
}
答案就在layout方法里
什么时候会将RenderObject设置为RelayoutBoundary呢?满足以下4种情况之一时,会将自身设置为RelayoutBoundary。
parentUsesSize = false:父节点的布局不依赖当前节点的大小。
sizedByParent = true:当前节点大小由父节点决定。
constraints.isTight:大小为确定的值,即宽高的最大值等于最小值。
parent is not RenderObject:如果父节点不是RenderObject,子节点layout变化不需要通知父节点更新。
以上条件很好理解,例如parentUsesSize = false,此时父节点的布局不依赖当前节点的大小,那当前节点布局更新自然不需要通知父节点,因此可以将其作为一个RelayoutBoundary。
总结:
RenderObject 通过RelayoutBoundary 把RenderObject Tree进行分段隔离,子RenderObject 的刷新不影响父亲RenderObject ,减少每一帧刷新的RenderObject,进而达到高性能的目的