Flutter - 探索run命令到底做了什么 🤔

2,713 阅读4分钟

欢迎关注微信公众号:FSA全栈行动 👋

一、背景

上一篇 Flutter - 我给官方提PR,解决run命令卡住问题 😃 中,虽然我提的 pr 最终成功 merge 了,但是我心中还存在着几点疑问,如:

  1. 只能删除 snapshot 文件才能使修改的 flutter_tools 代码生效吗?
  2. 终端内执行 flutter 命令后的流程是怎样的?

现在就让我们带着疑问一起来了解一下从 flutter runflutter_tools 调用 launchAndAttach 的过程吧。

二、终端

流程图如下:

flutter 文件

在终端内执行 flutter 命令,必然是与其环境变量相关的

# export PATH=~/developer/flutter/bin:$PATH
export PATH=~/fvm/default/bin:$PATH

在上述路径下找到名为 flutter 文件,并打开

#!/usr/bin/env bash

...

# /Users/lxf/fvm/versions/3.1.0/bin/flutter
PROG_NAME="$(follow_links "${BASH_SOURCE[0]}")"

# /Users/lxf/fvm/versions/3.1.0/bin
BIN_DIR="$(cd "${PROG_NAME%/*}" ; pwd -P)"


# To define `shared::execute()` function
source "$BIN_DIR/internal/shared.sh"

# $@: 代表输入的所有参数,但是每个区分对待
shared::execute "$@"

internal/shared.sh 脚本的内容引入,然后执行其 shared::execute 方法,并通过 "$@" 将我们在终端下输入的其它参数传递进去,即:

# 在终端上输入的内容
flutter run -v -d 设备id

# 实际上命令后续的参数是传递给了 shared::execute
shared::execute run -v -d 设备id

shared.sh 文件

打开 internal/shared.sh 来看一看

#!/usr/bin/env bash

...

function upgrade_flutter () (
  // 创建cache目录
  mkdir -p "$FLUTTER_ROOT/bin/cache"

  # 取出当前flutter源码HEAD提交的SHA1值
  local revision="$(cd "$FLUTTER_ROOT"; git rev-parse HEAD)"
  # 拼接,如 bcea432bce54a83306b3c00a7ad0ed98f777348d:
  local compilekey="$revision:$FLUTTER_TOOL_ARGS"

  # 判断缓存是否需要更新的几个步骤:
  #  * 没有snapshot文件, 或者
  #  * stamp文件不存在, 或者
  #  * stamp文件内容为空, 或者
  #  * stamp文件内的SHA1与当前HEAD的SHA1不同, 或者
  #  * pubspec.yaml 的修改时间比 pubspec.lock 的新
  if [[ ! -f "$SNAPSHOT_PATH" || ! -s "$STAMP_PATH" || "$(cat "$STAMP_PATH")" != "$compilekey" || "$FLUTTER_TOOLS_DIR/pubspec.yaml" -nt "$FLUTTER_TOOLS_DIR/pubspec.lock" ]]; then
    _wait_for_lock

    # A different shell process might have updated the tool/SDK.
    if [[ -f "$SNAPSHOT_PATH" && -s "$STAMP_PATH" && "$(cat "$STAMP_PATH")" == "$compilekey" && "$FLUTTER_TOOLS_DIR/pubspec.yaml" -ot "$FLUTTER_TOOLS_DIR/pubspec.lock" ]]; then
      exit $?
    fi

    # 拉取dart_sdk
    rm -f "$FLUTTER_ROOT/version"
    touch "$FLUTTER_ROOT/bin/cache/.dartignore"
    "$FLUTTER_ROOT/bin/internal/update_dart_sdk.sh"

    >&2 echo Building flutter tool...

    # Prepare packages...
    VERBOSITY="--verbosity=error"
    if [[ "$CI" == "true" || "$BOT" == "true" || "$CONTINUOUS_INTEGRATION" == "true" || "$CHROME_HEADLESS" == "1" ]]; then
      PUB_ENVIRONMENT="$PUB_ENVIRONMENT:flutter_bot"
      VERBOSITY="--verbosity=normal"
    fi
    export PUB_ENVIRONMENT="$PUB_ENVIRONMENT:flutter_install"
    if [[ -d "$FLUTTER_ROOT/.pub-cache" ]]; then
      export PUB_CACHE="${PUB_CACHE:-"$FLUTTER_ROOT/.pub-cache"}"
    fi
    pub_upgrade_with_retry

    # 编译产出snapshot文件
    "$DART" --verbosity=error --disable-dart-dev $FLUTTER_TOOL_ARGS --snapshot="$SNAPSHOT_PATH" --packages="$FLUTTER_TOOLS_DIR/.packages" --no-enable-mirrors "$SCRIPT_PATH"
    # 将 compilekey 写入到 stamp 文件
    echo "$compilekey" > "$STAMP_PATH"
  fi
  exit $?
)

# This function is intended to be executed by entrypoints (e.g. `//bin/flutter`
# and `//bin/dart`). PROG_NAME and BIN_DIR should already be set by those
# entrypoints.
function shared::execute() {
  export FLUTTER_ROOT="$(cd "${BIN_DIR}/.." ; pwd -P)"

  # If present, run the bootstrap script first
  BOOTSTRAP_PATH="$FLUTTER_ROOT/bin/internal/bootstrap.sh"
  if [ -f "$BOOTSTRAP_PATH" ]; then
    source "$BOOTSTRAP_PATH"
  fi

  FLUTTER_TOOLS_DIR="$FLUTTER_ROOT/packages/flutter_tools"
  SNAPSHOT_PATH="$FLUTTER_ROOT/bin/cache/flutter_tools.snapshot"
  STAMP_PATH="$FLUTTER_ROOT/bin/cache/flutter_tools.stamp"
  SCRIPT_PATH="$FLUTTER_TOOLS_DIR/bin/flutter_tools.dart"
  DART_SDK_PATH="$FLUTTER_ROOT/bin/cache/dart-sdk"
  DART="$DART_SDK_PATH/bin/dart"

  ...

  # 更新依赖、pub-cache目录
  upgrade_flutter 7< "$PROG_NAME"

  # BIN_NAME 的值为 flutter
  BIN_NAME="$(basename "$PROG_NAME")"
  case "$BIN_NAME" in
    flutter*)
      # 执行 flutter_tools.snapshot
      # exec
      # /Users/lxf/fvm/versions/3.1.0/bin/cache/dart-sdk/bin/dart
      # --disable-dart-dev
      # --packages="/Users/lxf/fvm/versions/3.1.0/packages/flutter_tools/.packages"
      # /Users/lxf/fvm/versions/3.1.0/bin/cache/flutter_tools.snapshot 
      exec "$DART" --disable-dart-dev --packages="$FLUTTER_TOOLS_DIR/.packages" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"
      ;;
    dart*)
      exec "$DART" "$@"
      ;;
    *)
      >&2 echo "Error! Executable name $BIN_NAME not recognized!"
      exit 1
      ;;
  esac
}
  • basename:去掉文件名前面的目录并打印出来
  • DARTdart 可执行文件的文件路径
  • FLUTTER_TOOLS_DIRflutter_tools 目录路径
  • FLUTTER_TOOL_ARGS:单独的空格分隔
  • SNAPSHOT_PATHflutter_tools.snapshot 文件路径
  • SCRIPT_PATH:编译的入口文件

所以最终执行的命令为:

exec \
/Users/lxf/fvm/versions/3.1.0/bin/cache/dart-sdk/bin/dart \
--disable-dart-dev \
--packages="/Users/lxf/fvm/versions/3.1.0/packages/flutter_tools/.packages" \
/Users/lxf/fvm/versions/3.1.0/bin/cache/flutter_tools.snapshot \
... 其它传递进来的参数

可以看到就是去执行 flutter_tools.snapshot

当然了,在到这里之前会执行 upgrade_flutter 方法,其主要是以下几个动作:

  1. 创建 bin/cache 目录
  2. 拉取 dart-sdk
  3. 创建或更新 flutter_tools.snapshot

为了确保 flutter_tools.snapshot 是存在的且为 "最新",其中判断是否需要对缓存进行更新有几个点:

  • 没有 flutter_tools.snapshot 文件, 或者
  • flutter_tools.stamp 文件不存在, 或者
  • flutter_tools.stamp 文件内容为空, 或者
  • flutter_tools.stamp 文件内的 SHA1 与当前 HEADSHA1 不同, 或者
  • pubspec.yaml 的修改时间比 pubspec.lock 的新

终端执行 flutter 命令的流程也就到这了,这部分让我们知道了 flutter 命令实际上就是去执行 flutter_tools.snapshot 文件,以及在 upgrade_flutter 方法的最后是去编译产出该 snapshot 文件,其编译的入口文件为 flutter/packages/flutter_tools/bin/flutter_tools.dart

那接下来让我们一起去看看 flutter_tools 的源码。

三、flutter_tools 源码

下面以命令 flutter run -v -d 设备ID 为例

整体流程图如下:

温馨提醒:请放大食用,最好可以边对照流程图边看下面的源码分析

1、准备阶段

在真正执行 flutter run 命令前会有一个准备阶段

入口文件

flutter_tools.dartflutter_tools 的入口文件,代码如下:

文件路径:bin/flutter_tools.dart

import 'package:flutter_tools/executable.dart' as executable;

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

作用就是去执行 executable.dart 文件里的 main 方法

main

文件路径:lib/executable.dart

Future<void> main(List<String> args) async {
  ...

  await runner.run(
    args,
    () => generateCommands(
      verboseHelp: verboseHelp,
      verbose: verbose,
    ),
    ...
  );
}

generateCommands 生成的是 List<FlutterCommand>,对应 lib/src/commands 目录下的所有命令,这里面包含了我们需要关注的 RunCommand,对应 flutter run

List<FlutterCommand> generateCommands({
  required bool verboseHelp,
  required bool verbose,
}) => <FlutterCommand>[
  AnalyzeCommand(verboseHelp: verboseHelp, fileSystem: globals.fs, platform: globals.platform, processManager: globals.processManager, logger: globals.logger, terminal: globals.terminal, artifacts: globals.artifacts!, allProjectValidators: <ProjectValidator>[]),
  AssembleCommand(verboseHelp: verboseHelp, buildSystem: globals.buildSystem),
  AttachCommand(verboseHelp: verboseHelp),
  BuildCommand(verboseHelp: verboseHelp),
  ChannelCommand(verboseHelp: verboseHelp),
  CleanCommand(verbose: verbose),
  ConfigCommand(verboseHelp: verboseHelp),
  CustomDevicesCommand(customDevicesConfig: globals.customDevicesConfig, operatingSystemUtils: globals.os, terminal: globals.terminal, platform: globals.platform, featureFlags: featureFlags, processManager: globals.processManager, fileSystem: globals.fs, logger: globals.logger),
  CreateCommand(verboseHelp: verboseHelp),
  DaemonCommand(hidden: !verboseHelp),
  DebugAdapterCommand(verboseHelp: verboseHelp),
  DevicesCommand(verboseHelp: verboseHelp),
  DoctorCommand(verbose: verbose),
  DowngradeCommand(verboseHelp: verboseHelp),
  DriveCommand(verboseHelp: verboseHelp, fileSystem: globals.fs, logger: globals.logger, platform: globals.platform),
  EmulatorsCommand(),
  FormatCommand(verboseHelp: verboseHelp),
  GenerateCommand(),
  GenerateLocalizationsCommand(fileSystem: globals.fs, logger: globals.logger),
  InstallCommand(),
  LogsCommand(),
  MakeHostAppEditableCommand(),
  PackagesCommand(),
  PrecacheCommand(verboseHelp: verboseHelp, cache: globals.cache, logger: globals.logger, platform: globals.platform, featureFlags: featureFlags),
  RunCommand(verboseHelp: verboseHelp),
  ScreenshotCommand(),
  ShellCompletionCommand(),
  TestCommand(verboseHelp: verboseHelp),
  UpgradeCommand(verboseHelp: verboseHelp),
  SymbolizeCommand(stdio: globals.stdio, fileSystem: globals.fs),
  // Development-only commands. These are always hidden,
  IdeConfigCommand(),
  UpdatePackagesCommand(),
];

runner.run 对应的是 runner.dart 文件里的 run 方法

run

文件路径: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 {
  ...

  return runInContext<int>(() async {
    ...

    final FlutterCommandRunner runner = FlutterCommandRunner(verboseHelp: verboseHelp);
    commands().forEach(runner.addCommand);

    ...

    String getVersion() => flutterVersion ?? globals.flutterVersion.getVersionString(redactUnknownBranches: true);
    Object firstError;
    StackTrace firstStackTrace;
    return runZoned<Future<int>>(() async {
      try {
        await runner.run(args);
        ...
    }, onError: (Object error, StackTrace stackTrace) async { // ignore: deprecated_member_use
      ...
    });
  }, overrides: overrides);
}

最终调用并返回 runInContext 的运行结果,传入的参数为回调函数,下面会执行

runInContext

runInContext 方法的代码如下:

文件路径: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>(
    name: 'global fallbacks',
    body: runnerWrapper,
    overrides: overrides,
    fallbacks: <Type, Generator>{
      ...
    },
  );
}

返回了 context.run,并且将 runnerWrapper 传递给了 body 参数,而 runnerWrapper 最终调用 runner,下面是 context.run 方法的实现:

Future<V> run<V>({
  required FutureOr<V> Function() body,
  String? name,
  Map<Type, Generator>? overrides,
  Map<Type, Generator>? fallbacks,
  ZoneSpecification? zoneSpecification,
}) async {
  final AppContext child = AppContext._(
    this,
    name,
    Map<Type, Generator>.unmodifiable(overrides ?? const <Type, Generator>{}),
    Map<Type, Generator>.unmodifiable(fallbacks ?? const <Type, Generator>{}),
  );
  return runZoned<Future<V>>(
    () async => await body(),
    zoneValues: <_Key, AppContext>{_Key.key: child},
    zoneSpecification: zoneSpecification,
  );
}

可以看到执行了 body,实际上就是执行 runnerWrapper,最终执行传进来的 runner 回调函数。

在调用 runInContext 时就传进了一个回调函数,在这个函数内创建了 FlutterCommandRunner 实例 runner,调用 commands 拿到上面提到的 generateCommands 方法返回的所有 FlutterCommand,并通过 addCommand 方法逐一添加到 runner 实例的 _commands 中,以及将相应的 command 和参数解析添加进 argParser 实例。 最后调用 FlutterCommandRunner 实例 runnerrun 方法。

我们先来看一下 addCommand 方法

CommandRunner.addCommand

文件路径:args/lib/command_runner.dart

Map<String, Command<T>> get commands => UnmodifiableMapView(_commands);
final _commands = <String, Command<T>>{};

ArgParser get argParser => _argParser;
final ArgParser _argParser;

...

/// Adds [Command] as a top-level command to this runner.
void addCommand(Command<T> command) {
  var names = [command.name, ...command.aliases];
  for (var name in names) {
    _commands[name] = command;
    argParser.addCommand(name, command.argParser);
  }
  command._runner = this;
}

以命令的名字和别名分别做为 keycommand 做为 value,这就是为什么执行命令时不论使用的是命令的名字还是别名,执行的结果都是一样的,因为都指向同一个 Command

ConfigCommand 对应的 flutter config --help 或是 flutter configure --help,输出的结果一致


class ConfigCommand extends FlutterCommand {
  ConfigCommand({ bool verboseHelp = false }) {
    argParser.addFlag('analytics', help: 'Enable or disable reporting anonymously tool usage statistics and crash reports.');
    argParser.addFlag('clear-ios-signing-cert', negatable: false, help: 'Clear the saved development certificate choice used to sign apps for iOS device deployment.');
    argParser.addOption('android-sdk', help: 'The Android SDK directory.');
    argParser.addOption('android-studio-dir', help: 'The Android Studio install directory.');
    argParser.addOption('build-dir', help: 'The relative path to override a projects build directory.', valueHelp: 'out/');
    argParser.addFlag('machine', negatable: false, hide: !verboseHelp, help: 'Print config values as json.');
		...
  }

  @override
  final String name = 'config';
	...

  @override
  final List<String> aliases = <String>['configure'];
  ...
}

回来继续看 FlutterCommandRunner 实例 runnerrun 方法

FlutterCommandRunner.run

文件路径:lib/src/runner/flutter_command_runner.dart

@override
Future<void> run(Iterable<String> args) {
  ...
  return super.run(args);
}

FlutterCommandRunnerrun 方法里主要还是调用了super.runsuper.run 如下

Future<T?> run(Iterable<String> args) =>
      Future.sync(() => runCommand(parse(args)));

ArgResults parse(Iterable<String> args) {
  try {
    return argParser.parse(args);
  } on ArgParserException catch (error) {
    if (error.commands.isEmpty) usageException(error.message);

    var command = commands[error.commands.first]!;
    for (var commandName in error.commands.skip(1)) {
      command = command.subcommands[commandName]!;
    }

    command.usageException(error.message);
  }
}

其先帮我们调用 argParser.parse 解析 args,再将解析后的数据传递给 runCommand 方法

FlutterCommandRunner.runCommand

runCommand 方法

文件路径:lib/src/runner/flutter_command_runner.dart

@override
Future<void> runCommand(ArgResults topLevelResults) async {
  ...
  await context.run<void>(
    overrides: contextOverrides.map<Type, Generator>((Type type, Object? value) {
      return MapEntry<Type, Generator>(type, () => value);
    }),
    body: () async {
      ...
      await super.runCommand(topLevelResults);
    },
  );
}

注意:这里的 runCommandFlutterCommandRunner 内重写了父类的 runCommand,最后在 body 内调用了父类(CommandRunner)的 runCommand 方法。

CommandRunner.runCommand

文件路径:args/lib/command_runner.dart

Future<T?> runCommand(ArgResults topLevelResults) async {
  var argResults = topLevelResults;
  var commands = _commands;
  Command? command;
  var commandString = executableName;

  while (commands.isNotEmpty) {
    ...

    // 取出匹配的command
    argResults = argResults.command!;
    command = commands[argResults.name]!;
    command._globalResults = topLevelResults;
    command._argResults = argResults;
    // 取出子命令数组赋值给commands
    commands = command._subcommands as Map<String, Command<T>>;
    commandString += ' ${argResults.name}';

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

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

匹配出 run 命令对应 RunCommand 实例的 command,并执行其 run 方法,即最后一行的 command.run()

注:RunCommand 继承自 FlutterCommand,且没有重写 run 方法,所以这里的 command.run() 调用的就是 FlutterCommandrun 方法。

2、命令执行

FlutterCommand.run

文件路径:lib/src/runner/flutter_command.dart

@override
Future<void> run() {
  ...

  return context.run<void>(
    name: 'command',
    overrides: <Type, Generator>{FlutterCommand: () => this},
    body: () async {
      ...
      try {
        commandResult = await verifyThenRunCommand(commandPath);
      } finally {
        ...
      }
    },
  );
}

verifyThenRunCommand 方法进行校验并真正开始执行相应的命令

@mustCallSuper
Future<FlutterCommandResult> verifyThenRunCommand(String? commandPath) async {
  globals.preRunValidator.validate();
  // 更新cache下的artifacts
  if (shouldUpdateCache) {
    // First always update universal artifacts, as some of these (e.g.
    // ios-deploy on macOS) are required to determine `requiredArtifacts`.
    final bool offline;
    if (argParser.options.containsKey('offline')) {
      offline = boolArg('offline');
    } else {
      offline = false;
    }
    await globals.cache.updateAll(<DevelopmentArtifact>{DevelopmentArtifact.universal}, offline: offline);
    await globals.cache.updateAll(await requiredArtifacts, offline: offline);
  }
  globals.cache.releaseLock();

  // 查找 pubspec.yaml 并更正项目路径
  await validateCommand();

  final FlutterProject project = FlutterProject.current();
  project.checkForDeprecation(deprecationBehavior: deprecationBehavior);

  if (shouldRunPub) {
    final Environment environment = Environment(
      artifacts: globals.artifacts!,
      ...
    );

    await generateLocalizationsSyntheticPackage(
      environment: environment,
      buildSystem: globals.buildSystem,
    );

    // 对项目执行 pub get,下载依赖
    await pub.get(
      context: PubContext.getVerifyContext(name),
      generateSyntheticPackage: project.manifest.generateSyntheticPackage,
      checkUpToDate: cachePubGet,
    );
    await project.regeneratePlatformSpecificTooling();
    if (reportNullSafety) {
      await _sendNullSafetyAnalyticsEvents(project);
    }
  }

  setupApplicationPackages();

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

  // 真正去执行相应的命令
  return runCommand();
}

这个方法主要做了以下几点:

  1. 更新 flutter/bin/cache 下的 artifacts
  2. 校验当前目录下是否有 pubspec.yaml 文件,没有就往父路径里找,并修正项目路径
  3. 执行 pub get 下载项目依赖
  4. runCommand() 执行的是 RunCommand 下的 runCommand

注:从这个地方开始才真正去执行 run 命令!

RunCommand.runCommand

文件路径:lib/src/commands/run.dart

@override
Future<FlutterCommandResult> runCommand() async {
  // Enable hot mode by default if `--no-hot` was not passed and we are in
  // debug mode.
  final BuildInfo buildInfo = await getBuildInfo();
  // 加载模式
  final bool hotMode = shouldUseHotMode(buildInfo);
  final String applicationBinaryPath = stringArg(FlutterOptions.kUseApplicationBinary);

  ...

  // 根据 hotMode 来创建 HotRunner 或 ColdRunner
  final ResidentRunner runner = await createRunner(
    applicationBinaryPath: applicationBinaryPath,
    flutterDevices: flutterDevices,
    flutterProject: flutterProject,
    hotMode: hotMode,
  );

  ...
  try {
    final int result = await runner.run(
      appStartedCompleter: appStartedTimeRecorder,
      enableDevTools: stayResident && boolArg(FlutterCommand.kEnableDevTools),
      route: route,
    );
    ...
  } on RPCError catch (error) {
    ...
  }
  return FlutterCommandResult(
    ExitStatus.success,
    timingLabelParts: <String>[
      if (hotMode) 'hot' else 'cold',
      getModeName(getBuildMode()),
      ...
    ],
    endTimeOverride: appStartedTime,
  );
}

这部分主要是根据 hotMode 来创建 HotRunnerColdRunner(忽略 web~),然后执行 HotRunnerrun 方法

@visibleForTesting
Future<ResidentRunner> createRunner({
  @required bool hotMode,
  @required List<FlutterDevice> flutterDevices,
  @required String applicationBinaryPath,
  @required FlutterProject flutterProject,
}) async {
  if (hotMode && !webMode) {
    return HotRunner(
      ...
    );
  } else if (webMode) {
    return webRunnerFactory.createWebRunner(
      ...
    );
  }
  return ColdRunner(
    ...
  );
}

hotMode 是怎么来的呢?我们点进 shouldUseHotMode 方法里看一下

ArgResults? get argResults => _argResults;
ArgResults? _argResults;
bool boolArgDeprecated(String name) => argResults?[name] as bool? ?? false;
bool get traceStartup => boolArgDeprecated('trace-startup');

bool shouldUseHotMode(BuildInfo buildInfo) {
  final bool hotArg = boolArgDeprecated('hot');
  final bool shouldUseHotMode = hotArg && !traceStartup;
  return buildInfo.isDebug && shouldUseHotMode;
}

hottrace-startup 是从 _argResults 取值,而 ArgResults 内部实现 [] 操作符,代码如下:

class ArgResults {
  ...
  dynamic operator [](String name) {
    if (!_parser.options.containsKey(name)) {
      throw ArgumentError('Could not find an option named "$name".');
    }

    return _parser.options[name]!.valueOrDefault(_parsed[name]);
  }
  ...
}

所以 hottrace-startup 是从 options 取值

RunCommand 在被实例化时就把 hottrace-startup 选项也添加进去了

const bool kHotReloadDefault = true;

...

abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopmentArtifacts {
  RunCommandBase({ required bool verboseHelp }) {
    ...
    argParser
      ..addFlag('trace-startup',
        negatable: false,
        help: 'Trace application startup, then exit, saving the trace to a file. '
              'By default, this will be saved in the "build" directory. If the '
              'FLUTTER_TEST_OUTPUTS_DIR environment variable is set, the file '
              'will be written there instead.',
      )
      ...
    }
}
...

class RunCommand extends RunCommandBase {
  RunCommand({ bool verboseHelp = false }) : super(verboseHelp: verboseHelp) {
      ...
      ..addFlag('hot',
        defaultsTo: kHotReloadDefault,
        help: 'Run with support for hot reloading. Only available for debug mode. Not available with "--trace-startup".',
      )
      ...
  }
  ...
}

addFlag 方法的代码如下:

void addFlag(String name,
    {String? abbr,
    String? help,
    bool? defaultsTo = false,
    bool negatable = true,
    void Function(bool)? callback,
    bool hide = false,
    List<String> aliases = const []}) {
  _addOption(...);
}
...
void _addOption(
    String name,
    String? abbr,
    String? help,
    String? valueHelp,
    Iterable<String>? allowed,
    Map<String, String>? allowedHelp,
    defaultsTo,
    Function? callback,
    OptionType type,
    {bool negatable = false,
    bool? splitCommas,
    bool mandatory = false,
    bool hide = false,
    List<String> aliases = const []}) {
  var allNames = [name, ...aliases];
  ...

  var option = newOption(...);
  _options[name] = option;
  _optionsAndSeparators.add(option);
  for (var alias in aliases) {
    _aliases[alias] = name;
  }
}

由此可见,调用 addFlag 方法其实是在往 _options 里添加 option,而 RunCommand 在初始化的时候就已经做了这个操作。

options_options 两者的关系:options_options 的不可修改引用,代码如下:

class ArgParser {
  final Map<String, Option> _options;

  /// The options that have been defined for this parser.
  final Map<String, Option> options;

  ...
  ArgParser._(Map<String, Option> options, Map<String, ArgParser> commands,
    this._aliases,
    {bool allowTrailingOptions = true, this.usageLineLength})
    : _options = options,
      options = UnmodifiableMapView(options),
      ...
}

所以说,对 options 取值其实就是对 _options 取值!

// Enable hot mode by default if `--no-hot` was not passed and we are in
// debug mode.

hot 选项默认为 true,如果你想将选项 hot 置为 false,可以使用选项 --no-hot,这是官方的 args 库提供的语法

再来看一下 buildInfo.isDebug

class BuildInfo {
  ...
  bool get isDebug => mode == BuildMode.debug;
  ...
}

总结:hotMode 的值由 hot 选项、trace-startup 选项和当前的编译模式共同决定。

HotRunner.run

文件路径:lib/src/run_hot.dart

HotRunnerrun 方法:

@override
Future<int> run({
  Completer<DebugConnectionInfo> connectionInfoCompleter,
  Completer<void> appStartedCompleter,
  bool enableDevTools = false,
  String route,
}) async {
  ...

  final List<Future<bool>> startupTasks = <Future<bool>>[];
  for (final FlutterDevice device in flutterDevices) {
    // 初始化前端编译器
    await runSourceGenerators();
    if (device.generator != null) {
      ...
      startupTasks.add(
        // 编译项目代码
        // outputPath: 编译产物路径,如:xx/xx/app.dill
        device.generator.recompile(
          mainFile.uri,
          <Uri>[],
          suppressErrors: applicationBinary == null,
          checkDartPluginRegistry: true,
          outputPath: dillOutputPath ??
            getDefaultApplicationKernelPath(
              trackWidgetCreation: debuggingOptions.buildInfo.trackWidgetCreation,
            ),
          packageConfig: debuggingOptions.buildInfo.packageConfig,
          projectRootPath: FlutterProject.current().directory.absolute.path,
          fs: globals.fs,
        ).then((CompilerOutput output) {
          compileTimer.stop();
          totalCompileTime += compileTimer.elapsed;
          return output?.errorCount == 0;
        })
      );
    }

    final Stopwatch launchAppTimer = Stopwatch()..start();
    startupTasks.add(device.runHot(
      hotRunner: this,
      route: route,
    ).then((int result) {
      totalLaunchAppTime += launchAppTimer.elapsed;
      return result == 0;
    }));
  }

  ...

  // 附加
  return attach(
    connectionInfoCompleter: connectionInfoCompleter,
    appStartedCompleter: appStartedCompleter,
    enableDevTools: enableDevTools,
  );
}

一共执行了两个任务:

  • 执行 device.generator.recompile,从项目中的 main.dart 开始进行编译,产出 app.dill 文件
  • 执行 device.runHot,编译运行 App

FlutterDevice.runHot

文件路径:lib/src/resident_runner.dart

Future<int> runHot({
  HotRunner hotRunner,
  String route,
}) async {
  ...
  
  // Start the application.
  final Future<LaunchResult> futureResult = device.startApp(
    package,
    mainPath: hotRunner.mainPath,
    debuggingOptions: hotRunner.debuggingOptions,
    platformArgs: platformArgs,
    route: route,
    prebuiltApplication: prebuiltMode,
    ipv6: hotRunner.ipv6,
    userIdentifier: userIdentifier,
  );
  
  final LaunchResult result = await futureResult;
  
  ...
  return 0;
}

这里的 device.runHotiOS 的为例,所以此时 device 的类型为 IOSDevice,找到该类的 startApp 方法

文件路径:lib/src/ios/devices.dart

@override
Future<LaunchResult> startApp(
  IOSApp package, {
  String? mainPath,
  String? route,
  required DebuggingOptions debuggingOptions,
  Map<String, Object?> platformArgs = const <String, Object?>{},
  bool prebuiltApplication = false,
  bool ipv6 = false,
  String? userIdentifier,
  @visibleForTesting Duration? discoveryTimeout,
}) async {
  String? packageId;

  if (!prebuiltApplication) {
    _logger.printTrace('Building ${package.name} for $id');

    // Step 1: 编译xcode项目
    final XcodeBuildResult buildResult = await buildXcodeProject(
        app: package as BuildableIOSApp,
        buildInfo: debuggingOptions.buildInfo,
        targetOverride: mainPath,
        activeArch: cpuArchitecture,
        deviceID: id,
    );
    ...
    packageId = buildResult.xcodeBuildExecution?.buildSettings['PRODUCT_BUNDLE_IDENTIFIER'];
  }

  packageId ??= package.id;

  // Step 2: 检查编译后的app文件是否存在
  final Directory bundle = _fileSystem.directory(package.deviceBundlePath);
  if (!bundle.existsSync()) {
    _logger.printError('Could not find the built application bundle at ${bundle.path}.');
    return LaunchResult.failed();
  }

  // Step 3: 尝试给设备安装app
  final String dartVmFlags = computeDartVmFlags(debuggingOptions);
  final List<String> launchArguments = <String>[
    '--enable-dart-profiling',
    ...
  ];

  final Status installStatus = _logger.startProgress(
    'Installing and launching...',
  );
  try {
    ProtocolDiscovery? observatoryDiscovery;
    int installationResult = 1;
    if (debuggingOptions.debuggingEnabled) {
      ...
      // 调试模式下开启observatory服务
      observatoryDiscovery = ProtocolDiscovery.observatory(
        deviceLogReader,
        portForwarder: portForwarder,
        hostPort: debuggingOptions.hostVmServicePort,
        devicePort: debuggingOptions.deviceVmServicePort,
        ipv6: ipv6,
        logger: _logger,
      );
    }
    // 通过ios-deploy安装App到设备(并附加)
    if (iosDeployDebugger == null) {
      installationResult = await _iosDeploy.launchApp(
        deviceId: id,
        bundlePath: bundle.path,
        appDeltaDirectory: package.appDeltaDirectory,
        launchArguments: launchArguments,
        interfaceType: interfaceType,
      );
    } else {
      installationResult = await iosDeployDebugger!.launchAndAttach() ? 0 : 1;
    }
    ...
    return LaunchResult.succeeded(observatoryUri: localUri);
  } on ProcessException catch (e) {
    await iosDeployDebugger?.stopAndDumpBacktrace();
    _logger.printError(e.message);
    return LaunchResult.failed();
  } finally {
    installStatus.stop();
  }
}

这部分的几个主要操作:

  1. 编译 Xcode 项目
  2. 检查编译后的 app 文件是否存在
  3. 调试模式下开启 observatory 服务
  4. 通过 ios-deploy 安装 App 到设备(并附加)

在第4点所对应的代码处,会调用 launchAndAttach 方法,即上一篇中 pr 主要修改的地方。

到此对 flutter run 命令的探索就结束了,相信大家对开头提出的疑问已经有了答案,感谢你的耐心观看。

如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有 iOS 技术,还有 AndroidFlutterPython 等文章, 可能有你想要了解的技能知识点哦~