Flutter之Dart编译

前言

App中使用flutter已经有段时间了,最近遇到一个bug记录一下。更新flutter module工程pubspec plugin依赖,App工程中pod update之后,从功能表现上看依然是老代码。第一感觉是缓存导致的,xcode clean 以及删除DerivedData目录重新build依然不行,flutter module工程中执行flutter clean然后xcode build是正常的,所以应该是dart编译产物有缓存导致的。接下来看下dart编译过程。

编译

1
2
cd path/to/flutter module
flutter build ios --debug --simulator

进入到flutter module工程目录 执行flutter build ios命令

1
2
3
4
5
Running Xcode build...

├─Assembling Flutter resources... 3.6s
└─Compiling, linking and signing... 25.4s
Xcode build done. 43.9s

可以看到会进行xcode build,进到.ios目录通过xcode打开Runner工程

IMAGE

可以看到build phases中这样一段脚本,这里就是执行dart代码编译的入口。

xcode_backend.sh

进入到脚本所在目录,看下build对应的方法 BuildApp

1
2
3
4
5
6
7
8
9
10
11
12
13
if [[ $# == 0 ]]; then
# Backwards-compatibility: if no args are provided, build.
BuildApp
else
case $1 in
"build")
BuildApp ;;
"thin")
ThinAppFrameworks ;;
"embed")
EmbedFlutterFrameworks ;;
esac
fi
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
BuildApp() {

...

StreamOutput " ├─Assembling Flutter resources..."
RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics \
${verbose_flag} \
build bundle \
--target-platform=ios \
--target="${target_path}" \
--${build_mode} \
--depfile="${build_dir}/snapshot_blob.bin.d" \
--asset-dir="${derived_dir}/App.framework/${assets_path}" \
${precompilation_flag} \
${flutter_engine_flag} \
${local_engine_flag} \
${track_widget_creation_flag}

if [[ $? -ne 0 ]]; then
EchoError "Failed to package ${project_path}."
exit -1
fi
StreamOutput "done"
StreamOutput " └─Compiling, linking and signing..."

RunCommand popd > /dev/null

echo "Project ${project_path} built and packaged successfully."
return 0
}

可以看到 ├─Assembling Flutter resources… 在build ios 执行过程中出现过,flutter build bundle 就会开始真正的dart编译
–depfile 指定参与编译的dart文件路径集合
–asset-dir 指定资源产物的目录

flutter命令

路径 $FLUTTER_ROOT/bin/flutter

1
2
3
4
5
6
7
8
9
10
11
12
...

FLUTTER_TOOLS_DIR="$FLUTTER_ROOT/packages/flutter_tools"
SNAPSHOT_PATH="$FLUTTER_ROOT/bin/cache/flutter_tools.snapshot"
STAMP_PATH="$FLUTTER_ROOT/bin/cache/flutter_tools.stamp"
SCRIPT_PATH="$FLUTTER_TOOLS_DIR/bin/flutter_tools.dart"
DART_SDK_PATH="$FLUTTER_ROOT/bin/cache/dart-sdk"

DART="$DART_SDK_PATH/bin/dart"
PUB="$DART_SDK_PATH/bin/pub"

"$DART" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@"

flutter_toools.snapshot实际上就是$FLUTTER_ROOT/packages/flutter_tools这个项目编译生成的snapshot文件

所以flutter build bundle 就是使用dart来执行flutter_tools项目的main方法

flutter_tools

路径 $FLUTTER_ROOT/packages/flutter_tools

main方法定义 $FLUTTER_ROOT/packages/flutter_tools/bin/flutter_tools.dart

1
2
3
void main(List<String> args) {
executable.main(args);
}

再看看 lib/executable.dart ,在这里会预先创建好每一种命令对应的对象command,通过解析args参数找到对应的command。在BuildCommand类中

1
2
3
4
5
6
7
8
9
BuildCommand({bool verboseHelp = false}) {
addSubcommand(BuildApkCommand(verboseHelp: verboseHelp));
addSubcommand(BuildAppBundleCommand(verboseHelp: verboseHelp));
addSubcommand(BuildAotCommand());
addSubcommand(BuildIOSCommand());
addSubcommand(BuildFlxCommand());
addSubcommand(BuildBundleCommand(verboseHelp: verboseHelp));
addSubcommand(BuildWebCommand());
}

看到BuildIOSCommand 以及 BuildBundleCommand的创建。
BuildIOSCommand 就是前面提到的flutter build ios 会执行到的,这里我们重点看下BuildBundleCommand是如何编译dart代码的?编译后生成了哪些资源?这些资源都是些什么?

BuildBundleCommand

IMAGE

  • app.dill : 这就是dart代码编译后的二级制文件
  • Frontend_server.d : 这里面放的是frontend_server.dart.snapshot的绝对路径,使用该snapshot来编译dart代码生成上面的app.dill
  • snapshot_blob.bin.d : 这里面放的是所有参与编译的dart文件的绝对路径的集合,包括项目的代码和flutterSdk的代码以及pub库中的三方代码。
  • snapshot_blob.bin.d.fingerprint : 这里面放的是snapshot_blob.bin.d中的所有文件的绝对路径以及每个文件所对应的md5值。使用这个md5来判断该文件是否有修改。在每次编译的时候会判断,如果没有文件修改,则直接跳过编译。

编译Dart资源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
Future<void> build({
TargetPlatform platform,
BuildMode buildMode,
String mainPath = defaultMainPath,
String manifestPath = defaultManifestPath,
String applicationKernelFilePath,
String depfilePath,
String privateKeyPath = defaultPrivateKeyPath,
String assetDirPath,
String packagesPath,
bool precompiledSnapshot = false,
bool reportLicensedPackages = false,
bool trackWidgetCreation = false,
String compilationTraceFilePath,
bool createPatch = false,
String buildNumber,
String baselineDir,
List<String> extraFrontEndOptions = const <String>[],
List<String> extraGenSnapshotOptions = const <String>[],
List<String> fileSystemRoots,
String fileSystemScheme,
}) async {

// xcode_backend.sh中通过--depfile传入进来的
// 默认是build/snapshot_blob.bin.d文件

depfilePath ??= defaultDepfilePath;

// 通过--asset-dir传入
// 该目录中文件就是flutter的产物,最终合并到app.framework中的flutter_assets目录

assetDirPath ??= getAssetBuildDirectory();
packagesPath ??= fs.path.absolute(PackageMap.globalPackagesPath);

// app.dill dart代码编译后的二级制文件
applicationKernelFilePath ??= getDefaultApplicationKernelPath(trackWidgetCreation: trackWidgetCreation);
final FlutterProject flutterProject = await FlutterProject.current();

if (compilationTraceFilePath != null) {
if (buildMode != BuildMode.dynamicProfile && buildMode != BuildMode.dynamicRelease) {

compilationTraceFilePath = null;

} else if (compilationTraceFilePath.isEmpty) {
// Disable JIT snapshotting if flag is empty.
printStatus('Code snapshot will be disabled for this build.');
compilationTraceFilePath = null;

} else if (!fs.file(compilationTraceFilePath).existsSync()) {
// Be forgiving if compilation trace file is missing.
printStatus('No compilation trace available. To optimize performance, consider using --train.');
final File tmp = fs.systemTempDirectory.childFile('flutterEmptyCompilationTrace.txt');
compilationTraceFilePath = (tmp..createSync(recursive: true)).path;

} else {
printStatus('Code snapshot will use compilation training file $compilationTraceFilePath.');
}
}

DevFSContent kernelContent;
if (!precompiledSnapshot) {
if ((extraFrontEndOptions != null) && extraFrontEndOptions.isNotEmpty)
printTrace('Extra front-end options: $extraFrontEndOptions');
ensureDirectoryExists(applicationKernelFilePath);
final KernelCompiler kernelCompiler = await kernelCompilerFactory.create(flutterProject);

// 编译dart代码,生成app.dill 和 snapshot_blob.bin.d 以及 snapshot_blob.bin.d.fingerprint
final CompilerOutput compilerOutput = await kernelCompiler.compile(
sdkRoot: artifacts.getArtifactPath(Artifact.flutterPatchedSdkPath),
incrementalCompilerByteStorePath: compilationTraceFilePath != null ? null :
fs.path.absolute(getIncrementalCompilerByteStoreDirectory()),
mainPath: fs.file(mainPath).absolute.path,
outputFilePath: applicationKernelFilePath,
depFilePath: depfilePath,
trackWidgetCreation: trackWidgetCreation,
extraFrontEndOptions: extraFrontEndOptions,
fileSystemRoots: fileSystemRoots,
fileSystemScheme: fileSystemScheme,
packagesPath: packagesPath,
linkPlatformKernelIn: compilationTraceFilePath != null,
);
if (compilerOutput?.outputFilename == null) {
throwToolExit('Compiler failed on $mainPath');
}
kernelContent = DevFSFileContent(fs.file(compilerOutput.outputFilename));
// 生成 frontend_server.d文件,向文件中写入frontendServerSnapshotForEngineDartSdk的路径
await fs.directory(getBuildDirectory()).childFile('frontend_server.d')
.writeAsString('frontend_server.d: ${artifacts.getArtifactPath(Artifact.frontendServerSnapshotForEngineDartSdk)}\n');

if (compilationTraceFilePath != null) {
final JITSnapshotter snapshotter = JITSnapshotter();
final int snapshotExitCode = await snapshotter.build(
platform: platform,
buildMode: buildMode,
mainPath: applicationKernelFilePath,
outputPath: getBuildDirectory(),
packagesPath: packagesPath,
compilationTraceFilePath: compilationTraceFilePath,
extraGenSnapshotOptions: extraGenSnapshotOptions,
createPatch: createPatch,
buildNumber: buildNumber,
baselineDir: baselineDir,
);
if (snapshotExitCode != 0) {
throwToolExit('Snapshotting exited with non-zero exit code: $snapshotExitCode');
}
}
}
// 生成 flutter_assets
final AssetBundle assets = await buildAssets(
manifestPath: manifestPath,
assetDirPath: assetDirPath,
packagesPath: packagesPath,
reportLicensedPackages: reportLicensedPackages,
);
if (assets == null)
throwToolExit('Error building assets', exitCode: 1);

await assemble(
buildMode: buildMode,
assetBundle: assets,
kernelContent: kernelContent,
privateKeyPath: privateKeyPath,
assetDirPath: assetDirPath,
compilationTraceFilePath: compilationTraceFilePath,
);
}

编译Dart代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
class KernelCompiler {
const KernelCompiler();

Future<CompilerOutput> compile({
String sdkRoot,
String mainPath,
String outputFilePath,
String depFilePath,
TargetModel targetModel = TargetModel.flutter,
bool linkPlatformKernelIn = false,
bool aot = false,
@required bool trackWidgetCreation,
List<String> extraFrontEndOptions,
String incrementalCompilerByteStorePath,
String packagesPath,
List<String> fileSystemRoots,
String fileSystemScheme,
bool targetProductVm = false,
String initializeFromDill,
}) async {
final String frontendServer = artifacts.getArtifactPath(
Artifact.frontendServerSnapshotForEngineDartSdk
);
FlutterProject flutterProject;
if (fs.file('pubspec.yaml').existsSync()) {
flutterProject = await FlutterProject.current();
}

// TODO(cbracken): eliminate pathFilter.
// Currently the compiler emits buildbot paths for the core libs in the
// depfile. None of these are available on the local host.
Fingerprinter fingerprinter;
// 如果snapshot_blob.bin.d文件不为空,则说明有编译缓存
if (depFilePath != null) {
// 判断与上次编译对比,是否有文件的md5改变
fingerprinter = Fingerprinter(
// snapshot_blob.bin.d.fingerprint文件
fingerprintPath: '$depFilePath.fingerprint',
paths: <String>[mainPath],
properties: <String, String>{
'entryPoint': mainPath,
'trackWidgetCreation': trackWidgetCreation.toString(),
'linkPlatformKernelIn': linkPlatformKernelIn.toString(),
'engineHash': Cache.instance.engineRevision,
'buildersUsed': '${flutterProject != null ? flutterProject.hasBuilders : false}',
},
depfilePaths: <String>[depFilePath],
pathFilter: (String path) => !path.startsWith('/b/build/slave/'),
);
// 判断是否有文件改动,如果没有,则直接返回。
if (await fingerprinter.doesFingerprintMatch()) {
printTrace('Skipping kernel compilation. Fingerprint match.');
return CompilerOutput(outputFilePath, 0, /* sources */ null);
}
}

...

// 如果没有上次编译缓存,或者文件有改变,Fingerprinter不匹配,则使用dart重新编译
final List<String> command = <String>[
engineDartPath,
frontendServer,
'--sdk-root',
sdkRoot,
'--strong',
'--target=$targetModel',
];

...

//参数拼接
final Process server = await processManager
.start(command)
.catchError((dynamic error, StackTrace stack) {
printError('Failed to start frontend server $error, $stack');
});

final StdoutHandler _stdoutHandler = StdoutHandler();

server.stderr
.transform<String>(utf8.decoder)
.listen(printError);
server.stdout
.transform<String>(utf8.decoder)
.transform<String>(const LineSplitter())
.listen(_stdoutHandler.handler);
final int exitCode = await server.exitCode;
if (exitCode == 0) {
if (fingerprinter != null) {
await fingerprinter.writeFingerprint();
}
return _stdoutHandler.compilerOutput.future;
}
return null;
}
}

Fingerprint对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
Future<bool> doesFingerprintMatch() async {
try {
// 获取到当前的 snapshot_blob.bin.d.fingerprint文件
final File fingerprintFile = fs.file(fingerprintPath);
if (!fingerprintFile.existsSync())
return false;

if (!_depfilePaths.every(fs.isFileSync))
return false;

final List<String> paths = await _getPaths();
if (!paths.every(fs.isFileSync))
return false;
// 读取缓存的的snapshot_blob.bin.d.fingerprint文件,构建一个老的Fingerprint对象
final Fingerprint oldFingerprint = Fingerprint.fromJson(await fingerprintFile.readAsString());

// 构建一个新的Fingerprint对象
final Fingerprint newFingerprint = await buildFingerprint();
// 对比两次的文件集合中的每个文件的md5是否一样
return oldFingerprint == newFingerprint;
} catch (e) {
// Log exception and continue, fingerprinting is only a performance improvement.
printTrace('Fingerprint check error: $e');
}
return false;
}

重点 看看 newFingerprint

1
2
3
4
5
6
7
8
9
10
11
12
13
Future<Fingerprint> buildFingerprint() async {
final List<String> paths = await _getPaths();
return Fingerprint.fromBuildInputs(_properties, paths);
}

Future<List<String>> _getPaths() async {
final Set<String> paths = _paths.toSet();
// 使用缓存的snapshot_blob.bin.d文件中的文件集合
for (String depfilePath in _depfilePaths)
paths.addAll(await readDepfile(depfilePath));
final FingerprintPathFilter filter = _pathFilter ?? (String path) => true;
return paths.where(filter).toList()..sort();
}

可以看到newFingerprint 路径依旧是使用缓存的路径,依次计算路径对应文件的md5,所以问题就在这里了

执行flutter packages upgrade更新pub依赖的时候,build目录下的缓存产物并不会有任何变动,路径依然是老的路径。有一种情况就是module工程 lib 目录下的dat文件有改动,newFingerprint就会跟old不一样,这会重新编译dart,这里又有一个问题,就是如果lib目录下是新增dart文件 则不会被编译进去。

最后

综上,执行flutter clean命令,清空build目录缓存文件,build ios 就会重新编译整个dart文件,包括pub依赖中的。

IMAGE

参考链接

Flutter深入之flutter-build-bundle命令如何编译Dart?