Flutter - 探索run命令到底做了什么 🤔
# 一、背景
上一篇 Flutter - 我给官方提PR,解决run命令卡住问题 😃 中,虽然我提的 pr
最终成功 merge
了,但是我心中还存在着几点疑问,如:
- 只能删除
snapshot
文件才能使修改的flutter_tools
代码生效吗? - 终端内执行
flutter
命令后的流程是怎样的?
现在就让我们带着疑问一起来了解一下从 flutter run
到 flutter_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
:去掉文件名前面的目录并打印出来DART
:dart
可执行文件的文件路径FLUTTER_TOOLS_DIR
:flutter_tools
目录路径FLUTTER_TOOL_ARGS
:单独的空格分隔SNAPSHOT_PATH
:flutter_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
方法,其主要是以下几个动作:
- 创建
bin/cache
目录 - 拉取
dart-sdk
- 创建或更新
flutter_tools.snapshot
为了确保 flutter_tools.snapshot
是存在的且为 "最新",其中判断是否需要对缓存进行更新有几个点:
- 没有
flutter_tools.snapshot
文件, 或者 flutter_tools.stamp
文件不存在, 或者flutter_tools.stamp
文件内容为空, 或者flutter_tools.stamp
文件内的SHA1
与当前HEAD
的SHA1
不同, 或者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.dart
为 flutter_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
实例 runner
的 run
方法。
我们先来看一下 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;
}
以命令的名字和别名分别做为 key
,command
做为 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.');
...
}
final String name = 'config';
...
final List<String> aliases = <String>['configure'];
...
}
回来继续看 FlutterCommandRunner
实例 runner
的 run
方法
# FlutterCommandRunner.run
文件路径:lib/src/runner/flutter_command_runner.dart
Future<void> run(Iterable<String> args) {
...
return super.run(args);
}
FlutterCommandRunner
的 run
方法里主要还是调用了super.run
,super.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
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);
},
);
}
注意:这里的 runCommand
是 FlutterCommandRunner
内重写了父类的 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()
调用的就是 FlutterCommand
的 run
方法。
# 2、命令执行
# FlutterCommand.run
文件路径:lib/src/runner/flutter_command.dart
Future<void> run() {
...
return context.run<void>(
name: 'command',
overrides: <Type, Generator>{FlutterCommand: () => this},
body: () async {
...
try {
commandResult = await verifyThenRunCommand(commandPath);
} finally {
...
}
},
);
}
verifyThenRunCommand
方法进行校验并真正开始执行相应的命令
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();
}
这个方法主要做了以下几点:
- 更新
flutter/bin/cache
下的artifacts
- 校验当前目录下是否有
pubspec.yaml
文件,没有就往父路径里找,并修正项目路径 - 执行
pub get
下载项目依赖 runCommand()
执行的是RunCommand
下的runCommand
注:从这个地方开始才真正去执行 run
命令!
# RunCommand.runCommand
文件路径:lib/src/commands/run.dart
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
来创建 HotRunner
或 ColdRunner
(忽略 web
~),然后执行 HotRunner
的 run
方法
Future<ResidentRunner> createRunner({
bool hotMode,
List<FlutterDevice> flutterDevices,
String applicationBinaryPath,
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;
}
hot
和 trace-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]);
}
...
}
所以 hot
和 trace-startup
是从 options
取值
而 RunCommand
在被实例化时就把 hot
和 trace-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 (opens new window) 库提供的语法
再来看一下 buildInfo.isDebug
class BuildInfo {
...
bool get isDebug => mode == BuildMode.debug;
...
}
总结:hotMode
的值由 hot
选项、trace-startup
选项和当前的编译模式共同决定。
# HotRunner.run
文件路径:lib/src/run_hot.dart
HotRunner
的 run
方法:
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.runHot
以 iOS
的为例,所以此时 device
的类型为 IOSDevice
,找到该类的 startApp
方法
文件路径:lib/src/ios/devices.dart
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,
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();
}
}
这部分的几个主要操作:
- 编译
Xcode
项目 - 检查编译后的
app
文件是否存在 - 调试模式下开启
observatory
服务 - 通过
ios-deploy
安装App
到设备(并附加)
在第4点所对应的代码处,会调用 launchAndAttach
方法,即上一篇中 pr
主要修改的地方。
到此对 flutter run
命令的探索就结束了,相信大家对开头提出的疑问已经有了答案,感谢你的耐心观看。
- 01
- Flutter - 危!3.24版本苹果审核被拒!11-13
- 02
- Flutter - 轻松搞定炫酷视差(Parallax)效果09-21
- 03
- Flutter - 轻松实现PageView卡片偏移效果09-08