写这篇文章的时间为:2019.08.25,当前 Flutter SDK 最新版本为 v1.9.5,其中 stable 最新版本为 v1.7.8+hotfix.4。python
随着版本的更新,Flutter 构建的产物也在调整,以 Android 为例,咱们使用默认的 flutter_app 项目来测试,在不修改源码的状况下,使用不一样的 SDK 版原本执行打包命令,每一个版本都打两个包:debug 和 release。android
每一个版本生成的 Flutter 产物以下所示,这里不列出 fonts、LICENSE 这些文件:ios
版本 | 类型 | 内容 | 说明 |
---|---|---|---|
1.5.4 | debug | assets/flutter_assets/kernel_blob.bin assets/flutter_assets/isolate_snapshot_data assets/flutter_assets/vm_snapshot_data lib/x86_64/libflutter.so lib/x86/libflutter.so lib/armeabi-v7a/libflutter.so |
|
1.5.4 | release | assets/isolate_snapshot_data assets/isolate_snapshot_instr assets/vm_snapshot_data assets/vm_snapshot_instr lib/armeabi-v7a/libflutter.so |
|
1.6.7 | debug | 新增了 assets/snapshot_blob.bind.d.fingerprint assets/snapshot_blob.bin.d |
|
1.6.7 | release | 无变化 | |
1.7.8 | debug | 删除了 assets/snapshot_blob.bind.d.fingerprint assets/snapshot_blob.bin.d 新增了 lib/arm64-v8a/libflutter.so |
|
1.7.8 | release | 删除了 assets/isolate_snapshot_data assets/isolate_snapshot_instr assets/vm_snapshot_data assets/vm_snapshot_instr 新增了 lib/armeabi-v7a/libapp.so lib/arm64-v8a/libflutter.so lib/arm64-v8a/libapp.so |
从这个版本开始,release 下不使用 snapshot,同时增长了同时打包 64 位 so 文件 |
1.8.4 | debug | 无变化 | |
1.8.4 | release | 无变化 | |
1.9.5 | debug | 无变化 | |
1.9.5 | release | 无变化 |
从上面的表格来看,1.5.x 版本和 1.7.x 版本的变化是比较明显的,主要的变化有两点,第一,1.7.x 版本增长了支持同时编译 32 位和 64 位两种架构,这个在以前一直被诟病,如今官方支持了。第二,release 模式下,不使用 snapshot 文件,而使用 libapp.so。snapshot 是 Dart VM 所支持的一种文件格式,相似 JVM 的 jar 同样,能够运行在虚拟机环境的文件。git
除了变化之外,咱们还注意到一些不变的文件,好比 debug 模式下,一直存在的 kernel_blob.bin 文件;lib 目录下 libflutter.so 文件;这些文件各自的做用又是什么呢。带着疑问,咱们一块儿看看 Flutter 的构建过程。github
关于编译模式这块主要参考的:shell
Flutter 是基于 Dart 开发的,因此 Flutter 的构建跟 Dart 是分不开的。因此,咱们先讲 Dart。json
在将 Dart 以前,咱们先了解两种编译模式,JIT 和 AOT:api
JIT(Just In Time) 翻译为 即时编译,指的是在程序运行中,将热点代码编译成机器码,提升运行效率。常见例子有 V8 引擎和 JVM,JIT 能够充分利用解释型语言的优势,动态执行源码,而不用考虑平台差别性。这里须要注意的是,对于 JVM 来讲,源码指字节码,而不是 Java 源码。性能优化
这里须要区分,JIT 和解释型语言的区别,JIT 是一种编译模式,好比,Java 是编译型语言,但它也可使用 JIT。bash
AOT(Ahead Of Time) 称为 运行前编译,指的是在程序运行以前,已经编译成对应平台的机器码,不须要在运行中解释编译,就能够直接运行。常见例子有 C 和 C++。
虽然,咱们会区别 JIT 和 AOT 两种编译模式,但实际上,有不少语言并非彻底使用 JIT 或者 AOT 的,一般它们会混用这两种模式,来达到最大的性能优化。
Dart VM 支持四种编译模式:
Script:最多见的 JIT 模式,能够直接在虚拟机中执行 Dart 源码,像解释型语言同样使用。
经过执行 dart xxx.dart
就能够运行,写一些临时脚本,很是方便。
Kernel Snapshots:JIT 模式,和 Script 模式不一样的是,这种模式执行的是 Kernel AST 的二进制数据,这里不包含解析后的类和函数,编译后的代码,因此它们能够在不一样的平台之间移植。
在 Flutter’s Compilation Patterns 这篇文章中,将 Kernel Snapshots 称为 Script Snapshots 也是对的,这应该是以前的叫法,从 dart-lang 的 wiki 中能够看出来:
Dart Kernel 是 Dart 程序中的一种中间语言,更多的资料可阅读 Kernel Documentation。
经过执行 dart --snapshot-kind=kernel --snapshot=xx.snapshot xx.dart
生成。
JIT Application Snapshots:JIT 模式,这里执行的是已经解析过的类和函数,因此它会运行起来会更快。可是这不是平台无关,它只能针对 32位 或者 64位 架构运行。
经过执行 dart --snapshot-kind=app-jit --snapshot=xx.snapshot xx.dart
生成。
AOT Application Snapshots:AOT 模式,在这种模式下,Dart 源码会被提早编译成特定平台的二进制文件。
要使用 AOT 模式,须要使用 dart2aot
命令, 具体使用为:dart2aot xx.dart xx.dart.aot
,而后使用 dartaotruntime
命令执行。
Dart JIT 模式须要配合 Dart VM(虚拟机) ,AOT 则会使用 runtime 来执行,引用官网中的图片来讲明:
能够用下面这张图来总结上面的四种模式:
四种模式下的产物大小和启动速度比较:
下面的分析都是基于 v1.5.4-hotfix.2
执行 flutter build apk
时,默认会生成 release APK,实际是执行的 sdk/bin/flutter 命令,参数为 build apk:
DART_SDK_PATH="$FLUTTER_ROOT/bin/cache/dart-sdk"
DART="$DART_SDK_PATH/bin/dart"
SNAPSHOT_PATH="$FLUTTER_ROOT/bin/cache/flutter_tools.snapshot"
"$DART" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"
复制代码
上面的命令完整应该为:dart flutter_tools.snapshot args
,snapshot 文件咱们上面提过,是 Dart 的一种代码集合,相似 Java 中 Jar 包。
flutter_tools 的源码位于 sdk/packages/flutter_tools/bin 下的 flutter_tools.dart,跟 Java 同样,这里也有个 main()
函数,其中调用的是 executable.main()
函数。
在 main 函数中,会注册多个命令处理器,好比:
flutter doctor
命令flutter clean
命令咱们此次要研究的目标是 BuildCommand,它里面又包含了如下子命令处理器:
flutter build apk
命令flutter build bundle
命令flutter build aot
命令首先咱们要理清 build apk 或者 build ios,和 build bundle、build aot 之间的关系。这里咱们以 build apk 为例:
当执行 flutter build apk
时,最终会调用到 gradle.dart 中的 _buildGradleProjectV2()
方法,在这里最后也是调用 gradle 执行 assemble task。而在项目中的 android/app/build.gradle 中能够看到:
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
复制代码
也就是说,会在 flutter.gradle 这里去插入一些编译 Flutter 产物的脚本。
flutter.gradle 是经过插件的形式去实现的,这个插件命名为 FlutterPlugin。这个插件的主要做用有一些几点:
除了默认的 debug 和 release 以外,新增了 profile、dynamicProfile、dynamicRelease 这三种 buildTypes:
project.android.buildTypes {
profile {
initWith debug
if (it.hasProperty('matchingFallbacks')) {
matchingFallbacks = ['debug', 'release']
}
}
dynamicProfile {
initWith debug
if (it.hasProperty('matchingFallbacks')) {
matchingFallbacks = ['debug', 'release']
}
}
dynamicRelease {
initWith debug
if (it.hasProperty('matchingFallbacks')) {
matchingFallbacks = ['debug', 'release']
}
}
}
复制代码
动态添加 flutter.jar 依赖:
private void addFlutterJarApiDependency(Project project, buildType, Task flutterX86JarTask) {
project.dependencies {
String configuration;
if (project.getConfigurations().findByName("api")) {
configuration = buildType.name + "Api";
} else {
configuration = buildType.name + "Compile";
}
add(configuration, project.files {
String buildMode = buildModeFor(buildType)
if (buildMode == "debug") {
[flutterX86JarTask, debugFlutterJar]
} else if (buildMode == "profile") {
profileFlutterJar
} else if (buildMode == "dynamicProfile") {
dynamicProfileFlutterJar
} else if (buildMode == "dynamicRelease") {
dynamicReleaseFlutterJar
} else {
releaseFlutterJar
}
})
}
}
复制代码
动态添加第三方插件依赖:
project.dependencies {
if (project.getConfigurations().findByName("implementation")) {
implementation pluginProject
} else {
compile pluginProject
}
复制代码
在 assemble task 中添加一个 FlutterTask,这个 task 很是重要,这里会去生成 Flutter 所须要的产物:
首先,当 buildType 是 profile 或 release 时,会执行 flutter build aot
:
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}"
}
}
复制代码
其次,执行 flutter build bundle
命令,当 buildType 为 profile 或 release 时,添加额外的 --precompiled 选项。
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 "--compilation-trace-file", compilationTraceFilePath
}
if (createPatch) {
args "--patch"
args "--build-number", project.android.defaultConfig.versionCode
if (buildNumber != null) {
assert buildNumber == project.android.defaultConfig.versionCode
}
}
if (baselineDir != null) {
args "--baseline-dir", baselineDir
}
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"
}
}
复制代码
稍微总结下,当 buildType 为 debug 时,只须要执行:
flutter build bundle
复制代码
而当 buildType 为 release 时,须要执行两个命令:
flutter build aot
flutter build bundle --precompiled
复制代码
当执行 flutter build aot
时,相关的逻辑在 BuildAotCommand 中,它主要分红两个步骤:
编译 kernel。kernel 指 Dart 的一种中间语言,更多资料能够阅读 Kernel-Documentation。最终会调用到 compile.dart 中的 KernelCompiler.compile()
方法:
final List<String> command = <String>[
engineDartPath,
frontendServer,
'--sdk-root',
sdkRoot,
'--strong',
'--target=$targetModel',
];
if (trackWidgetCreation)
command.add('--track-widget-creation');
if (!linkPlatformKernelIn)
command.add('--no-link-platform');
if (aot) {
command.add('--aot');
command.add('--tfa');
}
if (targetProductVm) {
command.add('-Ddart.vm.product=true');
}
if (incrementalCompilerByteStorePath != null) {
command.add('--incremental');
}
Uri mainUri;
if (packagesPath != null) {
command.addAll(<String>['--packages', packagesPath]);
mainUri = PackageUriMapper.findUri(mainPath, packagesPath, fileSystemScheme, fileSystemRoots);
}
if (outputFilePath != null) {
command.addAll(<String>['--output-dill', outputFilePath]);
}
if (depFilePath != null && (fileSystemRoots == null || fileSystemRoots.isEmpty)) {
command.addAll(<String>['--depfile', depFilePath]);
}
if (fileSystemRoots != null) {
for (String root in fileSystemRoots) {
command.addAll(<String>['--filesystem-root', root]);
}
}
if (fileSystemScheme != null) {
command.addAll(<String>['--filesystem-scheme', fileSystemScheme]);
}
if (initializeFromDill != null) {
command.addAll(<String>['--initialize-from-dill', initializeFromDill]);
}
if (extraFrontEndOptions != null)
command.addAll(extraFrontEndOptions);
command.add(mainUri?.toString() ?? mainPath);
printTrace(command.join(' '));
final Process server = await processManager
.start(command)
.catchError((dynamic error, StackTrace stack) {
printError('Failed to start frontend server $error, $stack');
});
复制代码
engineDart 指向 Dart SDK 目录,上面代码执行的最终命令能够简化为:
dart frontend_server.dart.snapshot --output-dill app.dill packages:main.dart
复制代码
app.dill 这里面其实就包含了咱们的业务代码了,可使用 strings app.dill
查看:
生成 snpshot。相关的代码位于 base/build.dart 中的 AOTSnapshotter.build()
函数中,有两种模式:app-aot-assemble 和 app-aot-blobs。
在执行 aot 命令时,增长 --build-shared-library
选项,完整命令以下:
flutter build aot --build-shared-library
iOS 只能使用这种模式,在 Flutter SDK 1.7.x 以后,这个也是 Android 的默认选项。
// buildSharedLibrary is ignored for iOS builds.
if (platform == TargetPlatform.ios)
buildSharedLibrary = false;
if (buildSharedLibrary && androidSdk.ndk == null) {
// 须要有 NDK 环境
final String explanation = AndroidNdk.explainMissingNdk(androidSdk.directory);
printError(
'Could not find NDK in Android SDK at ${androidSdk.directory}:\n'
'\n'
' $explanation\n'
'\n'
'Unable to build with --build-shared-library\n'
'To install the NDK, see instructions at https://developer.android.com/ndk/guides/'
);
return 1;
}
复制代码
这种模式下,会将产物编译为二进制文件,在 iOS 上为 App.framework,Android 上则为 app.so。
// Assembly AOT snapshot.
outputPaths.add(assembly);
genSnapshotArgs.add('--snapshot_kind=app-aot-assembly');
genSnapshotArgs.add('--assembly=$assembly');
复制代码
当使用这种模式时,会生成四个产物,分别是:
instr 全称是 Instructions
isolate_snapshot_data:
表示 isolate 堆存储区的初始状态和特定的信息。和 vm_snapshot_data 配合,更快的启动 Dart VM。
isolate_snapshot_instr:
包含由 Dart isolate 执行的 AOT 代码。
vm_snapshot_data:
表示 isolates 之间的共享的 Dart 堆存储区的初始状态,用于更快的启动 Dart VM。
vm_snapshot_instr:
包含 VM 中全部的 isolates 之间共享的常见例程的指令
isolate_snapshot_data 和 isolate_snapshot_instr 跟业务相关,而 vm_snapshot_data 和 vm_snapshot_instr 则是跟 VM 相关,无关业务。
咱们可使用 strings
查看下 isolate_snapshot_data 中内容:
上面两种模式相关的代码以下:
final String assembly = fs.path.join(outputDir.path, 'snapshot_assembly.S');
if (buildSharedLibrary || platform == TargetPlatform.ios) {
// Assembly AOT snapshot.
outputPaths.add(assembly);
genSnapshotArgs.add('--snapshot_kind=app-aot-assembly');
genSnapshotArgs.add('--assembly=$assembly');
} else {
// Blob AOT snapshot.
final String vmSnapshotData = fs.path.join(outputDir.path, 'vm_snapshot_data');
final String isolateSnapshotData = fs.path.join(outputDir.path, 'isolate_snapshot_data');
final String vmSnapshotInstructions = fs.path.join(outputDir.path, 'vm_snapshot_instr');
final String isolateSnapshotInstructions = fs.path.join(outputDir.path, 'isolate_snapshot_instr');
outputPaths.addAll(<String>[vmSnapshotData, isolateSnapshotData, vmSnapshotInstructions, isolateSnapshotInstructions]);
genSnapshotArgs.addAll(<String>[
'--snapshot_kind=app-aot-blobs',
'--vm_snapshot_data=$vmSnapshotData',
'--isolate_snapshot_data=$isolateSnapshotData',
'--vm_snapshot_instructions=$vmSnapshotInstructions',
复制代码
'--isolate_snapshot_instructions=$isolateSnapshotInstructions',
]);
}
从执行速度来看,app-aot-assemble 是要快于 app-aot-blobs 的,由于它不须要 Dart VM 环境,只须要 Dart Runtime 便可,而 snapshots 文件是须要 Dart VM 去加载执行的。但使用 snapshots 使得动态执行代码变成可能。
iOS 默认使用 app-aot-assemble 模式,更可能是 App Store 自己的限制:
> 引用于[flutters-compilation-patterns](https://proandroiddev.com/flutters-compilation-patterns-24e139d14177)
>
> App Store does not allow dispatch binary executable code.
而 Android 默认使用 app-aot-blobs 模式,可能更可能是从性能方面考虑,必须调用从 so 文件中调用 native 函数,须要用 JNI,有性能损耗,并且调用也比较麻烦,不过在 Flutter SDK 1.7.x 已经改为 app-aot-assemble 模式。
#### Build Bundle
当执行 `flutter build bundle` 时,相关的代码逻辑在 BuildBundleCommand 中,build bundle 主要作两件事:第一,若是没有添加 `--precompiled` 选项时,会先编译 kernel;第二,生成 assets(图片、字体等)。
1. 编译 kernel。这个步骤和 build aot 的第一个步骤是同样的,最终会生成 app.dill,这是业务代码编译后的产物。同时,这个会建立一个 kernelContent,这个在第二个步骤会讲到:
``` dart
kernelContent = DevFSFileContent(fs.file(compilerOutput.outputFilename));
```
2. 生成 assets。首先,会收集图片资源、字体等:
``` dart
final AssetBundle assets = await buildAssets(
manifestPath: manifestPath,
assetDirPath: assetDirPath,
packagesPath: packagesPath,
reportLicensedPackages: reportLicensedPackages,
);
```
其中包含有如下内容:

接着,会将这些,包含上面生成 kernelContent,一块儿收集到指定目录,kernelContent 就是编译生成的 app.dill,但这里会拷贝并重命名为 kernel_blob.bin:
``` dart
const String _kKernelKey = 'kernel_blob.bin';
const String _kVMSnapshotData = 'vm_snapshot_data';
const String _kIsolateSnapshotData = 'isolate_snapshot_data';
final String vmSnapshotData = artifacts.getArtifactPath(Artifact.vmSnapshotData, mode: buildMode);
final String isolateSnapshotData = artifacts.getArtifactPath(Artifact.isolateSnapshotData, mode: buildMode);
assetEntries[_kKernelKey] = kernelContent;
assetEntries[_kVMSnapshotData] = DevFSFileContent(fs.file(vmSnapshotData));
assetEntries[_kIsolateSnapshotData] = DevFSFileContent(fs.file(isolateSnapshotData));
```
咱们注意到,这里一样会生成 vm_snapshot_data 和 isolate_snapshot_data,但并无生成相应的指令集。在这里的 isolate_snapshot_data 中并不会包含咱们的业务代码,业务代码会存放在 kernel_blob.bin 文件中。可使用 `strings` 命令查看,并且这两个文件的 MD5 值是一致的。
```
MD5 (app.dill) = 3876e8c6f4b13a88cc3cfc3b9fd108c4
MD5 (flutter_assets/kernel_blob.bin) = 3876e8c6f4b13a88cc3cfc3b9fd108c4
```
关于 snapshot 生成相关能够去看 **gen_snapshot**。
#### 小结
##### debug
debug 模式下,会执行一个命令:
``` shell
flutter build bundle
复制代码
它会生成 assets、vm_snapshot_data、isolate_snapshot_data 和 kernel_bloc.bin。其中咱们的业务代码存放在 kernel_bloc.bin 中,它是 app.dill 的拷贝。
release 模式下,会执行两个命令:
flutter build aot
复制代码
它会先生成 app.dill,并且这里分为两种模式:app-aot-assemble 和 app-aot-blobs。iOS 或者 添加了 --build-shared-library
会使用 app-aot-assemble,这种模式会生成 App.Framework 或 app.so。Android 默认使用 app-aot-blobs,这种模式会生成 isolate_snapshot_data、isolate_snapshot_instr、vm_snapshot_data 和 vm_snapshot_instr 四个文件。
flutter build bundle --precompiled
复制代码
这里只会生成 assets。
调试是阅读源码最好的帮手,调试 flutter_tools 的方式比较简单,我用的是 IntelliJ,Android Studio 应该也相似,先导入源码,源码位于 sdk/packages/flutter_tools;新建一个 Dart Command Line App,设置 Dart file 为 bin/flutter_tools.dart,Program arguments 设置你要调试的命令,好比 build aot
,Working directory 则用 flutter create
建立个项目便可;最后打好断点,debug 运行。
由于源码中有不少异步代码,用 await 修饰的,可使用 Force run to Cursor 直接执行到下一行便可。
为了不篇幅太长,文章尽可能避免贴不少的代码,只是贴了关键函数,感兴趣的读者能够去阅读相关的源码。
Android 在 release 模式下,使用 app-aot-blobs 模式,用的是 snapshot 文件,这里要实现动态下发代码,应该仍是有可能的,须要进一步研究。下一步应该会研究下热加载的实现,这对于实现动态化应该颇有帮助。
最后,Flutter 是一个很是好玩的事物,Dart 也是一个很是优秀的语言,enjoy it。
由于我用习惯了 Typora 写 Markdown 了,但又须要用到掘金的图床,因此写了个小脚本用来上传图片。
import requests import sys if __name__ == "__main__": file_path = sys.argv[1] upload_file = open(file_path,mode='rb') data = requests.post('https://cdn-ms.juejin.im/v1/upload?bucket=gold-user-assets',files={'file': upload_file}).json() print('upload url: ' + data['d']['url']['https']) 复制代码