本文主要介绍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环境准备的步骤大多是一些准备工作,简单说明下,大致如下:
- BIN_DIR相关环境变量准备(flutter/bin/flutter#follow_links作为入口寻找flutter脚本的绝对路径,具体由shared.sh脚本完成。)
- 运行环境检查(flutter_tools.snapshot,flutter_tools.stamp检查等,这两个文件会在flutter sdk升级时介绍)
- flutter tool依赖下载、构建、缓存和更新逻辑。
- 真正的逻辑执行阶段
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的更新步骤大致分为四个步骤,如下:
- 判断是否需要更新,判断条件如下
-
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文件新)。
- 第二阶段是加锁,防止多个flutter命令执行导致Flutter SDK并行更新。(flutter tool选择了一次只能有一个shell进程对Flutter SDK进行更新)
- 真正的执行更新逻辑
- 构建阶段(flutter tool编译出Snapshot产物,上面也提到该Snapshot是为了提高flutter的响应速度)
简单概括更新的过程就是:准备环境变量,检查curl、unzip命令是否存在,然后拼接一个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.dart
的main
方法。
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方法主要做了以下工作:
-
部分参数的解析与处理(上述代码略过的部分)
-
Cache.flutterRoot对象初始化(上述代码也略过了,暂时不会用到)
-
调用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执行的实际逻辑。
主题逻辑代码调用顺序如图:
最终调用的是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 目录