注册

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

看到结局的问题:如何区分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初步了解完结。


作者:guuguo
链接:https://juejin.cn/post/7129770397838344206
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册