深入理解Flutter 源码刷新机制--------relayoutBoundary的妙用

大家有没有想过,当子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,进而达到高性能的目的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值