3.0 flutter tool的启动流程

1,665 阅读11分钟

本文主要介绍flutter tool的概念,以及flutter tool的启动流程,包含flutter tool更新、Zone的环境准备、命令解析3个阶段

注:flutter tool在Flutter SDK目录下,使用Android Studio可以直接打开Flutter SDK方便跟踪查看代码

1.1 flutter tool概念

Flutter为开发者提供了完备的开发工具支持,比如 flutter create 创建一个Flutter工程,flutter run构建并运行Flutter工程。对于IDE的快捷键和GUI操作,都是建立的flutter tool的基础上的。其源码在 flutter/packages/flutter_tools目录下。

flutter tool有十多个命令,但是各命令之间职责明确,依赖比较清晰。

执行flutter tool命令之后,将首先执行一系列Bash命令,完成SDK更新检查、Snapshot检查等准备工作,然后才会进入flutter tool的逻辑, 基于Dart的 Zone 机制,完成运行环境的准备,最后通过args工具解析出具体命令并执行对应的FlutterCommand。

ps: 对于Zone可以看下可以看下官方文档 Zone abstract class,因为Dart是单线程模式,Zone常被用来捕获异步逻辑产生的异常,在flutter tool中它还隔离了FlutterCommand运行环境。

1.2 基于Bash的环境准备

首先我们在开发过程中使用的各种flutter开头的命令,其对应的是flutter/bin目录下的同名脚本。 Bash环境准备的步骤大多是一些准备工作,简单说明下,大致如下:

  1. BIN_DIR相关环境变量准备(flutter/bin/flutter#follow_links作为入口寻找flutter脚本的绝对路径,具体由shared.sh脚本完成。)
  2. 运行环境检查(flutter_tools.snapshot,flutter_tools.stamp检查等,这两个文件会在flutter sdk升级时介绍)
  3. flutter tool依赖下载、构建、缓存和更新逻辑。
  4. 真正的逻辑执行阶段

1.3 Flutter SDK更新逻辑

如果需要改动flutter tool的源码,来修改一些打包流程或其它内容,你需要对Flutter SDK的更新逻辑有大概的了解,flutter tool的命令并不会直接执行源码,为了flutter tool相关的flutter命令的响应速度,会执行flutter tool编译成为DVM的可执行文件即上面提到的flutter_tools.snapshot

注: 如果有需要修改了flutter tool的源码(比如之前项目因为二方插件的缘故,插件不支持64位,打包时候需要指定32位的安装包),需要手动删除flutter_tools.snapshot文件,执行flutter 命令时,会重新编译生成该文件,以使改动的代码生效。

Flutter SDK的更新步骤大致分为四个步骤,如下:

  1. 判断是否需要更新,判断条件如下
  • a. flutter tool对应的Snapshot文件不存在 (即上面提到的flutter_tools.snapshot文件)

  • b. stamp文件不存在或大小为0(即上面提到的flutter_tools.stamp文件)

  • c. stampw文件存储的Flutter SDK 的reversion和当前Flutter SDK的reversion不同。(存放的是当前版本的commit id),如果不同表示拥护更新了Flutter SDK版本,对应的Dart SDK版本也可能会改变,flutter tool的Snapshot需要重新编译生成。

  • d. flutter tool 目录下pubspec.yaml文件在pubspec.lock生成之后发生了改变(就是pubspec.yaml文件比pubspec.lock文件新)。

  1. 第二阶段是加锁,防止多个flutter命令执行导致Flutter SDK并行更新。(flutter tool选择了一次只能有一个shell进程对Flutter SDK进行更新)
  2. 真正的执行更新逻辑
  3. 构建阶段(flutter tool编译出Snapshot产物,上面也提到该Snapshot是为了提高flutter的响应速度)

简单概括更新的过程就是:准备环境变量,检查curlunzip命令是否存在,然后拼接一个Dart SDK的资源路径。如果是本地源码调试等需求可以修改FLUTTER_STORAGE_BASE_URL(位置:flutter/bin/internal/update_dart_sdk.sh)来自定义路径。可以是curl的资源地址,也可以是本地路径。更新完成后会编译生成Snapshot文件,调用Snapshot产物执行flutter tool的真正逻辑。flutter tool的逻辑入口是flutter/packages/flutter_tools/bin/flutter_tools.dartmain方法。

2.1 基于Zone的上下文管理

因为Flutter是跨平台的,flutter tool同一个功能在不同的平台啊会有不同的实现。flutter tool借助Zone的语言特性进行设计,主要有一下两个重要的作用(上文附有Zone的官方文档)。

  • 为每个命令提供独立的上下文
  • 利用Zone里面变量隔离的特点为每个执行阶段提供独立的AppContext,功能解耦合,逻辑高度内聚 Zone的上下文管理在flutter tool中尤为重要,如果不了解,在跟读源码时容易被各种函数变量(就是callback)弄混。

flutter tool的入口方法在flutter_tools.dart的main方法中,而该方法实际调用了flutter/packages/flutter_tools/lib/executable.dart的main方法。代码如下(省略了部分flutter命令的的分析代码,如-v 是否输出详细日志等)

Future<void> main(List<String> args) async {
      // 这里省略了参数解析处理的部分代码...
  
  // runner指向的是同目录的runner.dart文件
  await runner.run(
    args,
    () => generateCommands(
      verboseHelp: verboseHelp,
      verbose: verbose,
    ),
    verbose: verbose, //是否输出详细信息
    muteCommandLogging: muteCommandLogging, // 是否关闭日志输出
    verboseHelp: verboseHelp,
    overrides: <Type, Generator>{ // 类型-生成器
      // The web runner is not supported in google3 because it depends
      // on dwds.
      WebRunnerFactory: () => DwdsWebRunnerFactory(),
      // The mustache dependency is different in google3
      TemplateRenderer: () => const MustacheTemplateRenderer(),
      // The devtools launcher is not supported in google3 because it depends on
      // devtools source code.
      DevtoolsLauncher: () => DevtoolsServerLauncher(
        processManager: globals.processManager,
        fileSystem: globals.fs,
        pubExecutable: globals.artifacts.getHostArtifact(HostArtifact.pubExecutable).path,
        logger: globals.logger,
        platform: globals.platform,
        persistentToolState: globals.persistentToolState,
      ),
      Logger: () {
        final LoggerFactory loggerFactory = LoggerFactory(
          outputPreferences: globals.outputPreferences,
          terminal: globals.terminal,
          stdio: globals.stdio,
        );
        return loggerFactory.createLogger(
          daemon: daemon,
          machine: runMachine,
          verbose: verbose && !muteCommandLogging,
          prefixedErrors: prefixedErrors,
          windows: globals.platform.isWindows,
        );
      },
    },
  );
}

从代码中可以看到 executable.dart 只是做了一些的简单的参数解析,并把参数传递给了 runner.run 方法。main方法主要做了以下工作:

  1. 部分参数的解析与处理(上述代码略过的部分)

  2. Cache.flutterRoot对象初始化(上述代码也略过了,暂时不会用到)

  3. 调用runner.run方法,这里的传参注意一下,其中第二个参数是commands传递的是一个函数,函数的返回值是FlutterCommand实例列表,而overrides参数是一个Map,其Generator也是一个生成具体实例的函数。方法的传接收的参数是函数参数,但是不会立即被调用。

接下来runner.run 方法会调用runInContext方法,并将上面提到的名为overrides的参数传入。注意这里的runInContext方法位于:路径:flutter/packages/flutter_tools/lib/src/context_runner.dart ,如下所示:

Future<T> runInContext<T>(
  FutureOr<T> Function() runner, {
  Map<Type, Generator> overrides,
}) async {

  // Wrap runner with any asynchronous initialization that should run with the
  // overrides and callbacks.
  bool runningOnBot;
  FutureOr<T> runnerWrapper() async {
    runningOnBot = await globals.isRunningOnBot;
    return runner();
  }

  return context.run<T>(  //新建一个AppContext
    name: 'global fallbacks', //AppContext的名称,
    body: runnerWrapper, //AppContext 对应的Zone将要执行的逻辑
    overrides: overrides,  //提供给AppContext的各种类的具体生成器,类似工厂方法
    fallbacks: <Type, Generator>{
        AndroidSdk: AndroidSdk.locateAndroidSdk,
        AndroidStudio: AndroidStudio.latestValid,
        //省略大量的<Type, Generator>
        //Generator是一个Function类型,定义是: typedef Generator = dynamic Function();
        
     }
  );
}

上面的runInContext方法的第一个参数是函数,作用是将flutter tool命令的参数转换成对应的FlutterCommand命令,runInContext没有具体作用,主要是调用了context#run方法,代码如下: 路径:flutter/packages/flutter_tools/lib/src/base/context.dart

Future<V> run<V>({ //属于AppContext
  required FutureOr<V> Function() body,
  String? name,  
  Map<Type, Generator>? overrides,
  Map<Type, Generator>? fallbacks,
  ZoneSpecification? zoneSpecification,
}) async {
  final AppContext child = AppContext._(
    this,   // AppContext的创建者,即parent
    name,   //AppContext的名称
    Map<Type, Generator>.unmodifiable(overrides ?? const <Type, Generator>{}),
    Map<Type, Generator>.unmodifiable(fallbacks ?? const <Type, Generator>{}),
  );
  return runZoned<Future<V>>(
    () async => await body(), // 上文提到的runnerWrapper 也是Zone的要执行的代码
    zoneValues: <_Key, AppContext>{_Key.key: child}, // 获取AppContext的入口
    zoneSpecification: zoneSpecification,
  );
}

上述这段逻辑是flutter tool启动过程中最关键的步骤,具体的逻辑在body中,官方注释说明是 Runs [body] in a child context and returns the value returned by [body]. 而overrides和fallbacks参数则懈怠了将要使用的类及生成器。每个Zone将会携带一个AppContext,即当前逻辑的上下文。具体的逻辑执行时,会通过context.get<T>()获取具体类型的实例。如果不存在则会在此时初始化(懒加载)。代码逻辑如下: 路径:flutter/packages/flutter_tools/lib/src/base/context.dart

const Object contextKey = _Key.key; // 上文中存储当前Zone的key
static final AppContext _root = AppContext._(null, 'ROOT'); //Bootstrap context.
AppContext get context => Zone.current[contextKey] as AppContext? ?? AppContext._root; // 当前AppContext,由Zone的层级决定


class AppContext {
    T? get<T>() {
      dynamic value = _generateIfNecessary(T, _overrides);//优先使用overrides
      if (value == null && _parent != null) { //在parent中递归查找
        value = _parent!.get<T>();
      }
      return _unboxNull(value ?? _generateIfNecessary(T, _fallbacks)) as T?; // 兜底
    }
}

对于类型T,AppConext会从当前Zone中获取其实例,如果不存在从父AppContext中查找,_generateIfNecessary用与生成类型T对应的实例。代码如下:

dynamic _generateIfNecessary(Type type, Map<Type, Generator> generators) {
  if (!generators.containsKey(type)) {
    return null;
  }

  return _values.putIfAbsent(type, () { //_values负责缓存
    _reentrantChecks ??= <Type>[];

    final int index = _reentrantChecks!.indexOf(type);
    if (index >= 0) {
      // We're already in the process of trying to generate this type.
      throw ContextDependencyCycleException._(
          UnmodifiableListView<Type>(_reentrantChecks!.sublist(index)));
    }

    _reentrantChecks!.add(type);
    try {
      return _boxNull(generators[type]!());
    } finally {
      _reentrantChecks!.removeLast();
      if (_reentrantChecks!.isEmpty) {
        _reentrantChecks = null;
      }
    }
  });
}

上述逻辑根据类型T生成具体的实例,如果当前Context不存在对应的generators或generators为null返回null,_reentrantChecks负责检查循环依赖,如果存在循环依赖直接抛出异常。到这里基本就理清了context.get<T>的底层逻辑

下面回头看下FlutterCommandRunner实例具体创建的地方(上文中flutter/packages/flutter_tools/lib/src/context_runner#runInContext方法的调用处)。

路径:flutter/packages/flutter_tools/lib/runner.dart

/// Runs the Flutter tool with support for the specified list of [commands].
Future<int> run(
  List<String> args,
  List<FlutterCommand> Function() commands, {
    bool muteCommandLogging = false,
    bool verbose = false,
    bool verboseHelp = false,
    bool reportCrashes,
    String flutterVersion,
    Map<Type, Generator> overrides,
  }) async {
  if (muteCommandLogging) { //关闭日志
    // Remove the verbose option; for help and doctor, users don't need to see
    // verbose logs.
    args = List<String>.of(args);
    args.removeWhere((String option) => option == '-v' || option == '--verbose');
  }

  return runInContext<int>(() async {
    reportCrashes ??= !await globals.isRunningOnBot;
    // 创建FlutterCommandRunner实例
    final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: verboseHelp);
     // 添加所有FlutterCommand (executable.dart的main方法调用runner.run方法中的generateCommands生成的FlutterCommand )
    commands().forEach(runner.addCommand);

    // Initialize the system locale.
    // 本地化相关的初始化
    final String systemLocale = await intl_standalone.findSystemLocale();
    intl.Intl.defaultLocale = intl.Intl.verifiedLocale(
      systemLocale, intl.NumberFormat.localeExists,
      onFailure: (String _) => 'en_US',
    );

    //Flutter版本号
    String getVersion() => flutterVersion ?? globals.flutterVersion.getVersionString(redactUnknownBranches: true);
    Object firstError; //错误处理相关
    StackTrace firstStackTrace;
    return runZoned<Future<int>>(() async {
      try {
        // 根据参数执行具体的FlutterCommand
        await runner.run(args);

        // Triggering [runZoned]'s error callback does not necessarily mean that
        // we stopped executing the body.  See https://github.com/dart-lang/sdk/issues/42150.
        if (firstError == null) {
          // 运行无异常,结束并退出
          return await _exit(0);
        }

        // We already hit some error, so don't return success.  The error path
        // (which should be in progress) is responsible for calling _exit().
        return 1;
      // This catches all exceptions to send to crash logging, etc.
      } catch (error, stackTrace) {  // ignore: avoid_catches_without_on_clauses
        firstError = error;
        firstStackTrace = stackTrace;
        return _handleToolError(
            error, stackTrace, verbose, args, reportCrashes, getVersion);
      }
    }, onError: (Object error, StackTrace stackTrace) async { // ignore: deprecated_member_use
      // If sending a crash report throws an error into the zone, we don't want
      // to re-try sending the crash report with *that* error. Rather, we want
      // to send the original error that triggered the crash report.
      firstError ??= error;
      firstStackTrace ??= stackTrace;
      await _handleToolError(firstError, firstStackTrace, verbose, args, reportCrashes, getVersion);
    });
  }, overrides: overrides);
}

上述代码看注释,创建了FlutterCommandRunner实例,然后将前面提到的所有FlutterCommand加入到FlutterCommandRunner,然后通过run方法来执行具体逻辑,最终会调用到自身的runCommand方法,如下。

flutter/packages/flutter_tools/lib/src/runner/flutter_command_runner.dart

@override
Future<void> runCommand(ArgResults topLevelResults) async {
  final Map<Type, dynamic> contextOverrides = <Type, dynamic>{};
  // 省略参数解析
  await context.run<void>(
    overrides: contextOverrides.map<Type, Generator>((Type type, dynamic value) {
      return MapEntry<Type, Generator>(type, () => value);
    }),
   
    body: () async {//实际的执行逻辑
      //省略参数解析
      await super.runCommand(topLevelResults);
    },
  );
}

该方法通过前面内容提到的AppContext再创建一个Zone对象,同样其实际的执行逻辑在body参数中, 也就是super.runCommand(topLevelResults),调用了args库中CommandRunner对象的一个方法,该方法最终会调用FlutterCommand的run方法。如下: 路径: flutter/packages/flutter_tools/lib/src/runner/flutter_command.dart

@override
Future<void> run() {
  final DateTime startTime = globals.systemClock.now();

  return context.run<void>( //再次新建一个AppContext
    name: 'command', // 名称
    overrides: <Type, Generator>{FlutterCommand: () => this},
    body: () async { //实际的执行逻辑
      // Prints the welcome message if needed.
      globals.flutterUsage.printWelcome();
      _printDeprecationWarning(); //过低版本警告信息
      final String commandPath = await usagePath;
      _registerSignalHandlers(commandPath, startTime); //注册程序终止处理回调
      FlutterCommandResult commandResult = FlutterCommandResult.fail();
      try { //验证并执行逻辑
        commandResult = await verifyThenRunCommand(commandPath);
      } finally {
        final DateTime endTime = globals.systemClock.now();
        globals.printTrace(userMessages.flutterElapsedTime(name, getElapsedAsMilliseconds(endTime.difference(startTime))));
        _sendPostUsage(commandPath, commandResult, startTime, endTime);
      }
    },
  );
}

FlutterCommand是所有子Command的父类,几乎所有的flutter xxx命令都对应一个具体的子类。上面的代码跟踪过程中有很多context.run方式启动的子命令,具体到FlutterCommand子类执行时,其AppContext的(name)关系应该是 command -> null -> global fallbacks -> ROOT

2.2 基于args的子命令管理

简单来说上面的只是分析了flutter tool的命令执行前的准备工作。还没有到flutter tool执行的实际逻辑。

主题逻辑代码调用顺序如图:

image.png

最终调用的是CommanderRunner的runCommand方法,代码如下:

路径:/Users/lwj/Desktop/development/flutter/.pub-cache/hosted/pub.dartlang.org/args-2.2.0/lib

Future<T?> runCommand(ArgResults topLevelResults) async {
  var argResults = topLevelResults;
  var commands = _commands; //create/build/attach等一级子命令
  Command? command;
  var commandString = executableName; //记录命令信息,方便输出帮助异常等

  while (commands.isNotEmpty) {
    if (argResults.command == null) { //各种异常处理
      if (argResults.rest.isEmpty) {
        if (command == null) {
          // No top-level command was chosen.
          printUsage();
          return null;
        }

        command.usageException('Missing subcommand for "$commandString".');
      } else {
        var requested = argResults.rest[0];

        // Build up a help message containing similar commands, if found.
        var similarCommands =
            _similarCommandsText(requested, commands.values);

        if (command == null) {
          usageException(
              'Could not find a command named "$requested".$similarCommands');
        }

        command.usageException('Could not find a subcommand named '
            '"$requested" for "$commandString".$similarCommands');
      }
    }

    // Step into the command.
    argResults = argResults.command!;
    command = commands[argResults.name]!; //从flutter 子命令中提取command的对应子类
    command._globalResults = topLevelResults; //更新当前command信息
    command._argResults = argResults;
    commands = command._subcommands as Map<String, Command<T>>; //更新commands信息,如build的apk/aar子命令
    commandString += ' ${argResults.name}';

    if (argResults.options.contains('help') && argResults['help']) {
      command.printUsage();
      return null;
    }
  }

  if (topLevelResults['help']) {
    command!.printUsage();
    return null;
  }

  // Make sure there aren't unexpected arguments.
  if (!command!.takesArguments && argResults.rest.isNotEmpty) {
    command.usageException(
        'Command "${argResults.name}" does not take any arguments.');
  }

  return (await command.run()) as T?;
}

这段逻辑的主要作用是找到要执行的FlutterCommand,以flutter build apk为例,从FlutterCommand开始搜索,第一轮while循环解析到build关键字,此时commands对用BuildCommand的子命令集合,第二轮while循环解析出apk关键字,那么其对应的就是BuildApkCommand,而该命令是具体可执行的命令,其commands为空,结束while循环,FlutterCommand真正的开始执行逻辑。上述代码中的最后执行的command.run()调用的最核心的逻辑是verifyThenRunCommand代码如下:

路径: flutter/packages/flutter_tools/lib/src/runner/flutter_command.dart

@mustCallSuper
Future<FlutterCommandResult> verifyThenRunCommand(String commandPath) async {
  // Populate the cache. We call this before pub get below so that the
  // sky_engine package is available in the flutter cache for pub to find.
  if (shouldUpdateCache) {
    ... //省略缓存更新
  }
  globals.cache.releaseLock();

  await validateCommand(); //检查命令有效性

  if (shouldRunPub) {
    ... //省略执行 flutter pub get
  }

  setupApplicationPackages();

  if (commandPath != null) {
    Usage.command(commandPath, parameters: CustomDimensions(
      commandHasTerminal: globals.stdio.hasTerminal,
    ).merge(await usageValues));
  }

  return runCommand(); //FlutterCommand的具体子类实现
}

该过程会进行一些环境检查,按需在执行FlutterCommand前处理一些逻辑。如上述注释(控制台看到 Running “flutter pub get”),如果项目不存在android目录,也会自动生成.android 目录