寫這篇文章的時間爲: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,
);
```
其中包含有如下內容:
![assets](https://user-gold-cdn.xitu.io/2019/8/30/16ce1dca5d92427d?w=745&h=265&f=png&s=49891)
接着,會將這些,包含上面生成 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']) 複製代碼