1. Why write this article?

The sizing of the Flutter ios installation package has been a topic of much attention since bytedance shared an article about the fit a few years ago (juejin.cn/post/684490…). , it mentioned that ios separates AOT compilation products and extracts data segments and resources from them to reduce the size of the installation package. However, the article did not introduce how to achieve this. This article will analyze how to separate AOT compilation products in detail. Tools are provided for those who have no experience compiling the Flutter Engine to quickly implement this function.

2. Composition of ios compilation products

This paper mainly analyzes the generation process of App.framework and how to separate AOT compilation products. The composition of app. framework is shown in the figure below.

The App dynamic library binaries are mainly composed of App dynamic library binaries, Flutter_assets and info-plist. The App dynamic library binaries are composed of four parts: VM data segment and code segment, and ISOLATE data segment and code segment. Among them, flutter_assets, VM data segment and ISOLATE data segment can not be packed into IPA, but can be loaded from external document, which makes it possible to reduce IPA package.

3. Comparison of AOT compilation products of real online projects before and after

A lot of people are certainly concerned about the effect of the eventual reduction. Let’s start with a real online project with a comparison of the official compiled engine and the app. framework generated by a separate engine.

The App. Framework generated by the official engine consists of 19.2m binary App dynamic library files and 3.3m Flutter_assets, a total of 22.5m.

The App. Framework generated from the engine of the split product is as follows, leaving only the App dynamic library binaries of 14.8m.

App. Framework sizes from 22.5 to 14.8 meters, depending on the project.

4. AOT compilation product generation principle and separation method are introduced

The script xcode_backend.sh will be run to package the flutter products before each Xcode project is built. We start from xcode_backend.sh. There are three files in app. framework that generate binary App, flutter_assets and info.plist. We only care how binary App and Flutter_assets are generated.

4.1 App file generation process

4.4.1, xcode_backend. Sh

By analyzing xcode_backend.sh, we can find the following key shell codes for generating App and Flutter_assets


#App dynamic library binaries
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}"


#Generate 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}
Copy the code

4.1.2, ${FLUTTER_ROOT} / bin/flutter

A shell script called /bin/flutter is used to execute the flutter command. The script that actually runs the flutter command is /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 "// True execution logic "$DART" $FLUTTER_TOOL_ARGS "$SNAPSHOT_PATH" "$@" // equivalent to /bin/cache/dart-sdk/bin/ DART $FLUTTER_TOOL_ARGS "bin/cache/flutter_tools.snapshot" "$@"Copy the code

That is, run the flutter_tools.snapshot product with the DART command

4.1.3 Dart code

The entry to flutter_tools.snapshot is

[-> flutter/packages/flutter_tools/bin/flutter_tools.dart]

import 'package:flutter_tools/executable.dart' as executable;

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

Copy the code
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(),
     });
}

Copy the code

After a round of calls, the actual compiled classes are in gensnapshot. run, on the call stack gityuan.com/2019/09/07/… This article has a detailed introduction, but I won’t go into details here

[-> 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); Final String snapshotterPath = getSnapshotterPath(snapshotType); // Obtain the path of the gen_snapshot command. //iOS gen_snapshot is a multi-architecture binary file. Running as an I386 binary generates ARMV7 code. Running as an X86_64 binary generates arm64 code. // /usr/bin/arch can be used to run binaries with a specified architectureif (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));
  }
}

Copy the code

According to the previous encapsulation, the gensnapshot. run command is equivalent to:

/ / this is targeted at iOS genSnapshot command/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.dillCopy the code

Gen_snapshot here is a binary executable file, the execution method of the source code for third_party/dart/runtime/bin/gen_snapshot. Cc this file is flutter engine inside the file, You need to pull the code of the engine to modify the build of the flutter engine. You can refer to the article to guide you to build the Flutter engine by hand. We will also describe how to get the binary files compiled by gen_snapshot after compiling the Flutter engine.

4.1.4, flutter engine c++ code

Flutter machine code generating gen_snapshot of gen_snapshot process this article made a detailed analysis, here I give the final conclusion, directly generate the data and code snippet of code in AssemblyImageWriter: : WriteText inside this function

[-> third_party/dart/runtime/vm/image_snapshot.cc]


void AssemblyImageWriter::WriteText(WriteStream* clustered_stream, bool vm) {
  Zone* zone = Thread::Current()->zone();
  // Write the header
  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);

  // Write header whitespace to make the instruction snapshot look like a heap page
  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);
  }

  // Write the prologue. 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 boolis_trampoline = data.trampoline_bytes ! =nullptr;
    if (is_trampoline) {     // For jumpers
      const auto start = reinterpret_cast<uword>(data.trampoline_bytes);
      const auto end = start + data.trampline_length;
       // Write the. Quad XXX string
      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_;
    Write the header to the entry point
    {
      NoSafepointScope no_safepoint;

      uword beginning = reinterpret_cast<uword>(insns.raw_ptr());
      uword entry = beginning + Instructions::HeaderSize(); //ARM64 32-bit alignment

      // The read-only tag of the directive
      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);
      // Write the flag
      WriteWordLiteralText(marked_tags);
      beginning += sizeof(uword);
      text_offset += sizeof(uword);
      text_offset += WriteByteSequence(beginning, entry);
    }

    // 2. Write the label at the entry point
    owner = code.owner();
    if (owner.IsNull()) {  
      // If the owner is empty, it is a regular stub, where the stub list is defined in stub_code_list.h with 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 specific stub code [see Section 3.5.1]
          name = NameOfStubIsolateSpecificStub(object_store, code);
        }
        assembly_stream_.Print("Precompiled__%s:\n", name); }}else if (owner.IsClass()) {
      // If the owner is Class, it is a stub assigned to the Class. The Class list is defined in CLASS_LIST_NO_OBJECT_NOR_STRING_NOR_ARRAY in class_id
      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()) { // The owner is Function, indicating a regular DART Function
      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
    // Create a label for DWARF
    if(! code.IsNull()) {const intptr_t dwarf_index = dwarf_->AddCode(code);
      assembly_stream_.Print(".Lcode%" Pd ":\n", dwarf_index);
    }
#endif

    {
      // 3. Write entry point to end
      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
  // Write the data segment
  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);
}

Copy the code

Snapshot_assembler.S is an App dynamic library file that is generated in the dart code. This is the c++ function that separates the code from the data. Save binary data somewhere else. You can then separate the code and data segments by modifying the engine’s loading process to load the binary data externally. Let’s continue our analysis of where to generate the App dynamic library binary after generating snapshot_assembly.s.

Dart code calls XCRun to generate binaries and dynamic libraries

**[-> 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]; }} 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;
  }

Copy the code

This is where the xcrun cc command and xcrun clang command are eventually called to package the dynamic library binaries.

4.1.6 Modify the process of generating dynamic library file App

According to the above analysis, the whole process involves dart code and c++ code. Dart code actually does not belong to the engine, but belongs to the flutter project. You only need to open **[-> packages/flutter_tools] to modify the flutter project directly. The compiled products of Flutter_tools are cached, and the cache path is [-> bin/cache/flutter_tools.snapshot]**. Every time we modify the Dart code, we need to delete flutter_tools.snapshot and regenerate it to take effect.

How about the c++ code? First of all, the design of c++ code requires recompiling the flutter engine. Please refer to the article to teach you how to compile the flutter engine

Copy the compiled gen_snapshot file to the flutter directory as shown in the following image.

Note that engine is architect-specific, the gen_snapshot name for arm64 is gen_snapshot_arm64, and the gen_snapshot name for armv7 is gen_snapshot_armv7.

4.1.7 Summary of App process for generating dynamic library files

The key part is to modify the c++ function mentioned in 4.1.4. The compiled product after modification is as follows.

Four files were extracted, namely VM data segments under ARM64 and ISOLATE data segments under ARMV7 architecture, which could be sent to applications as needed, thus realizing the tailoring of flutter ios dynamic library compilation products.

4.2 Flutter_Assets generation process

As described in 4.1.1 and 4.1.2, the code that specifically generates Flutter_assets is in the BundleBuilder.dart file

[-> 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 aroundfor 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;
  }

Copy the code

In this case, assetDirPath is the path where the bundle is eventually packaged. We can change this path to point to another path instead of to app. framework to avoid packaging into App.

4.3 Summary of AOT compilation product generation principle

So far, we have clearly analyzed the generation process of App and Flutter_assets, the dynamic library files in AOT compilation product, and also introduced the separation method. The product after modifying our demo is compared with the product before separation, as shown in the following figure

Before the separation of

After the separation

How to modify the load process of the Flutter Engine so that the engine does not load resources in the App. Framework (because they have been separated from the engine) instead of external resources

5. AOT compilation product loading process and modification method are introduced

We have successfully separated the data from app. framework and flutter_assets. Now we need to modify the loading process to load the external data.

5.1 Data section loading process analysis and modification

The stack for loading the data segment is as follows.

Uint8_t = uint8_t = uint8_t = uint8_t = uint8_t

I finally chose to change it in two places in the figure below

Here, I’ll just construct a SymbolMapping and return it. SymbolMapping is defined as follows


class SymbolMapping final : public Mapping {
 public:
  SymbolMapping(fml::RefPtr<fml::NativeLibrary> native_library,
                const char* symbol_name);
                
  // Add a new constructor to pass directly as external data
  SymbolMapping(const uint8_t * data);

  ~SymbolMapping() override;

  // |Mapping|
  size_t GetSize() const override;

  // |Mapping|
  const uint8_t* GetMapping(a) const override;

 private:
  fml::RefPtr<fml::NativeLibrary> native_library_;
  const uint8_t* mapping_ = nullptr;

  FML_DISALLOW_COPY_AND_ASSIGN(SymbolMapping);
};

Copy the code

With this modification, we are ready to load the external data segment.

5.2 Analysis and modification of flutter_Assets loading process

This is a little bit easier, so let’s just go to the code,

Just change settings.assets_path to an external path.

5.3. Modify engine summary

By now, we have successfully separated engine. After separation, for many mixed projects, flutter is not necessary, so we can download the data segment and flutter_assets as needed without packing them into IPA, thus reducing the size of IPA. The engine, gen_snapshot files and demo will be presented in the afternoon. Of course, some businesses do not even want to download, want to call the process completely unchanged, you can also reduce the size, due to the lack of space, we will write a special method and tools later.

6. Tool introduction and use

From the above analysis, we can see that it takes a lot of preparation to do this, which is very troublesome. Many students do not want to explore for such a long time before conducting experiments in their own projects. To see the results, for the convenience of verification, The engine, gen_snapshot, and demo files created based on v1.12.13+hotfix.7 are available on Github. The resulting Flutter. Framework is a fully architect-supported, optimized release that can be directly launched. The following describes the operation process.

6.1 How Do I Verify Demo

Download the demo on Github and run it directly on the real computer without any changes. You can see the product as follows: App dynamic library 5.5m, Flutter_assets 715K, with a total size of 6.3m.

Then do the following to replace the Engine

  • Flutter on the lot. The framework covers off [- > / bin/cache/artifacts/engine/ios release/Flutter. The framework] this now Flutter. The framework

  • Drop on making gen_snapshot_arm64 covering [- > / bin/cache/artifacts/engine/ios release/gen_snapshot_arm64]

  • Drop on making gen_snapshot_armv7 covering [- > / bin/cache/artifacts/engine/ios release/gen_snapshot_armv7]

  • Then put the bundle on the lot. The dart overwrite [- > packages/flutter_tools/lib/SRC/bundle. The dart] bundle in the directory. The dart files

  • Then delete [->bin/cache/flutter_tools.snapshot], which is the binary generated by the DART project. Dart will only take effect if you delete the new bundle.dart

  • Then re-run the project and observe the compiled product

As you can see in the image below, there are only 4.6m products left. This is the demo compression.

7,

Currently, this solution can separate compiled products from Flutter_assets, but the app also needs to make some changes, that is, download the data segment and Flutter_assets from the server to run FLUTTER. Of course, there is another method, directly compress the data segment, decompress at run time, this is also feasible, but the compression rate is not so high, we will open source later and give an article.