初入Flutter的开发者,首先需要了解的便是如何编译运行flutter应用。与通常Android工程项目的编译不同,Flutter的打包编译是通过调用flutter命令行来实现的。
在一遍遍编译运行的过程中,你可能经常会思考:在每一条flutter命令的背后究竟做了哪些事?Flutter的编译是如何与传统Android gradle编译流程串联起来的?Dart代码如何编译成可执行的代码?
我们这就来揭示其背后的奥秘。
flutter build apk
通常,对于一个标准的Flutter工程,只要执行以下命令就可以完成打包,
flutter build apk
这里默认的属性是--release
,因此会默认打出release包。当然,如果你需要打debug包,可以这么操作:
flutter build apk --debug
首先,我们来看下flutter
命令具体是什么东西。
flutter
本体,在Flutter SDK目录的bin下面,也就是/path-to-flutter-sdk/flutter/bin/flutter
,它是一个命令行脚本,里面的核心在于这一行:
"$DART" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"
其中,各个参数的具体含义如下:
- $DART:Dart可执行文件,用于启动一个Dart虚拟机。
- $FLUTTER_TOOL_ARGS:用于Google自己调试Flutter SDK用,一般情况下是空的。
- $SNAPSHOT_PATH:指定一个用于运行的snapshot文件,这里是
flutter/bin/cache/flutter_tools.snapshot
。snapshot的含义相当于java中的jar文件。 - $@:会透传用户传入的参数,这里就是
build apk
。
因此,这里实际上执行的就是如下命令:
flutter/bin/cache/dart-sdk/bin/dart FLUTTER_TOOL_ARGS= SNAPSHOT_PATH=flutter/bin/cache/flutter_tools.snapshot build apk
可以看出,这个命令实际上和java执行jar文件的方式如出一辙,只不过,这里编译打包的逻辑,都是用Dart语言实现在flutter_tools.snapshot
中的,因此采用了Dart的执行环境。
flutter_tool
Dart的snapshot相当于Java的jar,因此,一个snapshot在执行的时候,必然有它的执行入口,也就是类似main
函数的东西。
这里的入口,正是flutter/packages/flutter_tools/bin/flutter_tools.dart
可以看到它的main
函数如下:
void main(List args) {
executable.main(args);
}
它调用了flutter/packages/flutter_tools/lib/executable.dart
的main
函数,我们继续往下看:
/// Main entry point for commands.
///
/// This function is intended to be used from the `flutter` command line tool.
Future main(List args) async {
final bool verbose = args.contains('-v') || args.contains('--verbose');
final bool doctor = (args.isNotEmpty && args.first == 'doctor') ||
(args.length == 2 && verbose && args.last == 'doctor');
final bool help = args.contains('-h') || args.contains('--help') ||
(args.isNotEmpty && args.first == 'help') || (args.length == 1 && verbose);
final bool muteCommandLogging = help || doctor;
final bool verboseHelp = help && verbose;
await runner.run(args, [
AnalyzeCommand(verboseHelp: verboseHelp),
AttachCommand(verboseHelp: verboseHelp),
BuildCommand(verboseHelp: verboseHelp), // 对应flutter build apk
ChannelCommand(verboseHelp: verboseHelp),
CleanCommand(),
ConfigCommand(verboseHelp: verboseHelp),
CreateCommand(),
DaemonCommand(hidden: !verboseHelp),
DevicesCommand(),
DoctorCommand(verbose: verbose),
DriveCommand(),
EmulatorsCommand(),
FormatCommand(),
IdeConfigCommand(hidden: !verboseHelp),
InjectPluginsCommand(hidden: !verboseHelp),
InstallCommand(),
LogsCommand(),
MakeHostAppEditableCommand(),
PackagesCommand(),
PrecacheCommand(),
RunCommand(verboseHelp: verboseHelp),
ScreenshotCommand(),
ShellCompletionCommand(),
StopCommand(),
TestCommand(verboseHelp: verboseHelp),
TraceCommand(),
UpdatePackagesCommand(hidden: !verboseHelp),
UpgradeCommand(),
], verbose: verbose,
muteCommandLogging: muteCommandLogging,
verboseHelp: verboseHelp);
}
这里的runner
是flutter
的一个命令行运行解析的通用类,在执行runner.run
的时候传入了一系列的XXXCommand
方法,我们只需要知道flutter build
对应的命令都会匹配到BuildCommand()
方法就可以了。
再来看BuildCommand的实现:
class BuildCommand extends FlutterCommand {
BuildCommand({bool verboseHelp = false}) {
addSubcommand(BuildApkCommand(verboseHelp: verboseHelp));
addSubcommand(BuildAotCommand());
addSubcommand(BuildIOSCommand());
addSubcommand(BuildFlxCommand());
addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp));
}
@override
final String name = 'build'; // flutter build
@override
final String description = 'Flutter build commands.';
@override
Future runCommand() async => null;
}
这里定义的一系列子命令,正是对应了我们运行flutter build -h
所看到的内容:
Flutter build commands.
Usage: flutter build [arguments]
-h, --help Print this usage information.
Available subcommands:
aot Build an ahead-of-time compiled snapshot of your app's Dart code.
apk Build an Android APK file from your app.
bundle Build the Flutter assets directory from your app.
flx Deprecated
ios Build an iOS application bundle (Mac OS X host only).
BuildCommand
继承了FlutterCommand
,而外部调用BuildCommand
的时候,执行的是这个父类FlutterCommand
的run
方法:
/// Runs this command.
///
/// Rather than overriding this method, subclasses should override
/// [verifyThenRunCommand] to perform any verification
/// and [runCommand] to execute the command
/// so that this method can record and report the overall time to analytics.
@override
Future run() {
final DateTime startTime = systemClock.now();
return context.run(
name: 'command',
overrides: {FlutterCommand: () => this},
body: () async {
... ...
try {
commandResult = await verifyThenRunCommand();
} on ToolExit {
... ...
它主要调用了verifyThenRunCommand
方法。
/// Perform validation then call [runCommand] to execute the command.
/// Return a [Future] that completes with an exit code
/// indicating whether execution was successful.
///
/// Subclasses should override this method to perform verification
/// then call this method to execute the command
/// rather than calling [runCommand] directly.
@mustCallSuper
Future verifyThenRunCommand() async {
await validateCommand();
// 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)
await cache.updateAll();
if (shouldRunPub) {
await pubGet(context: PubContext.getVerifyContext(name));
final FlutterProject project = await FlutterProject.current();
await project.ensureReadyForPlatformSpecificTooling();
}
setupApplicationPackages();
final String commandPath = await usagePath;
if (commandPath != null) {
final Map additionalUsageValues = await usageValues;
flutterUsage.sendCommand(commandPath, parameters: additionalUsageValues);
}
return await runCommand();
}
这个方法主要做了三件事:
首先,
pubGet
会做一些验证,主要是下载pubspec.yaml
里配置的依赖。实际上是通过Dart中的pub
指令来完成的,完整命令行如下:flutter/bin/cache/dart-sdk/bin/pub --verbosity=warning get --no-precompile
接着
ensureReadyForPlatformSpecificTooling
与setupApplicationPackages
会依据对应的平台设定好相应的编译环境,这里设定好Android的gradle环境,并且加上一些额外的gradle属性。最后,调用子类真正的
runCommand
的方法。
由于这里执行的是build apk
,所以子类是BuildApkCommand
,因此继续看BuildApkCommand
的runCommand
:
class BuildApkCommand extends BuildSubCommand {
... ...
@override
Future runCommand() async {
await super.runCommand();
await buildApk(
project: await FlutterProject.current(),
target: targetFile,
buildInfo: getBuildInfo(),
);
return null;
}
}
核心也就是这里的buildApk
:
Future buildApk({
@required FlutterProject project,
@required String target,
BuildInfo buildInfo = BuildInfo.debug
}) async {
if (!project.android.isUsingGradle) {
throwToolExit(
'The build process for Android has changed, and the current project configuration\n'
'is no longer valid. Please consult\n\n'
' https://github.com/flutter/flutter/wiki/Upgrading-Flutter-projects-to-build-with-gradle\n\n'
'for details on how to upgrade the project.'
);
}
// 检测ANDROID_HOME
// Validate that we can find an android sdk.
if (androidSdk == null)
throwToolExit('No Android SDK found. Try setting the ANDROID_HOME environment variable.');
final List validationResult = androidSdk.validateSdkWellFormed();
if (validationResult.isNotEmpty) {
for (String message in validationResult) {
printError(message, wrap: false);
}
throwToolExit('Try re-installing or updating your Android SDK.');
}
return buildGradleProject(
project: project,
buildInfo: buildInfo,
target: target,
);
}
这里要求Android工程必须使用gradle,否则直接退出。然后,检测ANDROID_HOME
是否存在,并调用buildGradleProject
。
buildGradleProject
主要做的是加入一些必要的参数,执行gradle命令,其最终执行的完整命令行如下:
flutter_hello/android/gradlew -q -Ptarget=lib/main.dart -Ptrack-widget-creation=false -Ptarget-platform=android-arm assembleRelease
由此,又回到了我们熟悉的Android世界,它只是在我们熟悉的gradlew assembleRelease
中增加了一些额外参数。flutter_hello
是我们的flutter示例工程。
这里为什么需要如此大费周折地绕个圈子来执行gradle命令呢?自然是因为Flutter本身的定位,它是一个跨平台方案,因此对于各个平台都有其特定的实现,因此才需要以一个统一的flutter build
入口来转换成各个平台实际所需的编译命令。
flutter.gradle
既然已经走到了gradlew,理所当然地,我们直接看他的build.gradle
就可以了。他的内容是由Flutter SDK生成的,相比于普通的Android工程,是有些许不同。不过我们只需要根据gradle的知识体系来理解就能贯通始终。
其与标准Android gradle工程的不同之处仅仅在于文件开头的部分:
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
这里主要是取得local.properties
中的一些Flutter相关属性,分别是flutter.sdk
、flutter.versionCode
和flutter.versionName
,他们指示了flutter sdk的路径,以及一些版本号信息。
接下来,便进入了flutter/packages/flutter_tools/gradle/flutter.gradle
中。
flutter.gradle
里面主要是实现了一个FlutterPlugin
,它是一个标准的gradle plugin,因此,它必然会定义一些task以及设定必要的依赖,addFlutterTask
方法中设置了这些依赖关系:
// in addFlutterTask
// We know that the flutter app is a subproject in another Android app when these tasks exist.
Task packageAssets = project.tasks.findByPath(":flutter:package${variant.name.capitalize()}Assets")
Task cleanPackageAssets = project.tasks.findByPath(":flutter:cleanPackage${variant.name.capitalize()}Assets")
Task copyFlutterAssetsTask = project.tasks.create(name: "copyFlutterAssets${variant.name.capitalize()}", type: Copy) {
dependsOn flutterTask
dependsOn packageAssets ? packageAssets : variant.mergeAssets
dependsOn cleanPackageAssets ? cleanPackageAssets : "clean${variant.mergeAssets.name.capitalize()}"
into packageAssets ? packageAssets.outputDir : variant.mergeAssets.outputDir
with flutterTask.assets
}
if (packageAssets) {
// Only include configurations that exist in parent project.
Task mergeAssets = project.tasks.findByPath(":app:merge${variant.name.capitalize()}Assets")
if (mergeAssets) {
mergeAssets.dependsOn(copyFlutterAssetsTask)
}
} else {
variant.outputs[0].processResources.dependsOn(copyFlutterAssetsTask)
}
processXXXResources
这个Task会依赖于copyFlutterAssetsTask
,这就会使得快执行到processXXXResources
的时候必须先执行完copyFlutterAssetsTask
才能继续。就这样,flutter的相关处理就嵌入到gradle的编译流程中了。
另外,copyFlutterAssetsTask
依赖了flutterTask
和mergeXXXAssets
。也就是说,当flutterTask
使得flutter编译完成,并且mergeXXXAssets
执行完毕,也就是正常Android的assets处理完成后,flutter相应的产物就会被copyFlutterAssetsTask
复制到build/app/intermediates/merged_assets/debug/mergeXXXAssets/out
目录下。这里的XXX
指代各种build variant,也就是Debug
或者Release
。
flutter的编译产物,具体是由flutterTask
的getAssets
方法指定的:
CopySpec getAssets() {
return project.copySpec {
from "${intermediateDir}"
include "flutter_assets/**" // the working dir and its files
if (buildMode == 'release' || buildMode == 'profile') {
if (buildSharedLibrary) {
include "app.so"
} else {
include "vm_snapshot_data"
include "vm_snapshot_instr"
include "isolate_snapshot_data"
include "isolate_snapshot_instr"
}
}
}
}
具体来说,这些产物就是build/app/intermediates/flutter/XXX
下面的flutter_assets/
目录中的所有内容。如果是release或者profile版本的话,还包含Dart的二进制产物app.so
或者***_snapshot_***
。可以看到,除了默认情况的***_snapshot_***
,我们还可以指定Dart产物为常规的so
库形式。
显然,flutter的编译过程就包含在flutterTask中,他的定义是这样的:
FlutterTask flutterTask = project.tasks.create(name: "${flutterBuildPrefix}${variant.name.capitalize()}", type: FlutterTask) {
flutterRoot this.flutterRoot
flutterExecutable this.flutterExecutable
buildMode flutterBuildMode
localEngine this.localEngine
localEngineSrcPath this.localEngineSrcPath
targetPath target
verbose verboseValue
fileSystemRoots fileSystemRootsValue
fileSystemScheme fileSystemSchemeValue
trackWidgetCreation trackWidgetCreationValue
compilationTraceFilePath compilationTraceFilePathValue
buildHotUpdate buildHotUpdateValue
buildSharedLibrary buildSharedLibraryValue
targetPlatform targetPlatformValue
sourceDir project.file(project.flutter.source)
intermediateDir project.file("${project.buildDir}/${AndroidProject.FD_INTERMEDIATES}/flutter/${variant.name}")
extraFrontEndOptions extraFrontEndOptionsValue
extraGenSnapshotOptions extraGenSnapshotOptionsValue
}
这里传入了FlutterTask
所需的各种参数,而具体的FlutterTask
核心只有一句话
class FlutterTask extends BaseFlutterTask {
... ...
@TaskAction
void build() {
buildBundle()
}
}
也就是调用了buildBundle
方法,我们先看它的前半部分:
void buildBundle() {
if (!sourceDir.isDirectory()) {
throw new GradleException("Invalid Flutter source directory: ${sourceDir}")
}
intermediateDir.mkdirs()
if (buildMode == "profile" || buildMode == "release") {
project.exec {
executable flutterExecutable.absolutePath
workingDir sourceDir
if (localEngine != null) {
args "--local-engine", localEngine
args "--local-engine-src-path", localEngineSrcPath
}
args "build", "aot"
args "--suppress-analytics"
args "--quiet"
args "--target", targetPath
args "--target-platform", "android-arm"
args "--output-dir", "${intermediateDir}"
if (trackWidgetCreation) {
args "--track-widget-creation"
}
if (extraFrontEndOptions != null) {
args "--extra-front-end-options", "${extraFrontEndOptions}"
}
if (extraGenSnapshotOptions != null) {
args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
}
if (buildSharedLibrary) {
args "--build-shared-library"
}
if (targetPlatform != null) {
args "--target-platform", "${targetPlatform}"
}
args "--${buildMode}"
}
}
... ...
}
这里,由于是release版本,因此会先编译aot的二进制Dart产物,也就是***_snapshot_***
产物,实际是执行以下命令:
flutter build aot --suppress-analytics --quiet --target lib/main.dart --target-platform android-arm --output-dir /path-to-project/build/app/intermediates/flutter/release --target-platform android-arm --release
接着,buildBundle方法的后半部分还会调用一次flutter命令:
void buildBundle() {
... ....
project.exec {
executable flutterExecutable.absolutePath
workingDir sourceDir
if (localEngine != null) {
args "--local-engine", localEngine
args "--local-engine-src-path", localEngineSrcPath
}
args "build", "bundle"
args "--suppress-analytics"
args "--target", targetPath
if (verbose) {
args "--verbose"
}
if (fileSystemRoots != null) {
for (root in fileSystemRoots) {
args "--filesystem-root", root
}
}
if (fileSystemScheme != null) {
args "--filesystem-scheme", fileSystemScheme
}
if (trackWidgetCreation) {
args "--track-widget-creation"
}
if (compilationTraceFilePath != null) {
args "--precompile", compilationTraceFilePath
}
if (buildHotUpdate) {
args "--hotupdate"
}
if (extraFrontEndOptions != null) {
args "--extra-front-end-options", "${extraFrontEndOptions}"
}
if (extraGenSnapshotOptions != null) {
args "--extra-gen-snapshot-options", "${extraGenSnapshotOptions}"
}
if (targetPlatform != null) {
args "--target-platform", "${targetPlatform}"
}
if (buildMode == "release" || buildMode == "profile") {
args "--precompiled"
} else {
args "--depfile", "${intermediateDir}/snapshot_blob.bin.d"
}
args "--asset-dir", "${intermediateDir}/flutter_assets"
if (buildMode == "debug") {
args "--debug"
}
if (buildMode == "profile" || buildMode == "dynamicProfile") {
args "--profile"
}
if (buildMode == "release" || buildMode == "dynamicRelease") {
args "--release"
}
if (buildMode == "dynamicProfile" || buildMode == "dynamicRelease") {
args "--dynamic"
}
}
}
也就是执行了
flutter build bundle --suppress-analytics --target lib/main.dart --target-platform android-arm --precompiled --asset-dir /Users/xl/WorkSpace/FlutterProjects/flutter_hello/build/app/intermediates/flutter/release/flutter_assets --release]
我们马上就来看下flutter build aot
和flutter build bundle
这两个命令的具体实现。
flutter build aot
回顾一下这个命令:
flutter build aot --suppress-analytics --quiet --target lib/main.dart --target-platform android-arm --output-dir /path-to-project/build/app/intermediates/flutter/release --target-platform android-arm --release
之前在分析flutter build apk的时候有提到,flutter build会通过flutter命令行脚本转化为启动一个Dart虚拟机并执行flutter_tool.snapshot,因此上述命令转化为:
flutter/bin/cache/dart-sdk/bin/dart FLUTTER_TOOL_ARGS= SNAPSHOT_PATH=flutter/bin/cache/flutter_tools.snapshot build aot --suppress-analytics --quiet --target lib/main.dart --target-platform android-arm --output-dir /path-to-project/flutter_hello/build/app/intermediates/flutter/release --target-platform android-arm --release
回顾一下之前讲解flutter_tools.snapshot提到的,BuildCommand里定义了一系列子命令:
class BuildCommand extends FlutterCommand {
BuildCommand({bool verboseHelp = false}) {
addSubcommand(BuildApkCommand(verboseHelp: verboseHelp));
addSubcommand(BuildAotCommand());
addSubcommand(BuildIOSCommand());
addSubcommand(BuildFlxCommand());
addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp));
}
... ...
这里对应的自然是BuildAotCommand
,直接来看它的runCommand
:
class BuildAotCommand extends BuildSubCommand {
... ...
Future runCommand() async {
... ...
String mainPath = findMainDartFile(targetFile);
final AOTSnapshotter snapshotter = AOTSnapshotter();
// Compile to kernel.
mainPath = await snapshotter.compileKernel(
platform: platform,
buildMode: buildMode,
mainPath: mainPath,
packagesPath: PackageMap.globalPackagesPath,
trackWidgetCreation: false,
outputPath: outputPath,
extraFrontEndOptions: argResults[FlutterOptions.kExtraFrontEndOptions],
);
... ...
// Build AOT snapshot.
if (platform == TargetPlatform.ios) {
... ...
} else {
// Android AOT snapshot.
final int snapshotExitCode = await snapshotter.build(
platform: platform,
buildMode: buildMode,
mainPath: mainPath,
packagesPath: PackageMap.globalPackagesPath,
outputPath: outputPath,
buildSharedLibrary: argResults['build-shared-library'],
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
);
if (snapshotExitCode != 0) {
status?.cancel();
throwToolExit('Snapshotting exited with non-zero exit code: $snapshotExitCode');
}
}
... ...
}
这里面其实又再次调用了两个Dart虚拟机命令,主要做了两件事:
- 生成kernel文件
- 生成AOT可执行文件
这两个产物都是Dart代码生成的程序文件。
kernel文件格式是由Dart定义的一种特殊数据格式,Dart虚拟机,会以解释方式来执行它。我们之前提到的flutter_tool.snapshot
等snapshot文件,实际上都是kernel文件。
而AOT可执行文件,需要等kernel文件生成完毕后,在编译期间根据kernel来生成的。它是二进制的,机器平台(arm、arm64、x86等)可执行的代码,也因此它被称为AOT(Ahead of time compiling)。在运行的时候它的指令码已经是平台的机器码了,因此不再需要Dart虚拟机解析,执行速度更快。release模式下,打包进APK的Dart源码都是以AOT文件的形式存在的。
生成kernel文件
我们先来分析第一步,也就是Compile to kernel
这一步。它会根据Dart代码文件生成kernel文件,具体是执行了以下命令:
flutter/bin/cache/dart-sdk/bin/dart /path-to-flutter-sdk/flutter/bin/cache/artifacts/engine/darwin-x64/frontend_server.dart.snapshot --sdk-root flutter/bin/cache/artifacts/engine/common/flutter_patched_sdk/ --strong --target=flutter --aot --tfa -Ddart.vm.product=true --packages .packages --output-dill /path-to-project/flutter_hello/build/app/intermediates/flutter/release/app.dill --depfile /path-to-project/flutter_hello/build/app/intermediates/flutter/release/kernel_compile.d package:flutter_hello/main.dart
可以看到,这里是通过Dart虚拟机启动了frontend_server.dart.snapshot
来把Dart代码文件编译成名为app.dill
的kernel文件。
而这个frontend_server.dart.snapshot
不同于之前的flutter_tool.snapshot
,他是存在于engine的Dart代码中的,而不是Flutter SDK中。
engine的代码需要单独下载:https://github.com/flutter/engine
frontend_server.dart.snapshot
的入口就存在于engine/frontend_server/bin/starter.dart
。经过一系列跳转后,最终执行的是compileToKernel
方法:
Future compileToKernel(Uri source, CompilerOptions options,
{bool aot: false,
bool useGlobalTypeFlowAnalysis: false,
Map environmentDefines,
bool genBytecode: false,
bool dropAST: false,
bool useFutureBytecodeFormat: false,
bool enableAsserts: false,
bool enableConstantEvaluation: true}) async {
... ...
final component = await kernelForProgram(source, options);
... ...
// Run global transformations only if component is correct.
if (aot && component != null) {
await _runGlobalTransformations(
source,
options,
component,
useGlobalTypeFlowAnalysis,
environmentDefines,
enableAsserts,
enableConstantEvaluation,
errorDetector);
}
if (genBytecode && !errorDetector.hasCompilationErrors && component != null) {
await runWithFrontEndCompilerContext(source, options, component, () {
generateBytecode(component,
dropAST: dropAST,
useFutureBytecodeFormat: useFutureBytecodeFormat,
environmentDefines: environmentDefines);
});
}
// Restore error handler (in case 'options' are reused).
options.onDiagnostic = errorDetector.previousErrorHandler;
return component;
}
这里主要执行的有三步:kernelForProgram
、_runGlobalTransformations
和runWithFrontEndCompilerContext
。
kernelForProgram
Future kernelForProgram(Uri source, CompilerOptions options) async {
var pOptions = new ProcessedOptions(options: options, inputs: [source]);
return await CompilerContext.runWithOptions(pOptions, (context) async {
var component = (await generateKernelInternal())?.component;
if (component == null) return null;
if (component.mainMethod == null) {
context.options.report(
messageMissingMain.withLocation(source, -1, noLength),
Severity.error);
return null;
}
return component;
});
}
我们可以先看下这个方法的注释:
/// Generates a kernel representation of the program whose main library is in
/// the given [source].
///
/// Intended for whole-program (non-modular) compilation.
///
/// Given the Uri of a file containing a program's `main` method, this function
/// follows `import`, `export`, and `part` declarations to discover the whole
/// program, and converts the result to Dart Kernel format.
///
/// If `compileSdk` in [options] is true, the generated component will include
/// code for the SDK.
///
/// If summaries are provided in [options], the compiler will use them instead
/// of compiling the libraries contained in those summaries. This is useful, for
/// example, when compiling for platforms that already embed those sources (like
/// the sdk in the standalone VM).
///
/// The input [source] is expected to be a script with a main method, otherwise
/// an error is reported.
注释已经说得比较清楚了。它会根据传入的包含main函数的Dart代码文件路径,并根据其中的import
, export
, 和part
来找到完整的程序所包含的所有Dart代码,最终把他们转换成kernel格式的文件。这一步最主要是生成一个Component对象,它会持有整个Dart程序的所有信息。
Component的定义如下
/// A way to bundle up libraries in a component.
class Component extends TreeNode {
final CanonicalName root;
final List libraries;
/// Map from a source file URI to a line-starts table and source code.
/// Given a source file URI and a offset in that file one can translate
/// it to a line:column position in that file.
final Map uriToSource;
/// Mapping between string tags and [MetadataRepository] corresponding to
/// those tags.
final Map> metadata =
>{};
/// Reference to the main method in one of the libraries.
Reference mainMethodName;
... ...
Component
其实只是一个包装类,其主要功能就是组织程序中所有Library,后续所有对kernel的处理都是围绕Componet
展开的。而Library
指的是app源文件、所有的package或者dart库以及第三方库,它们每个Library都各自包含了自己包下面的所有Class
、Field
、Procedure
等组成部分。
class Library extends NamedNode implements Comparable, FileUriNode {
/// An import path to this library.
///
/// The [Uri] should have the `dart`, `package`, `app`, or `file` scheme.
///
/// If the URI has the `app` scheme, it is relative to the application root.
Uri importUri;
/// The URI of the source file this library was loaded from.
Uri fileUri;
/// If true, the library is part of another build unit and its contents
/// are only partially loaded.
///
/// Classes of an external library are loaded at one of the [ClassLevel]s
/// other than [ClassLevel.Body]. Members in an external library have no
/// body, but have their typed interface present.
///
/// If the library is non-external, then its classes are at [ClassLevel.Body]
/// and all members are loaded.
bool isExternal;
String name;
@nocoq
final List annotations;
final List dependencies;
/// References to nodes exported by `export` declarations that:
/// - aren't ambiguous, or
/// - aren't hidden by local declarations.
@nocoq
final List additionalExports = [];
@informative
final List parts;
final List typedefs;
final List classes;
final List procedures;
final List fields;
由于Componet
只是一个包装类,因此主要通过拿到它,来处理其中的各个Library
对象。
generateKernelInternal
方法经过一连串调用会走到buildBody
方法,这个方法会对单个Library
进行处理。而外层会依次把Componet
中的各个Library
传入到这个方法。
Future buildBody(LibraryBuilder library) async {
if (library is SourceLibraryBuilder) {
// We tokenize source files twice to keep memory usage low. This is the
// second time, and the first time was in [buildOutline] above. So this
// time we suppress lexical errors.
Token tokens = await tokenize(library, suppressLexicalErrors: true);
if (tokens == null) return;
DietListener listener = createDietListener(library);
DietParser parser = new DietParser(listener);
parser.parseUnit(tokens);
for (SourceLibraryBuilder part in library.parts) {
if (part.partOfLibrary != library) {
// Part was included in multiple libraries. Skip it here.
continue;
}
Token tokens = await tokenize(part);
if (tokens != null) {
listener.uri = part.fileUri;
listener.partDirectiveIndex = 0;
parser.parseUnit(tokens);
}
}
}
}
buildBody
方法会对该Library
以及Library
中的part
部分分别做词法分析(tokenize)和语法分析(parse)。
tokenize
做的就是对Library
中的Dart源码,根据词法规则解析成一个个的词法单元tokens。
而parseUnit
就是把前面tokenize
得到的tokens根据Dart语法规则进一步解析成为抽象语法树。
由此,Dart源码已经被转化成了一颗抽象语法树并存储于Component
中了。
_runGlobalTransformations
而第二步的_runGlobalTransformations
主要是执行了一系列transformations操作:
Future _runGlobalTransformations(
Uri source,
CompilerOptions compilerOptions,
Component component,
bool useGlobalTypeFlowAnalysis,
Map environmentDefines,
bool enableAsserts,
bool enableConstantEvaluation,
ErrorDetector errorDetector) async {
if (errorDetector.hasCompilationErrors) return;
final coreTypes = new CoreTypes(component);
_patchVmConstants(coreTypes);
// TODO(alexmarkov, dmitryas): Consider doing canonicalization of identical
// mixin applications when creating mixin applications in frontend,
// so all backends (and all transformation passes from the very beginning)
// can benefit from mixin de-duplication.
// At least, in addition to VM/AOT case we should run this transformation
// when building a platform dill file for VM/JIT case.
mixin_deduplication.transformComponent(component);
if (enableConstantEvaluation) {
await _performConstantEvaluation(source, compilerOptions, component,
coreTypes, environmentDefines, enableAsserts);
if (errorDetector.hasCompilationErrors) return;
}
if (useGlobalTypeFlowAnalysis) {
globalTypeFlow.transformComponent(
compilerOptions.target, coreTypes, component);
} else {
devirtualization.transformComponent(coreTypes, component);
no_dynamic_invocations_annotator.transformComponent(component);
}
// We don't know yet whether gen_snapshot will want to do obfuscation, but if
// it does it will need the obfuscation prohibitions.
obfuscationProhibitions.transformComponent(component, coreTypes);
}
可以看到最后一行执行了混淆的transform。这里的混淆主要是做一些mapping,其实就类似于proguard做的一些事,不过目前看来,这方面的支持还比较初级,一些自定义规则之类的功能应该还不如proguard那么完善。
runWithFrontEndCompilerContext
runWithFrontEndCompilerContext
主要传入一个回调方法:
await runWithFrontEndCompilerContext(source, options, component, () {
generateBytecode(component,
dropAST: dropAST,
useFutureBytecodeFormat: useFutureBytecodeFormat,
environmentDefines: environmentDefines);
});
也就是这里的generateBytecode
,它需要拿到之前kernelForProgram
所生成的Component
对象,并根据其中的抽象语法树来生成具体的kernel字节码。
void generateBytecode(Component component,
{bool dropAST: false,
bool omitSourcePositions: false,
bool useFutureBytecodeFormat: false,
Map environmentDefines,
ErrorReporter errorReporter}) {
final coreTypes = new CoreTypes(component);
void ignoreAmbiguousSupertypes(Class cls, Supertype a, Supertype b) {}
final hierarchy = new ClassHierarchy(component,
onAmbiguousSupertypes: ignoreAmbiguousSupertypes);
final typeEnvironment =
new TypeEnvironment(coreTypes, hierarchy, strongMode: true);
final constantsBackend =
new VmConstantsBackend(environmentDefines, coreTypes);
final errorReporter = new ForwardConstantEvaluationErrors(typeEnvironment);
new BytecodeGenerator(
component,
coreTypes,
hierarchy,
typeEnvironment,
constantsBackend,
omitSourcePositions,
useFutureBytecodeFormat,
errorReporter)
.visitComponent(component);
if (dropAST) {
new DropAST().visitComponent(component);
}
}
这里主要就是调用了BytecodeGenerator.visitComponent
,visit这个Component
对象的时候,就会顺势访问到其中所有的Library
。BytecodeGenerator
这个类比较繁琐,他对于语法树中各个语法类型的情况分别有不同的处理,我们来大概浏览一下:
class BytecodeGenerator extends RecursiveVisitor {
... ...
@override
visitComponent(Component node) => node.visitChildren(this);
@override
visitLibrary(Library node) {
if (node.isExternal) {
return;
}
visitList(node.classes, this);
visitList(node.procedures, this);
visitList(node.fields, this);
}
@override
visitClass(Class node) {
visitList(node.constructors, this);
visitList(node.procedures, this);
visitList(node.fields, this);
}
这里的visitXXXX
系列方法会递归地访问其成员节点,由此完整遍历并处理好整颗语法树。
一个Library
中,包含了Constructor
、Procedure
、Field
等几个基本的组成单元,在以visitXXXX
访问它们的时候,最终都会调用defaultMember
方法。
@override
defaultMember(Member node) {
if (node.isAbstract) {
return;
}
try {
if (node is Field) {
if (node.isStatic && !_hasTrivialInitializer(node)) {
start(node);
if (node.isConst) {
_genPushConstExpr(node.initializer);
} else {
node.initializer.accept(this);
}
_genReturnTOS();
end(node);
}
} else if ((node is Procedure && !node.isRedirectingFactoryConstructor) ||
(node is Constructor)) {
start(node);
if (node is Constructor) {
_genConstructorInitializers(node);
}
if (node.isExternal) {
final String nativeName = getExternalName(node);
if (nativeName == null) {
return;
}
_genNativeCall(nativeName);
} else {
node.function?.body?.accept(this);
// BytecodeAssembler eliminates this bytecode if it is unreachable.
asm.emitPushNull();
}
_genReturnTOS();
end(node);
}
} on BytecodeLimitExceededException {
// Do not generate bytecode and fall back to using kernel AST.
hasErrors = true;
end(node);
}
}
这个方法会继续调用_genXXXX
系列方法,并且_genXXXX
方法之间也会互相调用。我们先来大致看下它们都是哪些方法:
183: void _genNativeCall(String nativeName) {
288: void _genConstructorInitializers(Constructor node) {
314: void _genFieldInitializer(Field field, Expression initializer) {
330: void _genArguments(Expression receiver, Arguments arguments) {
339: void _genPushBool(bool value) {
347: void _genPushInt(int value) {
370: void _genPushConstExpr(Expression expr) {
383: void _genReturnTOS() {
387: void _genStaticCall(Member target, ConstantArgDesc argDesc, int totalArgCount,
401: void _genStaticCallWithArgs(Member target, Arguments args,
424: void _genTypeArguments(List typeArgs, {Class instantiatingClass}) {
453: void _genPushInstantiatorAndFunctionTypeArguments(List types) {
469: void _genPushInstantiatorTypeArguments() {
556: void _genPushFunctionTypeArguments() {
564: void _genPushContextForVariable(VariableDeclaration variable,
578: void _genPushContextIfCaptured(VariableDeclaration variable) {
584: void _genLoadVar(VariableDeclaration v, {int currentContextLevel}) {
... ...
从名字很容易看出,他们的作用,正是用来生成代码结构的各个元素,包括赋值语句、返回语句、判断语句等等。
而_genXXXX
方法还不是最终执行者,真正的幕后英雄,是asm.emitXXXX
系列函数。
就以bool
赋值语句_genPushBool
为例:
void _genPushBool(bool value) {
if (value) {
asm.emitPushTrue();
} else {
asm.emitPushFalse();
}
}
它分别根据value的不同,调用了asm.emitPushTrue和asm.emitPushFalse。就以设置true值为例:
void emitPushTrue() {
emitWord(_encode0(Opcode.kPushTrue));
}
int _encode0(Opcode opcode) => _uint8(opcode.index);
void emitWord(int word) {
if (isUnreachable) {
return;
}
_encodeBufferIn[0] = word;
bytecode.addAll(_encodeBufferOut);
}
Opcode.kPushTrue
表示一个push true的字节码值,emitPushTrue
会将其转为一个int大小的字节码,并写入到bytecode
之中
Dart定义了一系列的字节码指令集,
enum Opcode {
kTrap,
// Prologue and stack management.
kEntry,
kEntryFixed,
kEntryOptional,
kLoadConstant,
kFrame,
kCheckFunctionTypeArgs,
kCheckStack,
// Object allocation.
kAllocate,
kAllocateT,
kCreateArrayTOS,
// Context allocation and access.
kAllocateContext,
kCloneContext,
kLoadContextParent,
kStoreContextParent,
kLoadContextVar,
kStoreContextVar,
// Constants.
kPushConstant,
kPushNull,
kPushTrue,
kPushFalse,
kPushInt,
// Locals and expression stack.
kDrop1,
kPush,
kPopLocal,
kStoreLocal,
... ...
// Int operations.
kNegateInt,
kAddInt,
kSubInt,
kMulInt,
kTruncDivInt,
kModInt,
kBitAndInt,
kBitOrInt,
kBitXorInt,
kShlInt,
kShrInt,
kCompareIntEq,
kCompareIntGt,
kCompareIntLt,
kCompareIntGe,
kCompareIntLe,
}
语法树的各个部分将被翻译为上面的不同指令,最终,整个语法树完整解析为二进制指令流,并存放于BytecodeAssembler
的bytecode
成员中。
class BytecodeAssembler {
... ...
final List bytecode = new List();
... ...
void emitWord(int word) {
if (isUnreachable) {
return;
}
_encodeBufferIn[0] = word;
bytecode.addAll(_encodeBufferOut); // 都被add进bytecode
}
... ...
至此,Dart代码已经被解析为kernel格式的指令流,接下来,我们来看下它是如何被写进文件的。
写kernel文件
我们先来回顾一下之前的generateBytecode
,它是一个全局函数,在它其中new
了一个BytecodeGenerator
,并通过它的visitComponent
方法来解析语法树,并且生成二进制指令流到BytecodeAssembler
的bytecode
成员中,这个BytecodeAssembler
对应的是BytecodeGenerator
的成员字段asm
,主要代码如下:
void generateBytecode(Component component,
{bool dropAST: false,
bool omitSourcePositions: false,
bool useFutureBytecodeFormat: false,
Map environmentDefines,
ErrorReporter errorReporter}) {
... ...
new BytecodeGenerator(
component,
coreTypes,
hierarchy,
typeEnvironment,
constantsBackend,
omitSourcePositions,
useFutureBytecodeFormat,
errorReporter)
.visitComponent(component);
... ...
}
// BytecodeGenerator的成员字段asm是BytecodeAssembler
class BytecodeGenerator extends RecursiveVisitor {
... ...
BytecodeAssembler asm;
... ...
}
// BytecodeAssembler的bytecode中存放所有二进制指令流
class BytecodeAssembler {
... ...
final List bytecode = new List();
... ...
}
而在用visitComponent
遍历语法树的时候,记得我们调用的一些列visitXXXX
都会走到defaultMember
,我们再来看一下defaultMember
。
@override
defaultMember(Member node) {
if (node.isAbstract) {
return;
}
try {
if (node is Field) {
if (node.isStatic && !_hasTrivialInitializer(node)) {
start(node);
if (node.isConst) {
_genPushConstExpr(node.initializer);
} else {
node.initializer.accept(this);
}
_genReturnTOS();
end(node);
}
} else if ((node is Procedure && !node.isRedirectingFactoryConstructor) ||
(node is Constructor)) {
start(node);
if (node is Constructor) {
_genConstructorInitializers(node);
}
if (node.isExternal) {
final String nativeName = getExternalName(node);
if (nativeName == null) {
return;
}
_genNativeCall(nativeName);
} else {
node.function?.body?.accept(this);
// BytecodeAssembler eliminates this bytecode if it is unreachable.
asm.emitPushNull();
}
_genReturnTOS();
end(node);
}
} on BytecodeLimitExceededException {
// Do not generate bytecode and fall back to using kernel AST.
hasErrors = true;
end(node);
}
}
这里注意到,所有分支最终都会调用end(node)
,
void end(Member node) {
if (!hasErrors) {
final formatVersion = useFutureBytecodeFormat
? futureBytecodeFormatVersion
: stableBytecodeFormatVersion;
metadata.mapping[node] = new BytecodeMetadata(formatVersion, cp,
asm.bytecode, asm.exceptionsTable, nullableFields, closures);
}
... ...
}
可以看到,在end
函数中,asm.bytecode
被转移到了metadata
,而这个metadata
在构造方法的时候就已经被加进了component
中:
BytecodeGenerator(
this.component,
this.coreTypes,
this.hierarchy,
this.typeEnvironment,
this.constantsBackend,
this.omitSourcePositions,
this.useFutureBytecodeFormat,
this.errorReporter)
: recognizedMethods = new RecognizedMethods(typeEnvironment) {
component.addMetadataRepository(metadata);
}
再回到最开始,compileToKernel
是由compile
调用的,它被传入了一个component
对象,并且最后通过writeDillFile
方法来写到文件中。
@override
Future compile(
String filename,
ArgResults options, {
IncrementalCompiler generator,
}) async {
... ...
Component component;
... ...
component = await _runWithPrintRedirection(() => compileToKernel(
_mainSource, compilerOptions,
aot: options['aot'],
useGlobalTypeFlowAnalysis: options['tfa'],
environmentDefines: environmentDefines));
... ...
await writeDillFile(component, _kernelBinaryFilename,
filterExternal: importDill != null);
... ...
}
writeDillFile的实现如下:
writeDillFile(Component component, String filename,
{bool filterExternal: false}) async {
final IOSink sink = new File(filename).openWrite();
final BinaryPrinter printer = filterExternal
? new LimitedBinaryPrinter(
sink, (lib) => !lib.isExternal, true /* excludeUriToSource */)
: printerFactory.newBinaryPrinter(sink);
component.libraries.sort((Library l1, Library l2) {
return "${l1.fileUri}".compareTo("${l2.fileUri}");
});
component.computeCanonicalNames();
for (Library library in component.libraries) {
library.additionalExports.sort((Reference r1, Reference r2) {
return "${r1.canonicalName}".compareTo("${r2.canonicalName}");
});
}
if (unsafePackageSerialization == true) {
writePackagesToSinkAndTrimComponent(component, sink);
}
printer.writeComponentFile(component);
await sink.close();
}
filename
就是前面通过命令行参数传入的/path-to-project/flutter_hello/build/app/intermediates/flutter/release/app.dill
。这里我们主要关注writeComponentFile
。
void writeComponentFile(Component component) {
computeCanonicalNames(component);
final componentOffset = getBufferOffset();
writeUInt32(Tag.ComponentFile);
writeUInt32(Tag.BinaryFormatVersion);
indexLinkTable(component);
indexUris(component);
_collectMetadata(component);
if (_metadataSubsections != null) {
_writeNodeMetadataImpl(component, componentOffset);
}
libraryOffsets = [];
CanonicalName main = getCanonicalNameOfMember(component.mainMethod);
if (main != null) {
checkCanonicalName(main);
}
writeLibraries(component);
writeUriToSource(component.uriToSource);
writeLinkTable(component);
_writeMetadataSection(component);
writeStringTable(stringIndexer);
writeConstantTable(_constantIndexer);
writeComponentIndex(component, component.libraries);
_flush();
}
这其中写入了很多部分,我们就不一一分析了,这里我们主要看之前编译完成后存到BytecodeMetadata
里的asm.bytecode
的数据是如何在这里被写入到文件的。这个逻辑,主要在_writeNodeMetadataImpl
中。
void _writeNodeMetadataImpl(Node node, int nodeOffset) {
for (var subsection in _metadataSubsections) {
final repository = subsection.repository;
final value = repository.mapping[node];
if (value == null) {
continue;
}
if (!MetadataRepository.isSupported(node)) {
throw "Nodes of type ${node.runtimeType} can't have metadata.";
}
if (!identical(_sink, _mainSink)) {
throw "Node written into metadata can't have metadata "
"(metadata: ${repository.tag}, node: ${node.runtimeType} $node)";
}
_sink = _metadataSink;
subsection.metadataMapping.add(nodeOffset);
subsection.metadataMapping.add(getBufferOffset());
repository.writeToBinary(value, node, this);
_sink = _mainSink;
}
}
repository.writeToBinary
对应的是BytecodeMetadataRepository.writeToBinary
void writeToBinary(BytecodeMetadata metadata, Node node, BinarySink sink) {
sink.writeUInt30(metadata.version);
sink.writeUInt30(metadata.flags);
metadata.constantPool.writeToBinary(node, sink);
sink.writeByteList(metadata.bytecodes); // 这里bytecodes被写进了文件中
if (metadata.hasExceptionsTable) {
metadata.exceptionsTable.writeToBinary(sink);
}
if (metadata.hasNullableFields) {
sink.writeUInt30(metadata.nullableFields.length);
metadata.nullableFields.forEach((ref) => sink
.writeCanonicalNameReference(getCanonicalNameOfMember(ref.asField)));
}
if (metadata.hasClosures) {
sink.writeUInt30(metadata.closures.length);
metadata.closures.forEach((c) => c.writeToBinary(sink));
}
}
代码还是比较清晰的,就是写入了metadata的各个数据,sink.writeByteList(metadata.bytecodes);
完成了bytecodes
的写入,此外包括version
、flags
等等信息也一同写入了文件。sink
在这里指的是kernel二进制文件写入者,是一个BinaryPrinter
类,绑定一个具体文件,也就是app.dill
。
至此,kernel文件的编译和生成流程就走完了。
生成AOT可执行文件
现在,就可以根据上一步得到的app.dill
来生成AOT可执行文件了。回顾一下gradle脚本中调用命令行生成AOT的代码:
final int snapshotExitCode = await snapshotter.build(
platform: platform,
buildMode: buildMode,
mainPath: mainPath,
packagesPath: PackageMap.globalPackagesPath,
outputPath: outputPath,
buildSharedLibrary: argResults['build-shared-library'],
extraGenSnapshotOptions: argResults[FlutterOptions.kExtraGenSnapshotOptions],
);
实际对应的命令是
flutter/bin/cache/artifacts/engine/android-arm-release/darwin-x64/gen_snapshot
--causal_async_stacks
--packages=.packages
--deterministic
--snapshot_kind=app-aot-blobs
--vm_snapshot_data=/path-to-project/flutter_hello/build/app/intermediates/flutter/release/vm_snapshot_data
--isolate_snapshot_data=/path-to-project/flutter_hello/build/app/intermediates/flutter/release/isolate_snapshot_data
--vm_snapshot_instructions=/path-to-project/flutter_hello/build/app/intermediates/flutter/release/vm_snapshot_instr
--isolate_snapshot_instructions=/path-to-project/flutter_hello/build/app/intermediates/flutter/release/isolate_snapshot_instr
--no-sim-use-hardfp
--no-use-integer-division /path-to-project/flutter_hello/build/app/intermediates/flutter/release/app.dill
到这里,gen_snapshot不再是之前常见的Dart命令,而是一个货真价实的Native二进制可执行文件。其对应的是Dart虚拟机中的C++代码:dart/runtime/bin/gen_snapshot.cc
,入口在main函数中:
int main(int argc, char** argv) {
... ...
error = Dart_Initialize(&init_params);
... ...
return GenerateSnapshotFromKernel(kernel_buffer, kernel_buffer_size);
... ...
}
这里主要做了两件事。首先,根据传入的参数,初始化出一个Dart运行环境,主要是加载kernel文件,把所有Dart类都加载到运行环境中。接着,会根据已有的运行环境,直接编译生成二进制可执行文件snapshot。
我们重点来看后面GenerateSnapshotFromKernel
这步。
gen_snapshot中定义了许多snapshot的种类:
static const char* kSnapshotKindNames[] = {
"core",
"core-jit",
"core-jit-all",
"app-jit",
"app-aot-blobs",
"app-aot-assembly",
"vm-aot-assembly", NULL,
};
对应枚举类型分别为:
// Global state that indicates whether a snapshot is to be created and
// if so which file to write the snapshot into. The ordering of this list must
// match kSnapshotKindNames below.
enum SnapshotKind {
kCore,
kCoreJIT,
kCoreJITAll,
kAppJIT,
kAppAOTBlobs,
kAppAOTAssembly,
kVMAOTAssembly,
};
而我们的命令行参数传入的是--snapshot_kind=app-aot-blobs
,因此这里只需要看kAppAOTBlobs
类型的就可以了。
static int GenerateSnapshotFromKernel(const uint8_t* kernel_buffer,
intptr_t kernel_buffer_size) {
switch (snapshot_kind) {
... ...
case kAppAOTBlobs:
case kAppAOTAssembly: {
if (Dart_IsNull(Dart_RootLibrary())) {
Log::PrintErr(
"Unable to load root library from the input dill file.\n");
return kErrorExitCode;
}
CreateAndWritePrecompiledSnapshot();
CreateAndWriteDependenciesFile();
break;
}
... ...
主要逻辑在于CreateAndWritePrecompiledSnapshot中
static void CreateAndWritePrecompiledSnapshot() {
... ...
result = Dart_Precompile();
... ...
result = Dart_CreateAppAOTSnapshotAsBlobs(
&vm_snapshot_data_buffer, &vm_snapshot_data_size,
&vm_snapshot_instructions_buffer, &vm_snapshot_instructions_size,
&isolate_snapshot_data_buffer, &isolate_snapshot_data_size,
&isolate_snapshot_instructions_buffer,
&isolate_snapshot_instructions_size, shared_data, shared_instructions);
... ...
WriteFile(vm_snapshot_data_filename, vm_snapshot_data_buffer,
vm_snapshot_data_size);
WriteFile(vm_snapshot_instructions_filename,
vm_snapshot_instructions_buffer, vm_snapshot_instructions_size);
WriteFile(isolate_snapshot_data_filename, isolate_snapshot_data_buffer,
isolate_snapshot_data_size);
WriteFile(isolate_snapshot_instructions_filename,
isolate_snapshot_instructions_buffer,
isolate_snapshot_instructions_size);
... ...
}
一共分为三步。
- Dart_Precompile进行AOT编译
- 把snapshot代码转移到buffer中
- 写buffer到四个二进制文件
重点在于第一步,Dart_Precompile
调用的是Precompiler::CompileAll()
来实现编译的,具体细节比较复杂,大概说来,它会先根据前面Dart_Initialize
得到的Dart运行环境的数据生成FlowGraph
对象,再进行各种执行流图的优化,最后把优化后的FlowGraph
对象翻译为具体架构(arm/arm64/x86等)的二进制指令。
而后面两步就是把内存中的二进制数据最终落地到文件中,也就是isolate_snapshot_data、isolate_snapshot_instr、vm_snapshot_data、vm_snapshot_instr这四个文件。
至此,flutter build aot
执行完毕,Dart代码完全编译成了二进制可执行文件。
flutter build bundle
回顾一下这个命令:
flutter build bundle --suppress-analytics --target lib/main.dart --target-platform android-arm --precompiled --asset-dir /path-to-project/flutter_hello/build/app/intermediates/flutter/release/flutter_assets --release
之前在分析flutter build apk的时候有提到,flutter build会通过flutter命令行脚本转化为启动一个Dart虚拟机并执行flutter_tool.snapshot,因此上述命令转化为:
flutter/bin/cache/dart-sdk/bin/dart
FLUTTER_TOOL_ARGS=
SNAPSHOT_PATH=flutter/bin/cache/flutter_tools.snapshot
build bundle
--suppress-analytics
--target lib/main.dart
--target-platform android-arm
--precompiled
--asset-dir /path-to-project/flutter_hello/build/app/intermediates/flutter/release/flutter_assets
--release
build bundle
对应BuildBundleCommand
,由于是relase模式,参数中会带上--precompiled
,因此不会在这里编译kernel文件了。其最终执行的是以下代码:
Future writeBundle(
Directory bundleDir, Map assetEntries) async {
if (bundleDir.existsSync())
bundleDir.deleteSync(recursive: true);
bundleDir.createSync(recursive: true);
await Future.wait(
assetEntries.entries.map>((MapEntry entry) async {
final File file = fs.file(fs.path.join(bundleDir.path, entry.key));
file.parent.createSync(recursive: true);
await file.writeAsBytes(await entry.value.contentsAsBytes());
}));
}
实际上只是把一些文件放进了build/app/intermediates/flutter/release/flutter_assets
目录下,这些文件分别是:
packages/cupertino_icons/assets/CupertinoIcons.ttf
fonts/MaterialIcons-Regular.ttf
AssetManifest.json
FontManifest.json
LICENSE
因此,当build bundle
执行完毕后,所有flutter所需要的文件都已经放入flutter_assets
中了。
我们前面在讲flutter.gradle
的时候提到。build/app/intermediates/flutter/release/flutter_assets
里的东西会被全部复制到build/app/intermediates/merged_assets/debug/mergeXXXAssets/out
下。这样,这些flutter文件会在最后,一起跟着Android的标准taskmergeXXXAssets
打入到APK中。
总结
到这里,flutter编译release包的完整流程就全部分析完了。我们以一张图再来归纳一下整个编译流程:
而如果是执行的编译debug包的操作flutter build apk --debug
,它的流程是这样的:
当然,其中有些命令行的具体参数是有所不同的。总体而言,debug模式下没有了build aot
这一步,而编译kernel文件这一步,也由release版本下的build aot
中转移到了build bundle
中。
本文完整讲解了Android环境下Flutter编译apk的流程,但这其中还有很多细节没有完全展开,包括Dart的pub机制、抽象语法树的构建、机器码编译等等,如果每一点都要分析清楚也都是长篇大论。
可以说,flutter作为一门新技术,有太多值得去品味与探索的实现细节,并且在阅读代码的过程中我们也发现,一些代码实现目前也没有十分稳定,官方也在不断优化中。我们通过对其深层原理的学习,不仅可以学以致用,实现特定需求的改进,还可以共同改进与推进这门新技术,使得移动开发技术领域的环境更加多样和完善。