Flutter 必知必会系列 —— runApp 做了啥

avatar
代码迁移工

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情

前面介绍了 Flutter 的三棵树机制、Flutter 的自定义绘制、Flutter 路由机制,这些机制都是建立在 Flutter 已经运行起来的基础上的,那么 Flutter 是怎么运行的呢?

接下来的几篇文章,就介绍 Flutter 运行之前的准备工作。

程序开始的 Main 方法

每个程序都有入口方法,比较熟悉的 Java 项目就是 public static voidmain 方法,同样 Flutter 应用也是一个单纯的 Dart 程序,所以它的入口也是 main 方法。

Fluttermain 方法的默认签名如下:

void main() { 

    runApp(MyApp()); 
    
}

它是不接受参数,也没有返回值的同步方法。

这里注意一点,这个方法仅仅是 Flutter 程序的 main 签名。

Dartmain 方法支持异步、传参。下面我们分别来看。

让 main 方法异步

Dart 程序可以用同步的方式写异步代码,就是一组 async\/await 关键字,这组关键字的使用可以看这里:

改造之后的代码如下:


void main() async{ 
  await Future.delayed(Duration(seconds: 3)); 
  print('await 3 seconds'); 
}

和普通的 main 方法相比,上面的 main 方法在声明处增加了 async 关键字,在方法体内部增加了 await 关键字。

打印语句并不会立马执行,会在等待 3 秒之后执行。

Flutter 支持这种语法吗? 支持的!但是让 main 变成 async 的用处并不多。

让 main 方法支持参数

Dartmian 方法也支持添加入参,规则如下:

  • 第一个位置的普通参数必须是 List 数组类型,后面可以跟着其他类型的参数
  • 不允许存在 required 标记的可选命名参数,参数是可 null 类型

下面我们分别来看:

void main(List<String> args) { 
   print(args); 
}

上面是声明的地方,那么怎么用呢?为程序运行增加额外参数

企业微信截图_e95e7067-5c09-4d06-a122-e2bfcf47acaf.png

我们以 IntelliJ IDEA 为例,就是在 Edit Configuration 的地方,配置 Program arguments 参数。

上面的程序就会收到一个数组,数组的内容是 namesun

所以控制台会打印出 namesun

但是 Flutter 并不支持这种语法。我们来看为啥不支持。我们以 Android 为例。

Android 中承载 Flutter 运行的是 FlutterActivity,运行 Flutter 代码的是 FlutterEngine

FlutterActivityActivity 一样,也有 onCreate 等方法,在其 onCreate 方法中也进行了初始化的操作。


@Override 
protected void onCreate(@Nullable Bundle savedInstanceState) { 
 /// 代码省略 
 delegate = new FlutterActivityAndFragmentDelegate(this);
 delegate.onAttach(this);//第一处 
 /// 代码省略 
 }

FlutterActivityAndFragmentDelegate 是代理器,处理 FlutterActivityFlutterFragment 的通用逻辑。

第一处的绑定就是初始化了 FlutterEngine 并进行了绑定。

初始化如下:


void setupFlutterEngine() {
    
  // First, check if the host wants to use a cached FlutterEngine.
  String cachedEngineId = host.getCachedEngineId();
  ///--------
  // Second, defer to subclasses for a custom FlutterEngine.
  flutterEngine = host.provideFlutterEngine(host.getContext());
  ///--------
  flutterEngine =
      new FlutterEngine(
          host.getContext(),
          host.getFlutterShellArgs().toArray(),
          /*automaticallyRegisterPlugins=*/ false,
          /*willProvideRestorationData=*/ host.shouldRestoreAndSaveState());
  isFlutterEngineFromHost = false;
}

初始化 Engine 的流程就是:首先从缓存中取 Engine ,要是没有的话,就尝试生成用户自定义的 Engine这两个要是都没有的话,就真正执行构造方法来生成一个引擎

生成引擎的入参如下:

  • 第一个参数上下文:如果是 FlutterActivity 的话,这个上下文就是 Activity 本身,如果是 FlutterFragment 的话,这个上下文就是 Fragment 的宿主 Activity

  • 第二个参数虚拟机参数:dartVmArgs 就是 getFlutterShellArgs 返回值,这个返回值的类型是字符串类型的数组。用于设置 Dart 虚拟机的运行参数,比如:ARG_ENABLE_DART_PROFILING 就是虚拟机的端口号之类的。

更明确一点:

企业微信截图_138a2180-a15e-47ae-b7a8-2fec63d992a4.png

所以这个并不是 main 的参数,而是虚拟机的运行配置。

  • 第三个参数是否自动注册插件,就是我们在 pubspec.yaml 文件下注册的插件是否自动注册到native 的工程中,一般是自动的。

注册就是调用 GeneratedPluginRegister 的注册方法。

  • 第四个参数就是表示引擎是否延迟初始化来响应某些数据,如果设置为 true,表示引擎会延迟初始化,直到数据可用才初始化。

所以 FlutterActivityonCreate 构造了一些初始化的东西:FlutterEngineFlutterViewDartExecutor等等。

Activity 中用于显示的是 onStartFlutterActivityonStart 调用了代理的逻辑,我们看其中的一段流程。

private void doInitialFlutterViewRun() {
  /// 省略代码
  String initialRoute = host.getInitialRoute();
  if (initialRoute == null) {
    initialRoute = maybeGetInitialRouteFromIntent(host.getActivity().getIntent());
    if (initialRoute == null) {
      initialRoute = DEFAULT_INITIAL_ROUTE;
    }
  }
 
  // 省略代码
  DartExecutor.DartEntrypoint entrypoint =
      new DartExecutor.DartEntrypoint(
          appBundlePathOverride, host.getDartEntrypointFunctionName());
  flutterEngine.getDartExecutor().executeDartEntrypoint(entrypoint); //第一处
}

在运行 Flutter 代码之前做了两件事:

  • 确定初始化路由

  • 执行 Dart 的入口代码

我们看第一处,第一处只有方法名没有方法的参数,对吧! 方法的名字是什么呢?

public String getDartEntrypointFunctionName() {
  try {
    Bundle metaData = getMetaData();
    String desiredDartEntrypoint =
        metaData != null ? metaData.getString(DART_ENTRYPOINT_META_DATA_KEY) : null;
    return desiredDartEntrypoint != null ? desiredDartEntrypoint : DEFAULT_DART_ENTRYPOINT;
  } catch (PackageManager.NameNotFoundException e) {
    return DEFAULT_DART_ENTRYPOINT;
  }
}

就是 io.flutter.Entrypoint 对应的 value,默认是就是 main

这就是为什么可以执行到 Flutter 的 main 方法,这个过程中没有 main 方法的参数设置,所以 Flutter 的 main 方法不支持设置参数。

小结

Dart 程序的入口方法是 main 方法,支持异步 、入参等特性。但是由于 Flutter 的特殊性,异步并不常用,由于引擎的限制,参数并不支持。

Fluttermain 方法 是 FlutterActivity 或者 FlutterFragmentonStart 方法中调用的,调用的方式是 DartExecutor 来执行。

Flutter main 方法执行了啥

void main() {
  runApp(MyApp());
}

这是默认的 main 方法,这是最简单的 main,在我们的项目中我们可能会处理很多其他操作,但是最核心的就是一句话:runApp

下面我们来看其中的逻辑。

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized() // 第一处
    ..scheduleAttachRootWidget(app) //第二处
    ..scheduleWarmUpFrame(); //第三处
}

上面的三处代码就是三件事:WidgetsFlutterBinding 初始化,三棵树初始化并绑定,发布预热帧。

就是这三行代码运行了 Flutter 的工程。

WidgetsFlutterBinding 初始化

class WidgetsFlutterBinding extends BindingBase with GestureBinding, SchedulerBinding, ServicesBinding, PaintingBinding, SemanticsBinding, RendererBinding, WidgetsBinding {

  static WidgetsBinding ensureInitialized() {
    if (WidgetsBinding.instance == null)
      WidgetsFlutterBinding();
    return WidgetsBinding.instance!;
  }
}

ensureInitialized 确保初始化,就是一个确保单例的过程。

第一次执行 ensureInitialized 方法的时候,会走 BindingBase 及其子类的构造方法完成初始化,

确保 Flutter 项目完成初始化并只完成一次。

每一个 Binding 完成的初始化内容如下:

Binding 名作用
BindingBase初始化基类,规定了初始化的框架。initInstances 中完成初始化,比如单例等initServiceExtensions 完成服务注册初始化
GestureBinding初始化手势识别 和 手势追踪框架
SchedulerBinding初始化 帧调用任务
ServicesBinding初始化 插件通道、系统的插件。
PaintingBinding初始化 图片缓存
SemanticsBinding初始化 语义框架
RendererBinding初始化 渲染机制 和 根 RenderObject
WidgetsBinding初始化 Element 机制 和 Debug 显示机制

这一篇文章只介绍宏观上的流程,下一节详细介绍各种 Binding 的初始化内容。

绑定根 Widget

runApp 接受一个 Widget 参数,这个 Widget 就是我们的根 Widget

我们来看这个 Widget 是怎么绑定到根上的。

void scheduleAttachRootWidget(Widget rootWidget) {
  Timer.run(() {
    attachRootWidget(rootWidget);
  });
}

仅仅调用了 attach 的方法。其方法内部如下:


void attachRootWidget(Widget rootWidget) {
  final bool isBootstrapFrame = renderViewElement == null;
  _readyToProduceFrames = true;
  _renderViewElement = RenderObjectToWidgetAdapter<RenderBox>(
    container: renderView,
    debugShortDescription: '[root]',
    child: rootWidget,
  ).attachToRenderTree(buildOwner!, renderViewElement as RenderObjectToWidgetElement<RenderBox>?);
  if (isBootstrapFrame) {
    SchedulerBinding.instance!.ensureVisualUpdate();
  }
}

总结下来就是:先判断是否已经绑定过了。

根据 renderViewrootWidget 来生成一个 RenderObjectToWidgetAdapterRenderObjectToWidgetAdapter Element。三颗树的关系可以看这里。

现在三棵树的根节点都生成完毕了,然后调用 attachToRenderTree 方法把这三棵树管理起来。👉 Flutter 必知必会系列——三颗树到底是什么

管理就是 BuildOwner 来管理 Element,根 Element 发起三棵树的绑定流程,就是调用 Elementmount 方法,方法的细节可以看这里
👉 Element 生命周期

如果没有绑定过,那么就通过 SchedulerBinding 发起帧的调度和绘制流程。

预热帧

scheduleWarmUpFrame 之前的两行代码,完成了所有的准备工作。

scheduleWarmUpFrame 的作用就是尽可能快的把 Flutter 内容显示出来。

我们知道屏幕的显示是根据 Vsync 信号的,比如下面这张图:

image.png

每次收到 Vsync 之后,会进行一系列的计算等,然后显示出来。

scheduleWarmUpFrame 的作用就是不用等待下次的 Vsync,而是直接发起绘制。

发起绘制的是什么意思呢?就是安排帧

void scheduleWarmUpFrame() {
   /// 省略代码
    handleBeginFrame(null);
   /// 省略代码
    handleDrawFrame();
   /// 省略代码
}

省去了一些其他的准备和判断代码,handleBeginFramehandleDrawFrame 是核心代码。这两个方法是整个帧调度的核心,我们放在后面详细讲。

小结

Fluttermain 方法完成了前期的准备,三棵树的根节点的生成和绑定,发起预热帧三项任务。和前面的流程加起来就是:

image.png

总结

Flutter 程序的入口是 main 方法,调用 main 的地方是 Flutter 容器决定的。然后在 main 方法中执行了一系列的 Binding 初始化 和 根结点绑定的任务。做完这些铺垫,下一篇就看 Binding 是啥