解读Flutter源码之runApp
文章目录
注:本文代码基于Flutter SDK 3.13.5
一、前言
在上一文Flutter源码之三颗树分类中,我们对Flutter的三颗树Widget、Element、RenderObject进行了简单的分类,知道了Widget可分为RenderObjectWidget一类和非RenderObjectWidget另一类,并且XXXWidget基本上都有与之对应的XXXElement。
在本文中,将会通过一个小案例以及Debug调试源码的方式深入分析runApp方法的执行过程。
二、小案例之设计问题
笔者在最初设计小案例时,预期想要的Widget层次结构是下图这样的。

对应的代码如下。
|
|
程序运行起来后,可以看到如下效果。

但是当笔者通过Flutter Inspector查看Widget层次结构时,发现事与愿违,它的结构变为如下所示。

可以发现,此时的Widget层次结构多出了一个View结构,这无疑会给后面的分析带来不必要的干扰,因此需要看下有无办法去掉View结构部分。
看了下runApp的源码,发现View结构这一部分是在WidgetsBinding的wrapWithDefaultView方法中被添加进来的。


再看下wrapWithDefaultView方法的注释: 由runApp用于将提供的rootWidget包装在默认View中。 View决定应用程序渲染到哪个FlutterView中。目前这是来自platformDispatcher的PlatformDispatcher.implicitView 。提供给此方法的rootWidget必须尚未包装在View中。
OK,这wrapWithDefaultView方法的注释看得一头雾水,没关系,再看下View的红框部分注释。

翻译:提供的child包装在根据给定view构造的MediaQuery中。 查看了View的部分源码发现的确如此。

这里的child指我们传入的Widget,view指FlutterView,结合之前的分析,笔者大胆猜测wrapWithDefaultView方法的用意:用View对FlutterView进行一个指定渲染的优化管理,在View中先让MediaQuery初始化,为的是让后续的子Widget能够通过MediaQuery获取相关媒体查询数据。也就是说,如果不想调用wrapWithDefaultView方法,只要保证后续子Widget没用到MediaQuery就OK。
前面所给出的MyApp代码中是完全没有用到MediaQuery的,所以这点不用担心,我们可以大大方方的不调用wrapWithDefaultView方法,改造后runApp方法如下。
|
|
但是这样还不行,因为scheduleAttachRootWidget方法是一个@protected方法,外部调用该方法的话会有警告,所以直接把方法体的代码摘抄出来就好了。
|
|
再次运行程序,然后通过Flutter Inspector查看Widget层次结构,发现与预期的Widget层次结构一致。

温馨提示:此处runApp方法的改动是为了在Debug调试源码过程中减少其它Widget的干扰,而在实际项目中不能这样做。
三、Root分析
OK,现在从程序入口_runApp
方法开始分析。该方法接收一个Widget参数,接下来执行WidgetsFlutterBinding的ensureInitialized方法,返回一个WidgetsBinding实例。
2.1、WidgetsFlutterBinding.ensureInitialized()
|
|
由上面源码可知,在ensureInitialized方法中,首先会判断WidgetsBinding._instance == null
,如果满足条件,则调用WidgetsFlutterBinding的构造方法进行初始化,最后返回WidgetsBinding.instance
实例。
当然了,第一次调用ensureInitialized方法时WidgetsBinding._instance肯定为null,这里可以打个断点Debug看下。

此时会有个意外的发现,WidgetsFlutterBinding中根本就没有构造方法,毫无疑问会执行其父类BindingBase的构造方法。
OK,我们继续跟踪其父类BindingBase的源码。

发现父类BindingBase构造方法中有一个initInstances方法,这里打个断点Debug调试一下,进入initInstances方法。

然后进入了WidgetsBinding的initInstances方法,继续Debug下一步,来到super.initInstances()
。

继续Debug进入super.initInstances()
,然后进入了RendererBinding的initInstances方法。

以此类推,当断点Debug调试走到BindingBase的initInstances方法时,它的调用堆栈如下图所示。

对于出现这样栈帧顺序的方法调用栈,其实并不感到意外,回顾之前WidgetsFlutterBinding的声明处源码。
|
|
可以发现,WidgetsFlutterBinding混入了GestureBinding、SchedulerBinding、ServicesBinding、PaintingBinding、SemanticsBinding、RendererBinding、WidgetsBinding这七个Binding,并且这些Binding都是BindingBase的子类,每个Binding都重写了父类BindingBase的initInstances方法,也在各自重写的initInstances方法体中调用了super.initInstances()
以及执行了实例变量引用的赋值_instance = this
。
因此,initInstances方法的调用顺序为:WidgetsBinding -> RendererBinding -> SemanticsBinding -> PaintingBinding -> ServicesBinding -> SchedulerBinding -> GestureBinding,最后才调用父类BindingBase中的initInstances方法,这点从上图的方法调用栈也可以看出。
也许你会有疑问:为什么initInstances方法的调用顺序是这样的?
这其实是Dart语言中with关键字的一个语法特性了,如果一个类with混入的多个类中都有相同的方法(例如上面的initInstances方法),那么当调用该方法时,只会调用距离with关键字最远类中的方法,也就是上面WidgetsBinding中的initInstances方法。
但是由于WidgetsBinding在initInstances方法中又调用了super.initInstances()
,所以才会一级一级地往父类方向调用,直到调用父类BindingBase中的initInstances方法。
因为调用的是WidgetsFlutterBinding的构造方法来进行初始化,所以ensureInitialized方法实例化的是WidgetsFlutterBinding对象了,只不过把实例引用赋值给了WidgetsBinding,如下图所示。

2.2、attachRootWidget方法
在WidgetsFlutterBinding.ensureInitialized()
实例化WidgetsFlutterBinding后,接下来会继续执行WidgetsBinding的attachRootWidget方法,这个方法接收了一个Widget参数。

那么attachRootWidget方法做了什么事情?看下它的注释。
|
|
翻译:获取一个Widget并将其附加到rootElement ,并在必要时创建它。 attachRootWidget方法的源码如下图所示。

由上面源码可知,attachRootWidget方法体中先是执行了RenderObjectToWidgetAdapter构造方法,然后再执行该实例对象的attachToRenderTree方法。
2.3、RenderObjectToWidgetAdapter构造方法
先打个断点Debug看下RenderObjectToWidgetAdapter构造方法传入的参数。

可以看到,RenderObjectToWidgetAdapter构造方法传入了3个参数,其中参数child就是我们自己的MyApp;参数container传入了renderView,它的类型为RenderView;最后一个参数debugShortDescription从名字上看是调试用的,这个不用管。
关于参数container传入了renderView,那么这个renderView是怎么来的呢?
回顾下之前所讲的那七个混入Binding之一的RendererBinding,其实renderView是在RendererBinding的initInstances方法中初始化的。



OK,RenderObjectToWidgetAdapter构造方法的参数分析完毕,接下来继续看attachToRenderTree方法。
2.4、attachToRenderTree方法
先打个断点Debug看下attachToRenderTree方法传入的参数。

可以看到,attachToRenderTree方法接收2个参数,一个是BuildOwner,另一个是可选的位置参数RenderObjectToWidgetElement。
关于传入的参数buildOwner,那么这个buildOwner是怎么来的呢?它是在那七个混入Binding之一的WidgetsBinding的initInstances方法中初始化的。


OK,开始分析attachToRenderTree方法源码,因为element为null,所以重点分析下红框部分。

owner.lockState()
接收一个VoidCallback回调,lockState方法体中只是进行一些断言操作,然后在try-finally中执行callback方法,lockState方法的源码如下。

lockState方法体中没做什么操作,所以lockState方法回调中先调用了createElement方法,创建Element实例后,再调用Element实例的assignOwner方法,因为assignOwner方法不是主线的重点,所以这部分不做讲解。
继续看主线的重点:createElement方法,它调用的是RenderObjectToWidgetAdapter这个Widget的createElement方法。

可以发现,调用了RenderObjectToWidgetElement构造方法,方法参数传入的是this,表示RenderObjectToWidgetAdapter本身。

RenderObjectToWidgetElement构造方法参数执行super.widget
,就会把widget一级一级往上传递,先是执行了 RenderObjectElement的构造方法,最后执行了Element的构造方法。并且在Element的构造方法中,把一路传递过来的this,也就是RenderObjectToWidgetAdapter本身赋值给了Element的成员变量_widget
,那么Element就持有了这个Widget。


OK,owner.lockState()
已经分析完成,继续看owner.buildScope()
,buildScope方法体的实现与前面的lockState方法较为相似,也只是进行一些断言判断,最后在try-finally中执行callback方法。

buildScope方法体中没做什么操作,所以在buildScope方法回调中就会执行element的mount方法,也就是RenderObjectToWidgetElement的mount方法。

分析下RenderObjectToWidgetElement的mount方法,这里分两步走,一步是执行super.mount,也就是执行父类的mount方法;另一步是执行_rebuild
方法,这个后面会讲。
看下第一步,也就是执行父类的mount方法,它会一级一级往上执行,先是执行RenderObjectElement的mount方法,最后执行Element的mount方法。


可以看到,在Element的mount方法中主要是对一些属性进行赋值操作,如_parent
等。当Element的mount方法执行完成后就会出栈,然后回来执行RenderObjectElement的mount方法。
在RenderObjectElement的mount方法中会执行widget的createRenderObject方法,想必该Widget是谁应该知道吧,它就是之前在构造方法中将参数一级一级往上传递的this,也就是RenderObjectToWidgetAdapter,它是Root
Widget,如果不确定,可以Debug断点调试看下。

继续执行RenderObjectToWidgetAdapter的 createRenderObject方法,会返回一个container,也就是之前讲的RenderView。

然后将createRenderObject方法返回值赋值给RenderObjectElement的成员变量_renderObject
,于是得出一个结论:RenderObjectElement会持有一个RenderObject实例。

接着,继续执行RenderObjectElement中的attachRenderObject方法。

在RenderObjectElement中的attachRenderObject方法中,它一共做了三件事情。
1、执行_findAncestorRenderObjectElement
方法,沿着父类往上查找最远处RenderObjectElement类型的Element,并且赋值给 _ancestorRenderObjectElement
;其中_parent
是在Element的mount方法中赋值,一开始_parent
为null,所以_findAncestorRenderObjectElement
方法返回为null,相当于该方法没执行。

2、执行上一步返回的_ancestorRenderObjectElement
的insertRenderObjectChild方法将renderObject插入渲染树中;
但是由于_ancestorRenderObjectElement
为null,所以insertRenderObjectChild方法并没有触发。
3、执行_findAncestorParentDataElement
方法,沿着父类往上查找最近处ParentDataElement_parent
为null,所以该方法执行了个寂寞。

因此,整体上RenderObjectElement中的attachRenderObject方法相当于没执行到。
OK,RenderObjectElement的mount方法分析完了,此时会继续出栈,然后继续执行RenderObjectToWidgetElement的mount方法中super.mount()
之后的逻辑。
也就是执行RenderObjectToWidgetElement的_rebuild
方法,在 _rebuild
方法中,执行了updateChild方法,参数一_child
为null,参数二就是我们自己的MyApp,参数三为Object。

继续执行Element的updateChild方法,因为此时child为null,所以会执行红框部分,也就是执行inflateWidget方法。

继续执行Element的inflateWidget方法,之前已经知道传入的newWidget为MyApp,看下它的源码。

在Element中的inflateWidget方法中,它一共做了3件事情。
1、校验key是否为GlobalKey,如果是就会做相应逻辑,因为不是此次主线重点,所以不做讲解。
2、执行newWidget的createElement方法,也就是执行MyApp的createElement方法,返回一个Element实例。
3、执行上一步返回Element实例的mount方法。
看到这里,相信你会觉得createElement、mount方法似曾相识,因为之前分析过了。
总结下Root分析的执行流程,如下图所示。

四、StatelessWidget分析
接着Root分析,继续执行MyApp的createElement方法,因为MyApp的类型是StatelessWidget,所以执行了StatelessWidget的createElement方法。

然后创建了StatelessElement实例,并将this传入StatelessElement的构造方法中,this也就是MyApp本身,而StatelessElement构造方法的参数执行了super.widget
,会一级一级地向父类传递this,先是执行了ComponentElement的构造方法,最后再执行Element的构造方法,将传递的this赋值给Element的成员变量_widget
。



这种子类Element向父类一级一级传递this的行为我们之前讲过的,也是似曾相识了。
接着Root分析,继续执行Element的mount方法。因为StatelessElement没有mount方法,所以执行的是ComponentElement的mount方法。

可以看到,先是执行了super.mount
,也就是执行Element的mount方法,这个之前看过,只是一些成员变量的赋值操作。
然后执行了_firstBuild
方法,在该方法中又执行了Element的rebuild方法。

在Element的rebuild方法中,try-finally处执行了performRebuild方法,然后又回到了ComponentElement的performRebuild方法。

在ComponentElement的performRebuild方法中,做了两件事情。
1、执行build方法。这里执行了StatelessElement的build方法。

这里的widget就是我们自己的MyApp,所以执行了MyApp的build方法,创建了MyPage实例。

2、执行了Element的updateChild方法,看到该方法是否又似曾相识,OK,继续分析。
因为此时child为null,所以会执行红框部分,也就是执行inflateWidget方法。

继续执行Element的inflateWidget方法,之前已经知道传入的newWidget为MyPage,看下它的源码。

在Element中的inflateWidget方法中,它一共做了三件事情,这个前面讲过。
1、校验key是否为GlobalKey,如果是就会做相应逻辑,因为不是此次主线重点,所以不做讲解。
2、执行newWidget的createElement方法,也就是执行MyPage的createElement方法,返回一个Element实例。
3、执行上一步返回Element实例的mount方法。
看到这里,相信你会觉得再熟悉不过,因为之前多次分析过createElement、mount方法了。
五、StatefulWidget分析
接着StatelessWidget分析,继续执行MyPage的createElement方法,因为MyPage的类型是StatefulWidget,所以执行了StatefulWidget的createElement方法。

然后创建了StatefulElement实例,并将this传入StatefulElement的构造方法中,this也就是MyPage本身。
在StatefulElement的构造方法中,执行了widget.createState()
,widget就是我们的MyPage,也就是执行了MyPage的createState方法。
将创建好的_MyPageState
实例赋值给StatefulElement的成员变量_state
,也就是说StatefulElement持有了State实例。


与此同时,StatefulElement构造方法中还执行了super(widget)
,会一级一级地向父类传递this,先是执行了ComponentElement的构造方法,最后再执行Element的构造方法,将传递的this赋值给Element的成员变量_widget
。


这种子类Element向父类一级一级传递this的行为我们之前讲过的,也是老相识了。
接着StatelessWidget分析,继续执行Element的mount方法。因为StatefulElement没有mount方法,所以执行的是ComponentElement的mount方法。

可以看到,先是执行了super.mount
,也就是执行Element的mount方法,这个之前看过,只是一些成员变量的赋值操作。
然后执行了_firstBuild
方法,在该方法中又执行了Element的rebuild方法。

在Element的rebuild方法中,try-finally处执行了performRebuild方法,然后又回到了ComponentElement的performRebuild方法。

在ComponentElement的performRebuild方法中,做了两件事情。
1、执行build方法。这里执行了StatefulElement的build方法。

这里执行了state.build()
,也就是执行了_MyPageState
的build方法,创建了ColoredBox实例。

2、执行了Element的updateChild方法,该方法是老朋友了。
再后面就是ColoredBox的分析,因为ColoredBox最终父类的类型与根布局RenderObjectToWidgetAdapter的类型都是RenderObjectWidget,因此分析都是差不多的,后面就留给你们发挥了。
当然了,runApp方法的分析肯定不止上面这些内容,后续有机会再补上。