Flutter ios安装包size的裁剪一直是个备受关注的主题,年前字节跳动分享了一篇文章(http://www.javashuo.com/article/p-srlgmach-gp.html),提到了ios分离AOT编译产物,把里面的数据段和资源提取出来以减小安装包size,但文章里面并无展开介绍如何实现,这篇文章会很详细的分析如何分离AOT编译产物。并给出工具,方便没编译flutter engine经验的同窗也能够快速的实现这功能。ios
本文主要分析App.framework里面的生成流程,以及如何分离AOT编译产物,App.framework的构成以下图所示。c++
主要有App动态库二进制文件、flutter_assets还有Info.plist三部分构成,而App动态库二进制文件又由4部分构成,vm的数据段、代码段和isolate的数据段、代码段。其中flutter_assets、vm数据段、isolate数据段都是能够不打包到ipa中,能够从外部document中加载到,这就让咱们有缩减ipa包的可能了。git
不少人确定会关心最终缩减的效果。咱们先给出一个真实线上项目,用官方编译engine和用分离产物的engine生成的App.framework的对比图。github
官方engine生成的App.framework构成以下,其中App动态库二进制文件19.2M,flutter_assets有3.3M,共22.5M。shell
用分离产物的engine生成的App.framework构成以下,只剩App动态库二进制文件14.8M。xcode
App.framework从22.5裁到14.8M,不一样项目可能不同。缓存
每次xcode项目进行进行构建前都会运行xcode_backend.sh这个脚本进行flutter产物打包,咱们从xcode_backend.sh开始分析。从上文分析App.framework里面总共有三个文件生成二进制文件App、资源文件flutter_assets目录和Info.plist文件,这里面咱们只关心二进制文件App和flutter_assets目录是怎样生成的。服务器
分析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}
从上面的代码能够看到这里调用了的远行了 /bin/flutter 这个shell脚本,这里介绍另外一篇讲解Flutter命令执行机制的文章, /bin/flutter 里面提到真正运行代码的是app
... 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这个产物
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 能够参考文章手把手教你编译Flutter engine,下文咱们也会介绍编译完flutter engine ,怎么拿到gen_snapshot编译后的二进制文件。
Flutter机器码生成gen_snapshot这篇文章对gen_snapshot流程作了详细的分析,这里我直接给出最后结论,生成数据段和代码段的代码在
AssemblyImageWriter::WriteText这个函数里面
[-> third_party/dart/runtime/vm/image_snapshot.cc]
void AssemblyImageWriter::WriteText(WriteStream* clustered_stream, bool vm) { Zone* zone = Thread::Current()->zone(); //写入头部 const char* instructions_symbol = vm ? "_kDartVmSnapshotInstructions" : "_kDartIsolateSnapshotInstructions"; assembly_stream_.Print(".text\n"); assembly_stream_.Print(".globl %s\n", instructions_symbol); assembly_stream_.Print(".balign %" Pd ", 0\n", VirtualMemory::PageSize()); assembly_stream_.Print("%s:\n", instructions_symbol); //写入头部空白字符,使得指令快照看起来像堆页 intptr_t instructions_length = next_text_offset_; WriteWordLiteralText(instructions_length); intptr_t header_words = Image::kHeaderSize / sizeof(uword); for (intptr_t i = 1; i < header_words; i++) { WriteWordLiteralText(0); } //写入序幕.cfi_xxx FrameUnwindPrologue(); Object& owner = Object::Handle(zone); String& str = String::Handle(zone); ObjectStore* object_store = Isolate::Current()->object_store(); TypeTestingStubNamer tts; intptr_t text_offset = 0; for (intptr_t i = 0; i < instructions_.length(); i++) { auto& data = instructions_[i]; const bool is_trampoline = data.trampoline_bytes != nullptr; if (is_trampoline) { //针对跳床函数 const auto start = reinterpret_cast<uword>(data.trampoline_bytes); const auto end = start + data.trampline_length; //写入.quad xxx字符串 text_offset += WriteByteSequence(start, end); delete[] data.trampoline_bytes; data.trampoline_bytes = nullptr; continue; } const intptr_t instr_start = text_offset; const Instructions& insns = *data.insns_; const Code& code = *data.code_; // 1. 写入 头部到入口点 { NoSafepointScope no_safepoint; uword beginning = reinterpret_cast<uword>(insns.raw_ptr()); uword entry = beginning + Instructions::HeaderSize(); //ARM64 32位对齐 //指令的只读标记 uword marked_tags = insns.raw_ptr()->tags_; marked_tags = RawObject::OldBit::update(true, marked_tags); marked_tags = RawObject::OldAndNotMarkedBit::update(false, marked_tags); marked_tags = RawObject::OldAndNotRememberedBit::update(true, marked_tags); marked_tags = RawObject::NewBit::update(false, marked_tags); //写入标记 WriteWordLiteralText(marked_tags); beginning += sizeof(uword); text_offset += sizeof(uword); text_offset += WriteByteSequence(beginning, entry); } // 2. 在入口点写入标签 owner = code.owner(); if (owner.IsNull()) { // owner为空,说明是一个常规的stub,其中stub列表定义在stub_code_list.h中的VM_STUB_CODE_LIST const char* name = StubCode::NameOfStub(insns.EntryPoint()); if (name != nullptr) { assembly_stream_.Print("Precompiled_Stub_%s:\n", name); } else { if (name == nullptr) { // isolate专有的stub代码[见小节3.5.1] name = NameOfStubIsolateSpecificStub(object_store, code); } assembly_stream_.Print("Precompiled__%s:\n", name); } } else if (owner.IsClass()) { //owner为Class,说明是该类分配的stub,其中class列表定义在class_id.h中的CLASS_LIST_NO_OBJECT_NOR_STRING_NOR_ARRAY str = Class::Cast(owner).Name(); const char* name = str.ToCString(); EnsureAssemblerIdentifier(const_cast<char*>(name)); assembly_stream_.Print("Precompiled_AllocationStub_%s_%" Pd ":\n", name, i); } else if (owner.IsAbstractType()) { const char* name = tts.StubNameForType(AbstractType::Cast(owner)); assembly_stream_.Print("Precompiled_%s:\n", name); } else if (owner.IsFunction()) { //owner为Function,说明是一个常规的dart函数 const char* name = Function::Cast(owner).ToQualifiedCString(); EnsureAssemblerIdentifier(const_cast<char*>(name)); assembly_stream_.Print("Precompiled_%s_%" Pd ":\n", name, i); } else { UNREACHABLE(); } #ifdef DART_PRECOMPILER // 建立一个标签用于DWARF if (!code.IsNull()) { const intptr_t dwarf_index = dwarf_->AddCode(code); assembly_stream_.Print(".Lcode%" Pd ":\n", dwarf_index); } #endif { // 3. 写入 入口点到结束 NoSafepointScope no_safepoint; uword beginning = reinterpret_cast<uword>(insns.raw_ptr()); uword entry = beginning + Instructions::HeaderSize(); uword payload_size = insns.raw()->HeapSize() - insns.HeaderSize(); uword end = entry + payload_size; text_offset += WriteByteSequence(entry, end); } } FrameUnwindEpilogue(); #if defined(TARGET_OS_LINUX) || defined(TARGET_OS_ANDROID) || \ defined(TARGET_OS_FUCHSIA) assembly_stream_.Print(".section .rodata\n"); #elif defined(TARGET_OS_MACOS) || defined(TARGET_OS_MACOS_IOS) assembly_stream_.Print(".const\n"); #else UNIMPLEMENTED(); #endif //写入数据段 const char* data_symbol = vm ? "_kDartVmSnapshotData" : "_kDartIsolateSnapshotData"; assembly_stream_.Print(".globl %s\n", data_symbol); assembly_stream_.Print(".balign %" Pd ", 0\n", OS::kMaxPreferredCodeAlignment); assembly_stream_.Print("%s:\n", data_symbol); uword buffer = reinterpret_cast<uword>(clustered_stream->buffer()); intptr_t length = clustered_stream->bytes_written(); WriteByteSequence(buffer, buffer + length); }
这里是生成的是snapshot_assembly.S,后面在dart代码还将对这个文件加工成App动态库文件,咱们会在下文介绍,咱们要作代码段和数据段分离修改的就是这个c++函数,首先改掉代码不写进snapshot_assembly.S,在另外的地方把二进制数据保存起来。后面经过修改engine的加载流程从外部加载这二进制数据,便可达到分离代码段和数据段的目的。下面咱们继续分析生成完snapshot_assembly.S后,在哪里生成App动态库二进制文件。
生成完snapshot_assembly.S后,再加工关键代码在[-> lib/src/base/build.dart]
/// Builds an iOS or macOS framework at [outputPath]/App.framework from the assembly /// source at [assemblyPath]. Future<RunResult> _buildFramework({ @required DarwinArch appleArch, @required bool isIOS, @required String assemblyPath, @required String outputPath, @required bool bitcode, @required bool quiet }) async { final String targetArch = getNameForDarwinArch(appleArch); if (!quiet) { printStatus('Building App.framework for $targetArch...'); } final List<String> commonBuildOptions = <String>[ '-arch', targetArch, if (isIOS) '-miphoneos-version-min=8.0', ]; const String embedBitcodeArg = '-fembed-bitcode'; final String assemblyO = fs.path.join(outputPath, 'snapshot_assembly.o'); List<String> isysrootArgs; if (isIOS) { final String iPhoneSDKLocation = await xcode.sdkLocation(SdkType.iPhone); if (iPhoneSDKLocation != null) { isysrootArgs = <String>['-isysroot', iPhoneSDKLocation]; } } //生成snapshot_assembly.o二进制文件 final RunResult compileResult = await xcode.cc(<String>[ '-arch', targetArch, if (isysrootArgs != null) ...isysrootArgs, if (bitcode) embedBitcodeArg, '-c', assemblyPath, '-o', assemblyO, ]); if (compileResult.exitCode != 0) { printError('Failed to compile AOT snapshot. Compiler terminated with exit code ${compileResult.exitCode}'); return compileResult; } final String frameworkDir = fs.path.join(outputPath, 'App.framework'); fs.directory(frameworkDir).createSync(recursive: true); final String appLib = fs.path.join(frameworkDir, 'App'); final List<String> linkArgs = <String>[ ...commonBuildOptions, '-dynamiclib', '-Xlinker', '-rpath', '-Xlinker', '@executable_path/Frameworks', '-Xlinker', '-rpath', '-Xlinker', '@loader_path/Frameworks', '-install_name', '@rpath/App.framework/App', if (bitcode) embedBitcodeArg, if (isysrootArgs != null) ...isysrootArgs, '-o', appLib, assemblyO, ]; //打包成动态库 final RunResult linkResult = await xcode.clang(linkArgs); if (linkResult.exitCode != 0) { printError('Failed to link AOT snapshot. Linker terminated with exit code ${compileResult.exitCode}'); } return linkResult; }
这里最终会调用xcrun cc命令和xcrun clang命令打包动态库二进制文件。
根据上面的分析整个流程涉及dart代码和c++代码,dart代码其实不在engine,属于flutter项目,只须要用打开[-> packages/flutter_tools]这个flutter
项目,直接修改就好,要注意一点,flutter_tools的编译产物是有缓存的,缓存路径是[-> bin/cache/flutter_tools.snapshot],每次咱们修改完dart代码,都须要删掉flutter_tools.snapshot从新生成才能生效。
那c++部分代码呢,首先设计c++代码都是须要从新编译flutter engine, 能够参考文章手把手教你编译Flutter engine,编译后engine的产物,以下图
把编译后的gen_snapshot文件拷贝到flutter目录下,下图的位置便可。
注意,engine是分架构的,arm64的gen_snapshot名字是gen_snapshot_arm64,armv7的gen_snapshot名字是gen_snapshot_armv7,完成替换后,咱们定制的代码就能够生效了。
至此,生成动态库文件App的所有流程都介绍清楚了,关键部分就是修改4.1.4提到的c++函数,咱们修改完后的编译产物以下。
提取到了4个文件,分别是arm64和armv7架构下的vm数据段和isolate数据段,能够按需下发给数据段文件给应用,从而实现flutter ios 动态库编译产物的裁剪。
像4.1.1和4.1.2说的那样,具体生成flutter_assets的代码在BundleBuilder.dart文件
[-> packages/flutter_tools/lib/src/bundle.dart]
Future<void> build({ @required TargetPlatform platform, BuildMode buildMode, String mainPath, String manifestPath = defaultManifestPath, String applicationKernelFilePath, String depfilePath, String privateKeyPath = defaultPrivateKeyPath, String assetDirPath, String packagesPath, bool precompiledSnapshot = false, bool reportLicensedPackages = false, bool trackWidgetCreation = false, List<String> extraFrontEndOptions = const <String>[], List<String> extraGenSnapshotOptions = const <String>[], List<String> fileSystemRoots, String fileSystemScheme, }) async { mainPath ??= defaultMainPath; depfilePath ??= defaultDepfilePath; assetDirPath ??= getAssetBuildDirectory(); printStatus("assetDirPath" + assetDirPath); printStatus("mainPath" + mainPath); packagesPath ??= fs.path.absolute(PackageMap.globalPackagesPath); final FlutterProject flutterProject = FlutterProject.current(); await buildWithAssemble( buildMode: buildMode ?? BuildMode.debug, targetPlatform: platform, mainPath: mainPath, flutterProject: flutterProject, outputDir: assetDirPath, depfilePath: depfilePath, precompiled: precompiledSnapshot, trackWidgetCreation: trackWidgetCreation, ); // Work around for flutter_tester placing kernel artifacts in odd places. if (applicationKernelFilePath != null) { final File outputDill = fs.directory(assetDirPath).childFile('kernel_blob.bin'); if (outputDill.existsSync()) { outputDill.copySync(applicationKernelFilePath); } } return; }
这里assetDirPath就是最终打包产生bundle产物的路径,咱们只要修改这个路径,不指向App.framework,指向其余路径,就能够避免打包进app。
至此,咱们已经把AOT编译产物里面的动态库文件App、flutter_assets,的生成流程解析清楚了,也把如何分离的方法介绍了,对咱们的demo作完修改后的产物跟分离前的产物对好比下图所示
分离前
分离后
那下面咱们分析如何修改flutter engine的加载流程,使engine再也不加载App.framework里面的资源(由于已经分离出来),去加载外部给予的资源
上面咱们已经成功从App.framework里面分离出了数据段数据已经flutter_assets,如今须要修改加载流程,加载外部数据。
加载数据段的堆栈以下。
能够看到实际上是用::dlsym从动态库里面读出数据段的数据强转成const uint8_t使用,咱们只要修改代码,不从动态库读取,外部提供一个const uint8_t来代替就行了
我最终选择在下图的两个地方修改
这里我直接构造一个SymbolMapping返回,SymbolMapping的定义以下
class SymbolMapping final : public Mapping { public: SymbolMapping(fml::RefPtr<fml::NativeLibrary> native_library, const char* symbol_name); //新增一个构造函数直接传如外部数据 SymbolMapping(const uint8_t * data); ~SymbolMapping() override; // |Mapping| size_t GetSize() const override; // |Mapping| const uint8_t* GetMapping() const override; private: fml::RefPtr<fml::NativeLibrary> native_library_; const uint8_t* mapping_ = nullptr; FML_DISALLOW_COPY_AND_ASSIGN(SymbolMapping); };
修改了这里,咱们就能够完成外部数据段的加载了。
这个比较简单,咱们直接上代码,
只要改了settings.assets_path,改为外部的路径就行了。
到这里,咱们已经成功分离好engine了,分离以后对于不少混编的项目就是,flutter并非必须的,就能够吧数据段部分和flutter_assets不打包进ipa,按需的下载下来,从而实现ipa的减size,下午会给出编好的engine、gen_snapshot文件和demo。固然,有些业务甚至不但愿下载,想调用流程彻底不变,也能够减size,这个因为篇幅有限,咱们后面再写一篇专门给出方法和工具。
从上面的分析能够看出,搞这个事情,要不少铺垫,很麻烦,不少同窗并不想摸索这么久才能在本身的项目进行实验,看效果,为了方便你们验证,我直接把基于v1.12.13+hotfix.7编好的engine、gen_snapshot文件和demo放到github上,让你们直接用.编出来的Flutter.framework是全架构支持的、通过优化的release版,能够直接上线的。下面介绍下运行流程。
在github上下载demo,不作任何改动,用真机直接运行,能够看到产物以下图所示,App动态库 5.5M,flutter_assets 715k,总大小 6.3M。
而后执行下面的操做,替换engine
能够看到产物以下图所示,只剩下4.6M的产物了,这是demo的压缩效果。
目前使用这方案,能够分离编译产物和flutter_assets,但也须要app作必定的改动,就是从服务器下载数据段和flutter_assets,才能运行flutter。固然还有一个方法,直接对数据段进行压缩,运行的时候解压,这个也是可行的,但压缩率就没这么高,后面咱们也会开源并给出文章介绍。