Flutter框架分析(七)-relayoutBoundary

1,922 阅读4分钟

1. 前言

Flutter框架分析(四)-RenderObject一文中,我们简单介绍了RenderObject中一个重要成员变量:RelayoutBoundary。下面我们简单回顾下RelayoutBoundary的主要作用。
当一个组件的大小被改变时,其parent的大小可能也会被影响,因此需要通知其父节点。如果这样迭代上去,需要通知整棵RenderObject Tree重新布局,必然会影响布局效率。因此,Flutter通过RelayoutBoundaryRenderObject 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会调用RenderObjectperformLayout函数,实现该RenderObject的重新布局。
以上流程的示意图如下:

image.gif

由上述逻辑可知,当Widget有更新,需要重新布局时,加入_nodesNeedingLayout的元素的多少直接关系到需要重新布局元素的多少,如果能将尽可能少的RenderObject加入_layoutWithoutResize,即可尽可能提高布局效率。这就是设计RelayoutBoundary的核心思路。
下面我们来看什么时候会将RenderObject添加进_nodesNeedingLayout。从源码可以看到,添加进_nodesNeedingLayout有两个地方:

  • 初始化RenderView的时候,源码如下:
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();
    }
  }
}

那本函数的调用时机是什么呢?主要有以下几种:

  • 子节点变动,例如attachdetach
  • 自身布局变化,例如Size变化。 当Flutter初始化进行第一次布局,每个RenderObject均需要布局,因此无优化空间,本文主要关注对重新布局的优化,即对markNeedsLayout的调用。接下来我们分析markNeedsLayout的调用链。其流程图如下:

image.gif

可见,在一个RenderObject调用markNeedsLayout函数后,如果其本身不是_relayoutBoundary,则会通过markParentNeedsLayout函数调用到parentmarkNeedsLayout函数,从而形成递归调用,直到找到最近的一个是_relayoutBoundary的上级节点,才会停止递归,并将该节点加入_nodesNeedingLayout。因此,通过_relayoutBoundary,FlutterRenderObject Tree划分成了数段,当位于某段的RenderObject需要重新布局时,只会更新该段及其下的RenderObject,而不是整个RenderObject Tree。示意图如下:

image.gif

那么,什么时候会将RenderObject设置为RelayoutBoundary呢?满足以下4种情况之一时,会将自身设置为RelayoutBoundary

  • parentUsesSize = false:父节点的布局不依赖当前节点的大小。
  • sizedByParent = true:当前节点大小由父节点决定。
  • constraints.isTight:大小为确定的值,即宽高的最大值等于最小值。
  • parent is not RenderObject:如果父节点不是RenderObject,子节点layout变化不需要通知父节点更新。

以上条件很好理解,例如parentUsesSize = false,此时父节点的布局不依赖当前节点的大小,那当前节点布局更新自然不需要通知父节点,因此可以将其作为一个RelayoutBoundary

3. 小结

本文首先介绍了RelayoutBoundary的作用,然后结合源码分析了RelayoutBoundary的作用原理,其重点如下:

  1. RelayoutBoundary通过减少待布局节点列表数量(加入_nodesNeedingLayout)的方式优化节点更新时的布局效率。
  2. 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框架分析(八)-Platform Channel
Flutter框架分析- Parent Data
Flutter框架分析 -InheritedWidget