【flutter进阶】Widget源码详解-如何实现自由组合,动态刷新,布局绘制?

2,624 阅读12分钟

www.gaoding.com_design_id=20925982878221356&mode=user.png

刚刚看完张风捷特烈Flutter 布局探索小册。感觉受益良多。

看到结局的问题:如何区分StatelessWidgetStatefulWidget 的使用场景,不禁开始自问,对于StatefulWidget ,StatelessWidget,以及flutter中Widget的众多子类我真的足够了解吗?

对于自己经常要打交道的东西,如果只是一知半解则不利于进步。

下面就从源码的角度来学习下flutter基础的几个Widget 都起到了什么作用。

image.png

先给个简单总结:

  • 其中StatelessWidget 和 StatefulWidget 起到了组织组合子组件的作用。
  • RenderObjectWidget 起到渲染作用。包含绘制偏移和测量信息。
  • ProxyWidget 可以携带信息,以供其他组件使用。

一、探索StatelessWidget的组件构建

在使用StatelessWidget的时候,通常只需要实现一个build方法。就拿我们常用的Container组件举例,他就是StatelessWidget 的子类。他的build方法返回的就是各种组件的组合嵌套。 img

他的各种成员属性也只是用来配置子组件的组合方式而已。

1. StatelessWidget 的build调用时机,以及widget树遍历流程

Container组件是StatelessWidget的经典子类。

我们通过断点调试看看Container 组件build方法的调用堆栈

img

ComponentElementperformRebuild 方法调用的时候,触发了build方法,从stateless中获取了build返回的Widget,而又在performRebuild 调用了updateChild方法,对所有的子孙Element进行build遍历。

ComponentElement是Widget对应元素StatelessElementStatefulElement的父类。

我们拉到最初的调用栈。Element栈调用的起点在于attachRootWidget方法。

还记得我们flutter app开发的起点吗?就是runApp(App())方法,开启了整个flutter app。 attachRootWidget方法正是我们在调用runApp的时候执行的。

在其中,执行了RenderObjectToWidgetAdapter组件的初始化,将renderViewrootWidget作为入参。并且调用attachToRenderTree返回元素树顶点的Element。

img

三颗树的顶点

其中renderViewRenderObject树的顶点,_renderViewElementElement树的顶点。匿名的RenderObjectToWidgetAdapter则是Widget树的顶点,但是他没有被引用。Widget树的维护依赖于Element树,rootWidget就是我们的runApp组件节点,被作为参数挂载到RenderObjectToWidgetAdapter根组件中,被后续的Element挂载循环使用。

Element中也存放了_parent变量,所以我们通过Element对象可以轻松的追溯到祖先节点。

img

我们从上面的分析可以得出ComponentElement 的 performRebuild方法是element.build传承关键方法 ,mount方法也能由此挂载出所有子树(其他类型的Element实现方案略有不同)

在ComponentElement中。也由performRebuild构建出一层层的子孙节点。代码如下,注意红色方框的代码。

img

第一个红框中是build()方法的执行。意味着每次performRebuild被调用的时候,子组件都会被build出来,由此可知widget是唯一的,每次更新都会有新的Widget生成。

updateChild的过程中,如果子element还未生成,就会调用widget.createElement()方法获得element

我们再看StatelessWidget 的源码,实现了createElement方法返回了自定义的StatelessElement

img

生成的子Element 都会在ComponentElement中被持有,以便后续更新

img

由此可知,ComponentElement维系了祖孙关系,其子类Element对应的 StatelessWidget,StatefulWidget,ParentDataWidget 和 InheritedWidget都天然拥有子孙关系能力。

如下所示,StatefulElementComponentElement 的子类。 img

2. StatelessWidget 和Element在渲染中的更新

widget的创建都是在element树遍历的过程中执行的。 widget树依赖于element树,在Element创建的时候widget实例将会被持有。

StatelessWidget在布局和渲染流程中依赖Element维系,树关系被Element挖掘。 img

Element performeRebuild重新构建的时候,有一个是否更新Element的判定机制,以优化性能。

不管是更新update还是挂载mount,每次子widget都会先build()出来。再进行新旧比较。Widget都是一次性的,如果有状态需要保存是由其他方式实现的。 我们再看updateChild方法。上面一小节提到在子element为空的时候,会在其中createElement。而在子Element不为空的时候,会根据新旧Widget 的不同,进行不同的操作。

img

其中通过新旧widgetequals判定。决定是否复用之前的element。如果复用了element,根据canUpdate方法的返回值,来执行child.update方法。所以我们可以得出这样一个结论。

widgetcanUpdate 实现,将很大程度上决定 Element 的复用。减少重新绘制,对State重新赋值,甚至状态丢失的资源浪费。

3. 探索key的作用

canUpdate的默认实现中以Widget的类型和key作为关键字进行判断。如果有对key定义,那么Key的一致性就会对widget的更新显得尤为关键。

这也是我们在做性能优化的时候需要注意的。可以利用Key的配置,来控制组件是否需要更新。

static bool canUpdate(Widget oldWidget, Widget newWidget) {
  return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
}

Key的几种子类基本上都是根据需求,对== 操作符做不同的实现。以更好的自定义 canUpdate 的结果。

其中GlobalKey比较特殊。作为全局的唯一秘钥。提供了对应 widgetBuildContextwidget 的访问方式。并针对 StatefulWidget。还提供了 State 的访问。

以便用户对状态进行全局的更新。比如我们需要在外部使用 BuildContext 进行初始化的时候,可以进行这样调用

img

4. 小结

通过以上对StatelessWidgetComponentElement 的分析,可以得出以下的判断。

StatelessWidget 基于 ComponentElement。主要功能就是提供了组合各种widget的能力,并维持了祖孙的build传承。

当然在探索当中也发现了一些技术债务,由于我们已经知道了statelesswidget的使用场景,对于具体的源码细节先按下不表,在此只记录

  • 生命周期_lifecycleState 起到什么作用
  • _dirty 标记和 markNeedsBuild 的用法和原理是什么
  • BuildOwner 的作用是什么

二、探索StatefulWidget的动态刷新机制

StatefulWidgetStateflessWidget 有很多共同之处。最主要的原因就是他们创建的元素都是ComponentElement的子类,其提供了widget子孙build传承的能力。

可知StatefulWidgetStateflessWidget一样,也是一个有能力组合各种widget的组件。

1. State生命周期分析

StatefulWidget 定义了createState方法。提供了状态刷新能力。 img

再次从StatefullElementbuild方法入手。直接调用了state.build(this)。代理了state的构建行为。

performRebuild方法中也进行了state.didChangeDependencies生命周期回调。

img

在State中,除了生命周期方法外, 最重要的就是build方法了。作用和StatelessWidget的build方法一致。都是提供了组合widget的能力。 initState则给用户提供了初始化state状态的机会。断点调试看看调用栈如何。

img

调试中直观看到,在firstBuld的时候,stateinitState被调用。并在之后调用了didChangeDependencies生命周期方法,和build方法。

img

代码中也对方法做了限制,不可以返回Future类型。 所以我们可以在initState中放心做一些初始化工作,没有异步参与,工作将会在build之前完成。

2. setState方法刷新页面方式分析

对于setState方法。除开生命周期的判断之外,关键代码只有一句,就是调用了element 的markNeedsBuild() img

该方法将对应的element标记为dirty。并且调用owner``!.scheduleBuildFor(``this``);将其加入到 BuildOwner的脏列表(_dirtyElements)中。 将会在下次帧刷新的时候调用BuildOwner.owner.buildScope 重新构建该列表中的元素。

3. 小结

StatelessWidget给使用者提供了一个便捷的布局刷新入口,我们可以利用setState刷新布局。该方法会将对应Element标记为待刷新元素,在下次帧刷新的时候重建布局。状态的改动将会被重建的布局重新获取。

三、探索SingleChildRenderObjectWidget

SingleChildRenderObjectWidget对应的元素类是SingleChildRenderObjectElement。 我们作为开发者,布局过程中SingleChildRenderObjectWidget 的子类使用频率非常频繁,布局的约束,偏移和渲染都是由RenderObjectWidget 实现的,SingleChildRenderObjectWidget继承了RenderObjectWidget的渲染能力,并提供了单子传承的能力。布局的过程中该对象的子类不可或缺,flutter框架中也有不少对应的实现类。

Flutter 框架中实现的SingleChildRenderObjectWidget有以下几种。

  1. SizedBox
  2. LimitedBox
  3. ShaderMask
  4. RotatedBox
  5. SizedOverflowBox
  6. Padding
  7. ...

1. 探索SingleChildRenderObjectElement中对于子widget的挂载和更新

SingleChildRenderObjectElement`的`mount` 和 `update`方法都很简单,都是直接调用了`updateChild`方法,传进去的子widget直接是`widget.child

img

这个方法和ComponentElement基本上一样,都是利用canUpdate的结果进行更新或者是创建子Element

1. 以Padding为例了解RenderObjectWidget 的布局和绘制实现。

名词解释

RenderObject:渲染对象,flutter对象布局的约束,绘制,位移全是由该对象实现,RenderObject树的祖孙中传递着约束,以做到布局大小的传承影响。

RenderObject的创建

RenderObjectWidget 会在mount挂载的时候,创建RenderObject,直接调用widge.createRenderObject。我们的约束,绘制,位移全是由RenderObject传递和实现的。

img

RenderPadding的布局实现

Padding为例。createRenderObject创建了RenderPadding实例,widget的成员原封不动交给了该实例。

img

约束(BoxConstraint)是Flutter确定布局大小的方案,各种RenderObject对于约束的传递都有自己的实现。

下方是RenderPaddingperformLayout代码。红框标记起来的代码中就展示了Padding的约束传承逻辑。

其父布局传给自己约束基础上减去Padding再传递给子RenderObject

观察performLayout方法可以发现,该方法完成了约束的传递,计算了偏移量Offset,并确定了自己的大小。

img

确定大小约束之后,就会在paint中绘制自己和子孙。RenderPadding没有自定义绘制,直接使用了父类RenderShiftedBox的实现。RenderShiftedBox 提供了offset偏移。在绘制子renderObject的时候,为其施加绘制偏移量。有些需要计算子布局偏移的widget,如PaddingAlign等,都对RenderShiftedBox进行了实现。 img

可以看到子布局的offset存在他的parentData中。PaddingRender使用的parentDataBoxParentData,内部提供了offset变量以供父布局使用。

/// Parent data used by [RenderBox] and its subclasses.
class BoxParentData extends ParentData {
  /// The offset at which to paint the child in the parent's coordinate system.
  Offset offset = Offset.zero;
  @override
  String toString() => 'offset=$offset';
}

所有的RenderBox都持有BoxParentData对象,用于存储位移信息,在setUpPrentData的时候进行的初始化。红框中的代码展示了这一细节。

img

到此,就能了解RenderObject是如何被约束BoxConstraint,如何被布局layout,以及如何被绘制paint

1. RenderObjectElement的传承方式

RenderObjectElement 的父子传承在两个子类中实现,在第1小结中已经提到SingleChildRenderObjectWidgetComponentElement十分类似,只是直接把widget.child拿来传承,而不再提供build方法以供子组件组合。

MultiChildRenderObjectElement 也类似,只不过作为多子组件,三棵树分叉的主要因子,维护的是children 列表。 img

在mount 和 update 的时候,子孙组件会像点了爆竹一样被逐一构建和更新。

1. 小结

每个SingleChildRenderObjectWidget组件都实现了各自的布局和绘制方案,也各自处理了约束并传递下去。

比如ColordBox作为绘制组件,借助了RenderColord,绘制了自身颜色,约束则取得是父约束的最小值。Align作为定位组件,借助了RenderPositionedBox,布局的时候计算了对应的偏移量offset,在绘制子布局的时候使用,约束则在传递的时候转了松约束。

诸如此类,所有组件都利用了对应的RenderObject满足了各自布局和渲染的所有需求。我们自己当然也可以自定义对应的RenderObject实现自己的布局。

MultiChildRenderObjectWidgetSingleChildRenderObjectWidget类似,只是维护一个子widget变成了多个子widget。

他的RenderObject基本上都是ContainerRenderObjectMixinRenderBox的子类,内部维护了头尾两个子节点,并利用存储在parentData中的双相链表维护所有的子RenderObject

四、谈谈ProxyWidget

最后稍微提一下ProxyWidgetProxyElement也上ComponentElement的子类。和StatefulWidget 以及StatelessWidget是兄弟关系。也有子孙维系的能力,只不过他的build方法是固定的,返回的就是child。 UML 图.jpg

1. InheritedWidget

我们获取 Theme,MediaQuery数据的时候,都是使用了InheritedWidget

MediaQuery.of(context).size.width;
Theme.of(context).appBarTheme;

通过context 也就是Element实例,获取祖先节点的数据。实现数据共享的效果。

Element中维护了祖先的所有InheritedElement映射,就可以在需要的时候直接通过子孙Element获取。

2. ParentDataWidget

ParentDataWidget提供了子组件向父组件传递渲染信息的能力。 FlexiblePositioned 等组件都是ParentDataWidget 的子类。

需要注意的是:ParentDataWidget只用于渲染信息的传递

在Element.attachRenderObject的时候会调用updateParentData,然后会辗转调用到对应的ParentDataWidget.applyParentData。可以看出只有子组件是RenderObjectWidget子类的时候才会应用对应的ParentDataWidget传递信息。

img

由此可知,只有在子节点渲染的时候,才会应用RenderObject的数据传递赋值。 img

子节点的ParentData对象由父布局创建代码如下,创建时机在子节点插入的时候执行。 img

img

最后

作为开发者,很多时候完成一个任务只会建立在使用的层面。对于为什么这么使用往往不甚了解。

如果我们能更多的学习他的原理。那么如果在开发中碰到问题,我们能够更加得心应手得去解决。

flutter布局渲染的原理以前总是一层雾蒙在我地眼前。但现在,终于有一片薄雾散去,内部轮廓在我面前变得清晰。

坚持学习,见识真实的世界。

小试

我们最后尝试一下一个简单地布局,分析其三棵树结构。嵌套结构如下。其中builderStatelessWidgetColumnMultiChildRenderObjectWidget其他都是SingleChildRenderObjectWidget

void main() {
  runApp(Builder(builder: (context) {
    return Column(
      mainAxisSize: MainAxisSize.max,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        Center(
          child: SizedBox(
            width: 100,
            height: 100,
            child: ColoredBox(color: Colors.blue),
          ),
        ),
        Expanded(
          child: ColoredBox(color: Colors.red),
        ),
      ],
    );
  }));
}

展示出来的样式如下。

img

分析得出的三棵树如下,源头从RenderView而起,然后构建出RenderObjectToWidgetAdapter,再构建出RootRenderObjectElement。由此从根开始三棵树的循环,直到叶子节点。

RenderObjectWidget并非一一对应,只有RenderObjjectWidget才有,但是RenderObject能自动找出自己的组件RenderObjject 自动插入到其child中,所以也能自动成树。

流程图.jpg

至此,我们的Widget初步了解完结。