手把手教你分离flutter ios 编译产物–附工具
1、为什么写这篇文章?
Flutter ios安装包size的裁剪一直是个备受关注的主题,年前字节跳动分享了一篇文章(https://juejin.im/post/5de8a32c51882512664affa4),提到了ios分离AOT编译产物,把里面的数据段和资源提取出来以减少安装包size,但文章里面并没有展开介绍如何实现,这篇文章会很详细的分析如何分离AOT编译产物。并给出工具,方便没编译flutter engine经验的同学也可以快速的实现这功能。
2、ios编译产物构成
本文主要分析App.framework里面的生成流程,以及如何分离AOT编译产物,App.framework的构成如下图所示。
主要有App动态库二进制文件、flutter_assets还有Info.plist三部分构成,而App动态库二进制文件又由4部分构成,vm的数据段、代码段和isolate的数据段、代码段。其中flutter_assets、vm数据段、isolate数据段都是可以不打包到ipa中,可以从外部document中加载到,这就让我们有缩减ipa包的可能了。
3、真实线上项目AOT编译产物前后对比
很多人肯定会关心最终缩减的效果。我们先给出一个真实线上项目,用官方编译engine和用分离产物的engine生成的App.framework的对比图。
官方engine生成的App.framework构成如下,其中App动态库二进制文件19.2M,flutter_assets有3.3M,共22.5M。
用分离产物的engine生成的App.framework构成如下,只剩App动态库二进制文件14.8M。
App.framework从22.5裁到14.8M,不同项目可能不一样。
4、AOT编译产物生成原理及分离方法介绍
每次xcode项目进行进行构建前都会运行xcode_backend.sh这个脚本进行flutter产物打包,我们从xcode_backend.sh开始分析。从上文分析App.framework里面总共有三个文件生成二进制文件App、资源文件flutter_assets目录和Info.plist文件,这里面我们只关心二进制文件App和flutter_assets目录是怎样生成的。
4.1、App文件生成流程
4.1.1、xcode_backend.sh
分析xcode_backend.sh,我们可以发现生成App和flutter_assets的关键shell代码如下
# App动态库二进制文件
RunCommand "${FLUTTER_ROOT}/bin/flutter" --suppress-analytics \
${verbose_flag} \
build aot \
--output-dir="${build_dir}/aot" \
--target-platform=ios \
--target="${target_path}" \
--${build_mode} \
--ios-arch="${archs}" \
${flutter_engine_flag} \
${local_engine_flag} \
${bitcode_flag}
.
.
.
RunCommand cp -r -- "${app_framework}" "${derived_dir}"
# 生成flutter_assets
RunCommand "${FLUTTER_ROOT}/bin/flutter" \
${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}
4.1.2、${FLUTTER_ROOT}/bin/flutter
从上面的代码可以看到这里调用了的远行了 /bin/flutter 这个shell脚本,这里介绍另一篇讲解Flutter命令执行机制的文章, /bin/flutter 里面提到真正运行代码的是
...
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" "$@"
//等价于下面的命令
/bin/cache/dart-sdk/bin/dart $FLUTTER_TOOL_ARGS "bin/cache/flutter_tools.snapshot" "$@"
就是说通过dart命令运行flutter_tools.snapshot这个产物
###4.1.3、dart代码
flutter_tools.snapshot的入口是
[-> flutter/packages/flutter_tools/bin/flutter_tools.dart]
import 'package:flutter_tools/executable.dart' as executable;
void main(List<String> args) {
executable.main(args);
}
import 'runner.dart' as runner;
Future<void> main(List<String> args) async {
...
await runner.run(args, <FlutterCommand>[
AnalyzeCommand(verboseHelp: verboseHelp),
AttachCommand(verboseHelp: verboseHelp),
BuildCommand(verboseHelp: verboseHelp),
ChannelCommand(verboseHelp: verboseHelp),
CleanCommand(),
ConfigCommand(verboseHelp: verboseHelp),
CreateCommand(),
DaemonCommand(hidden: !verboseHelp),
DevicesCommand(),
DoctorCommand(verbose: verbose),
DriveCommand(),
EmulatorsCommand(),
FormatCommand(),
GenerateCommand(),
IdeConfigCommand(hidden: !verboseHelp),
InjectPluginsCommand(hidden: !verboseHelp),
InstallCommand(),
LogsCommand(),
MakeHostAppEditableCommand(),
PackagesCommand(),
PrecacheCommand(),
RunCommand(verboseHelp: verboseHelp),
ScreenshotCommand(),
ShellCompletionCommand(),
StopCommand(),
TestCommand(verboseHelp: verboseHelp),
TraceCommand(),
TrainingCommand(),
UpdatePackagesCommand(hidden: !verboseHelp),
UpgradeCommand(),
VersionCommand(),
], verbose: verbose,
muteCommandLogging: muteCommandLogging,
verboseHelp: verboseHelp,
overrides: <Type, Generator>{
CodeGenerator: () => const BuildRunner(),
});
}
经过一轮调用后,真正编译产物的类在 GenSnapshot.run,调用栈http://gityuan.com/2019/09/07/flutter_run/这篇文章有详细介绍,这里就不细说了
[-> lib/src/base/build.dart]
class GenSnapshot {
Future<int> run({
@required SnapshotType snapshotType,
IOSArch iosArch,
Iterable<String> additionalArgs = const <String>[],
}) {
final List<String> args = <String>[
'--causal_async_stacks',
]..addAll(additionalArgs);
//获取gen_snapshot命令的路径
final String snapshotterPath = getSnapshotterPath(snapshotType);
//iOS gen_snapshot是一个多体系结构二进制文件。 作为i386二进制文件运行将生成armv7代码。 作为x86_64二进制文件运行将生成arm64代码。
// /usr/bin/arch可用于运行具有指定体系结构的二进制文件
if (snapshotType.platform == TargetPlatform.ios) {
final String hostArch = iosArch == IOSArch.armv7 ? '-i386' : '-x86_64';
return runCommandAndStreamOutput(<String>['/usr/bin/arch', hostArch, snapshotterPath]..addAll(args));
}
return runCommandAndStreamOutput(<String>[snapshotterPath]..addAll(args));
}
}
GenSnapshot.run具体命令根据前面的封装,最终等价于:
//这是针对iOS的genSnapshot命令
/usr/bin/arch -x86_64 flutter/bin/cache/artifacts/engine/ios-release/gen_snapshot
--causal_async_stacks
--deterministic
--snapshot_kind=app-aot-assembly
--assembly=build/aot/arm64/snapshot_assembly.S
build/aot/app.dill
此处gen_snapshot是一个二进制可执行文件,所对应的执行方法源码为third_party/dart/runtime/bin/gen_snapshot.cc
这个文件是flutter engine里面文件,需要拉取engine的代码才能修改,编译flutter engine 可以参考文章手把手教你编译Flutte