1. 1. 一、探索StatelessWidget的组件构建
    1. 1.1. 1. StatelessWidget 的build调用时机,以及widget树遍历流程
      1. 1.1.1. 三颗树的顶点
    2. 1.2. 2. StatelessWidget 和Element在渲染中的更新
    3. 1.3. 3. 探索key的作用
    4. 1.4. 4. 小结
  2. 2. 二、探索StatefulWidget的动态刷新机制
    1. 2.1. 1. State生命周期分析
    2. 2.2. 2. setState方法刷新页面方式分析
    3. 2.3. 3. 小结
  3. 3. 三、探索SingleChildRenderObjectWidget
    1. 3.1. 1. 探索SingleChildRenderObjectElement中对于子widget的挂载和更新
    2. 3.2. 1. 以Padding为例了解RenderObjectWidget 的布局和绘制实现。
      1. 3.2.0.1. 名词解释
      2. 3.2.0.2. RenderObject的创建
      3. 3.2.0.3. RenderPadding的布局实现
  4. 3.3. 1. RenderObjectElement的传承方式
  5. 3.4. 1. 小结
  • 4. 四、谈谈ProxyWidget
    1. 4.1. 1. InheritedWidget
    2. 4.2. 2. ParentDataWidget
  • 5. 最后
    1. 5.1. 小试
  • 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的配置,来控制组件是否需要更新。

    1
    2
    3
    4
    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

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

    1
    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变量以供父布局使用。

    1
    2
    3
    4
    5
    6
    7
    /// 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

    1
    2
    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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    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初步了解完结。