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

avatar
代码迁移工

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

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

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

程序开始的 Main 方法

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

Flutter 中 main 方法的默认签名如下:

void main() { 

    runApp(MyApp()); 
    
}

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

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

而 Dart 的 main 方法支持异步、传参。下面我们分别来看。

让 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 方法支持参数

Dart 的 mian 方法也支持添加入参,规则如下:

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

下面我们分别来看:

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

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

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

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

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

所以控制台会打印出 name 和 sun。

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

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

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


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

FlutterActivityAndFragmentDelegate 是代理器,处理 FlutterActivity 和 FlutterFragment 的通用逻辑。

第一处的绑定就是初始化了 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,表示引擎会延迟初始化,直到数据可用才初始化。

所以 FlutterActivity 的 onCreate 构造了一些初始化的东西:FlutterEngine 、FlutterView、DartExecutor等等。

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

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 的特殊性,异步并不常用,由于引擎的限制,参数并不支持。

Flutter 的 main 方法 是 FlutterActivity 或者 FlutterFragment 的 onStart 方法中调用的,调用的方式是 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();
  }
}

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

根据 renderView 和 rootWidget 来生成一个 RenderObjectToWidgetAdapter,RenderObjectToWidgetAdapter 是 Element。三颗树的关系可以看这里。

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

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

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

预热帧

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

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

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

image.png

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

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

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

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

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

小结

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

image.png

总结

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