Flutter|子组件大小位置改变时,父组件如何知道并且改变自身的size?

问题背景

当一个Row组件的第一个子组件size变化的时候,第二个组件的位置是如何被改变的?Row本身的size又是如何改变的?

当我思考的时候,按照我对widget层的理解,既然setState是一个从上往下执行的操作,那么子组件变化然后通知父组件有改动的这个操作,很明显是一个反向的过程。需要从最底层的子组件往上不断通知各个父组件。

说实话,我一开始,是没有切入点的,一顿乱找,然后在一篇大佬的帖子中找到了这么一行话

然后将pipelineOwner.flushLayout();作为切入点,从这篇文章捋出了下面的思路:

正片开始前,需要有setState原理基础

正片开始

RenderBinding在init的时候,将帧绘制方法(drawFrame)放入(addPersistentFrameCallback)了一个用于处理引擎发起绘制的数组(_persistentCallbacks)中,每次引擎发起绘制需求,负责响应的方法(handleDrawFrame)会遍历数组(_persistentCallbacks)来处理绘制。(这段逻辑就不梳理了,不是本话题的重点)

这段话重点肯定是在drawFrame方法,进入RenderBinding.drawFrame方法:

Dart 复制代码
@protected
void drawFrame() {
  assert(renderView != null);
  pipelineOwner.flushLayout();///如果在这里断点,会发现调用频率非常高
  pipelineOwner.flushCompositingBits();
  pipelineOwner.flushPaint();
  if (sendFramesToEngine) {
    renderView.compositeFrame(); // this sends the bits to the GPU
    pipelineOwner.flushSemantics(); // this also sends the semantics to the OS.
    _firstFrameSent = true;
  }
}

继续进入pipelineOwner.flushLayout()方法:

Dart 复制代码
/// 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>[];
      dirtyNodes.sort((RenderObject a, RenderObject b) => a.depth - b.depth);
      for (int i = 0; i < dirtyNodes.length; i++) {
        if (_shouldMergeDirtyNodes) {
          _shouldMergeDirtyNodes = false;
          if (_nodesNeedingLayout.isNotEmpty) {
            _nodesNeedingLayout.addAll(dirtyNodes.getRange(i, dirtyNodes.length));
            break;
          }
        }
        final RenderObject node = dirtyNodes[i];
        if (node._needsLayout && node.owner == this) {
          node._layoutWithoutResize();
        }
      }
      _shouldMergeDirtyNodes = false;
    }
  }
}

机翻注释:

更新所有脏渲染对象的布局信息。 该函数是渲染管线的核心阶段之一。 在绘制之前会清理布局信息,以便渲染对象将出现在屏幕上的最新位置。 有关如何使用此函数的示例,请参阅 [RendererBinding]。

你看!这个过程,是不是很像BuildOwner.buildScope方法中遍历标脏的Element这种处理方式!而且_layoutWithoutResize方法内调用了performLayout()方法。

好!按照这个对应方式,我们查找「什么时候把这个节点标脏」,基本就能解决问题!

文件内搜索「_nodesNeedingLayout.add(」,两个地方使用:

markNeedsLayout

scheduleInitialLayout(从名字就可以看出,只在初始化的时候调用,所以,不是重点)

markNeedsLayout源码:

Dart 复制代码
/// Mark this render object's layout information as dirty, and either register
/// this object with its [PipelineOwner], or defer to the parent, depending on
/// whether this object is a relayout boundary or not respectively.
///
/// ## Background
///
/// Rather than eagerly updating layout information in response to writes into
/// a render object, we instead mark the layout information as dirty, which
/// schedules a visual update. As part of the visual update, the rendering
/// pipeline updates the render object's layout information.
///
/// This mechanism batches the layout work so that multiple sequential writes
/// are coalesced, removing redundant computation.
///
/// If a render object's parent indicates that it uses the size of one of its
/// render object children when computing its layout information, this
/// function, when called for the child, will also mark the parent as needing
/// layout. In that case, since both the parent and the child need to have
/// their layout recomputed, the pipeline owner is only notified about the
/// parent; when the parent is laid out, it will call the child's [layout]
/// method and thus the child will be laid out as well.
///
/// Once [markNeedsLayout] has been called on a render object,
/// [debugNeedsLayout] returns true for that render object until just after
/// the pipeline owner has called [layout] on the render object.
///
/// ## Special cases
///
/// Some subclasses of [RenderObject], notably [RenderBox], have other
/// situations in which the parent needs to be notified if the child is
/// dirtied (e.g., if the child's intrinsic dimensions or baseline changes).
/// Such subclasses override markNeedsLayout and either call
/// `super.markNeedsLayout()`, in the normal case, or call
/// [markParentNeedsLayout], in the case where the parent needs to be laid out
/// as well as the child.
///
/// If [sizedByParent] has changed, calls
/// [markNeedsLayoutForSizedByParentChange] instead of [markNeedsLayout].
void markNeedsLayout() {
  if (_needsLayout) {
    return;
  }
  if (_relayoutBoundary == null) {
    _needsLayout = true;
    if (parent != null) {
      // _relayoutBoundary is cleaned by an ancestor in RenderObject.layout.
      // Conservatively mark everything dirty until it reaches the closest
      // known relayout boundary.
      markParentNeedsLayout();
    }
    return;
  }
  if (_relayoutBoundary != this) {
    markParentNeedsLayout();
  } else {
    _needsLayout = true;
    if (owner != null) {
      owner!._nodesNeedingLayout.add(this);
      owner!.requestVisualUpdate();
    }
  }
}

部分机翻注释:

将此渲染对象的布局信息标记为脏,并使用其 [PipelineOwner] 注册此对象,或遵循父级,具体取决于此对象是否是重新布局边界。 如果渲染对象的父级指示在计算其布局信息时使用其渲染对象子级之一的大小,则在为子级调用此函数时,也会将父级标记为需要布局。 在这种情况下,由于父级和子级都需要重新计算布局,因此管道所有者只会收到有关父级的通知; 当父级被布局时,它将调用子级的 [layout] 方法,因此子级也会被布局。(这一点在RenderObject类的实现没有体现,可以在RenderProxyBoxMixin.performLayout看到实现) 一旦在渲染对象上调用了 [markNeedsLayout],[debugNeedsLayout] 就会为该渲染对象返回 true,直到管道所有者在渲染对象上调用 [layout] 之后。

注释已经说的很明显了「如果当前节点的大小会影响它的父节点的大小,就会把父节点标脏,而不是自身标脏」(注意!侧面说明:这是一个从下往上的过程),那我们接着看_relayoutBoundary这个属性。

_relayoutBoundary决定了布局更新的范围。结合markNeedsLayout注释,可以知道markNeedsLayout是一个从下往上的过程,那,这上,上到哪里为止?就是通过_relayoutBoundary控制。

文件内搜索「_relayoutBoundary = 」,只有5处使用,其中关键的赋值在layout方法中:

Dart 复制代码
///class RenderObject
/// Compute the layout for this render object.
///
/// This method is the main entry point for parents to ask their children to
/// update their layout information. The parent passes a constraints object,
/// which informs the child as to which layouts are permissible. The child is
/// required to obey the given constraints.
///
/// If the parent reads information computed during the child's layout, the
/// parent must pass true for `parentUsesSize`. In that case, the parent will
/// be marked as needing layout whenever the child is marked as needing layout
/// because the parent's layout information depends on the child's layout
/// information. If the parent uses the default value (false) for
/// `parentUsesSize`, the child can change its layout information (subject to
/// the given constraints) without informing the parent.
///
/// Subclasses should not override [layout] directly. Instead, they should
/// override [performResize] and/or [performLayout]. The [layout] method
/// delegates the actual work to [performResize] and [performLayout].
///
/// The parent's [performLayout] method should call the [layout] of all its
/// children unconditionally. It is the [layout] method's responsibility (as
/// implemented here) to return early if the child does not need to do any
/// work to update its layout information.
@pragma('vm:notify-debugger-on-exception')
void layout(Constraints constraints, { bool parentUsesSize = false }) {
  if (!kReleaseMode && debugProfileLayoutsEnabled) {
    Map<String, String>? debugTimelineArguments;
    Timeline.startSync(
      '$runtimeType',
      arguments: debugTimelineArguments,
    );
  }
  final bool isRelayoutBoundary = !parentUsesSize || sizedByParent || constraints.isTight || parent is! RenderObject;
  final RenderObject relayoutBoundary = isRelayoutBoundary ? this : (parent! as RenderObject)._relayoutBoundary!;
  if (!_needsLayout && constraints == _constraints) {
    if (relayoutBoundary != _relayoutBoundary) {
      _relayoutBoundary = relayoutBoundary;
      visitChildren(_propagateRelayoutBoundaryToChild);
    }
    if (!kReleaseMode && debugProfileLayoutsEnabled) {
      Timeline.finishSync();
    }
    return;
  }
  _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;
  if (sizedByParent) {
    try {
      performResize();
    } catch (e, stack) {
      _reportException('performResize', e, stack);
    }
  }
  RenderObject? debugPreviousActiveLayout;
  try {
    performLayout();
    markNeedsSemanticsUpdate();

  } catch (e, stack) {
    _reportException('performLayout', e, stack);
  }
  _needsLayout = false;
  markNeedsPaint();
  if (!kReleaseMode && debugProfileLayoutsEnabled) {
    Timeline.finishSync();
  }
}

机翻注释:

计算此渲染对象的布局。 该方法是父母要求孩子更新布局信息的主要入口点。 父级传递一个约束对象,该对象通知子级哪些布局是允许的。 孩子必须遵守给定的约束。 如果父级读取在子级布局期间计算的信息,则父级必须为"parentUsesSize"传递 true。 在这种情况下,每当子级被标记为需要布局时,父级将被标记为需要布局,因为父级的布局信息取决于子级的布局信息。 如果父级使用"parentUsesSize"的默认值(false),则子级可以更改其布局信息(受给定约束),而无需通知父级。 子类不应该直接重写[layout]。 相反,他们应该覆盖 [performResize] 和/或 [performLayout]。 [layout] 方法将实际工作委托给 [performResize] 和 [performLayout]。 父级的 [performLayout] 方法应无条件调用其所有子级的 [layout]。 如果子级不需要执行任何工作来更新其布局信息,则 [layout] 方法有责任(如此处实现)提前返回。

以下四种情况时,当前节点设为布局更新的边界(_relayoutBoundary = this):

  • 父节点不依赖当前节点的size(父节点不依赖这个字节点,当然不需要标脏父节点,自己爱咋变咋变)
  • size是父节点给的(既然父给定了size,就和上一种情况类似,当前节点不应该影响父节点)
  • 当前节点为紧约束(同上,紧约束,即size已经固定)
  • 父节点不是RenderObject类型

当前节点不是更新边界,则一直往上向父节点取该属性。

如果布局更新的边界不为空,或者不等于当前节点,那么让父节点标脏自己。否则(说明布局边界是当前节点),将自己加入owner!._nodesNeedingLayout数组,等待被调用layout。

小结一下_relayoutBoundary属性:

_relayoutBoundary是用来尽可能缩小布局更新的范围的。因为markNeedsLayout是一个从下往上的过程,需要限制最小的更新范围,降低开销(注释有说父节点layout会触发子节点的layout)。当markNeedsLayout来到边界节点的时候,就会停止往上,并将当前节点真正标脏。

markParentNeedsLayout方法:

Dart 复制代码
///class RenderObject
@protected
void markParentNeedsLayout() {
  _needsLayout = true;
  final RenderObject parent = this.parent! as RenderObject;
  if (!_doingThisLayoutWithCallback) {
    parent.markNeedsLayout();
  }
}

调用父节点的markNeedsLayout

核心流程

RowColumn的父类对应的RenderFlex类为例子,只找到了修改方向、主轴对齐方式这些set方法对markNeedsLayout的显式调用,并没有解决我的问题「child的size改变的时候,父节点是如何知道并且改变自己的大小的」(但是,这里探索没有白费,最后谜底揭开的时候,还是会回到这里 🥶 🥶 🥶 🥶)

(其实到这里,我已经,黔驴技穷了,实在是找不到哪里调用了这个方法,去找了项目资深技术大佬,大佬和我说没空,你自己断点看下堆栈)嘿,还真让我断点断出来了! 🥵 🥵 🥵 🥵

贴下demo源码:

Dart 复制代码
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';

class ColorStatefulBox extends StatefulWidget {
  const ColorStatefulBox({Key? key, required this.color}) : super(key: key);
  final Color color;

  @override
  State<ColorStatefulBox> createState() => _ColorStatefulBoxState();
}

class _ColorStatefulBoxState extends State<ColorStatefulBox> {
  int count = 0;
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: () {
        count++;
        setState(() {});
      },
      child: Container(
        color: widget.color,
        width: 100+count.toDouble(),
        height: 100+count.toDouble(),
        child: Center(
          child: Text(
            count.toString(),
            style: TextStyle(fontSize: 30),
          ),
        ),
      ),
    );
  }
}

class TestPage extends StatefulWidget {
  @override
  _TestPageState createState() => _TestPageState();
}

class _TestPageState extends State<TestPage> {
  List<Widget> widgetList = [const ColorStatefulBox(color: Colors.blue,), const ColorStatefulBox(color: Colors.green,)];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        appBar: AppBar(
          title: const Text("key test"),
        ),
        body: Container(
          padding: const EdgeInsets.all(20),
          child: Column(
            children: [...widgetList],
          ),
        ),
        floatingActionButton: GestureDetector(
          onTap: () {
            widgetList = widgetList.reversed.toList();
            setState(() {});
          },
          child: const Icon(
            Icons.add,
          ),
        ));
  }
}

RenderObject.markNeedsLayout打下断点,点击Box。

setState的流程最后(左下角的调用堆栈一堆updateChildupdateperformRebuild可以看出)会走到RenderObjectElement._performRebuild

根据demo中ColorStatefulBox的build方法,可以知道类的包裹为ContainerConstrainedBoxSingleChildRenderObjectWidget,那么上图中RenderObject._performRebuild就是被SingleChildRenderObjectElement.update中的super.update调用的。

看完这个调用堆栈可以知道,是「element的刷新调用了renderObject的更新」。

接着堆栈继续看,调用ConstrainedBox.updateRenderObject

Dart 复制代码
///class ConstrainedBox extends SingleChildRenderObjectWidget
@override
  void updateRenderObject(BuildContext context, RenderConstrainedBox renderObject) {
    renderObject.additionalConstraints = constraints;
  }

接着来到ConstrainedBox对应的RenderObject类RenderConstrainedBoxupdateRenderObject

Dart 复制代码
///class RenderConstrainedBox extends RenderProxyBox
set additionalConstraints(BoxConstraints value) {
    if (_additionalConstraints == value) {
      return;
    }
    _additionalConstraints = value;
    markNeedsLayout();
  }

呐,可以看到,set方法中调用了markNeedsLayout将通过setState更新widget的ConstrainedBox.constraints通过RenderConstrainedBox.additionalConstraints更新到renderObject。(对应上面RenderFlex类的探索结果)对应堆栈如下图:

看到这里,可以回答标题的问题了:size更改的节点在setState最后会调用对应Element的updateRenderObject方法,让节点对应的widget调用RenderObject的markNeedsLayout方法。该方法会往上寻找需要更改的父节点,从而让父节点也执行layout。

总结

以demo中的ColorStatefulBox为例,每次点击通过setState方法改变自身的大小,接着通过对应的Element的_performRebuild来调用widget.updateRenderObject,来更新RenderObject对象的数据,然后该RenderObject将自己(或者祖先节点节点)标脏(markNeedsLayout)。

这里也可以看出来:Element层,衔接了Widget层和RenderObject层,起到一个承上启下的作用,上对接开发者们对widget的更改,下通知布局层RenderObject更新。

最后附上demo调用流程图:

参考链接:

zhuanlan.zhihu.com/p/391946680

juejin.cn/post/690599...

juejin.cn/post/691415...

相关推荐
吕彬-前端27 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱29 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai38 分钟前
uniapp
前端·javascript·vue.js·uni-app
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
君蓦2 小时前
Flutter 本地存储与数据库的使用和优化
flutter
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js