The slightly larger package size of Flutter in mixed development scenarios has been one of the criticisms, and Google has made it clear that Flutter does not support dynamic behavior, and the Flutter SDK has not yet provided a customized solution. Therefore, if you want to slim down, you can only do it yourself.

The so-called reduction package, the premise is that you have to know what the product content is? What parts of the product can you subtract? How do we add back what’s been subtracted? Therefore, this paper will discuss the principles and schemes of Flutter packet reduction on both sides of iOS and Android respectively around two themes of “product analysis” and “Packet reduction scheme”.

So, let’s start with iOS.

Note: The data and code snippets in this document are derived from a Build of the Flutter Module based on Flutter 1.17.1 in Release (AOT Assembly) Mode without any compression.

1. The iOS

1.1 Product Composition

We know that a Flutter Module can be built into a framework for ios hosts to integrate with by using flutter build ios-Framework. This is called product integration. This “product” is then a Flutter product, which consists of the following components:

  1. App.framework
    • App: This is a product of the Dart business code AOT
    • Flutter_assets: Flutter static resource file
  2. Flutter.framework
    • Flutter: A compilation of the Flutter Engine
    • Icudtl. dat: Internationalization support data files

After the product is printed, the volume of each part can be displayed on the terminal. Finally, the structure of the iOS terminal Flutter product can be adjusted as shown in the figure below:

Note that the volume displayed in Mac Finder is a bit larger, with a conversion rate of 1000 instead of 1024, requiring us to use the command line to retrieve the displayed volume and then manually calculate the actual volume.

In addition, we selected the volume of Engine product under profile mode (ARM64 + ARM32). Due to the bug of Flutter 1.17.1 release, bitcode could not be compressed, resulting in a volume of 351.47MB, which affected the analysis. Flutter app size is too big · Issue #45519.

1.2 Packet reduction Scheme

There are two basic ways to reduce packages:

  1. Delete product: Delete the useless parts of the product directly
  2. Move products: To change the product loading logic of a Flutter to dynamically load parts of the product that can be temporarily removed to be delivered remotely

According to the product structure summarized in the previous article, we realized product package reduction first, the App part of App. Framework.

1.2.1 App. Framework/App

Before we talk about the solution, let’s take a look at how the App under app. framework is built, as shown below:

First, frontend_server will compile the Dart source into an intermediate dILL, which we can also achieve by running the following command:

App. dill is binary bytecode, which we can see from string app.dill is the result of Dart code merging:

And Dart in development mode to provide Hot Reload was through will get the new code changes by frontend_server compiled kernel (app. Dill. Incremental. Dill), After WS submission to Dart VM Update, the entire tree is rebuilt to achieve Hot Reload.

The IL instruction set and optimized code will be compiled through gen_snapshot on both platforms, and the assembly product will be output at last. The assembly product is obtained from the single architecture App product through xcRUN tool, and the final dual-ARM architecture App product is obtained through LIPo. So, the size of the App under the App. Framework we’re showing here is biarchitectural.

ARMv7: iOS devices before the iPhone 5S. ARM64: iPhone 5S and later iOS devices.

Then, we explain how to reduce the volume of the product from the two levels of delete product and remove product.

Delete the product

The volume of this part is the product after AOT of Dart code. It has a large volume and is the focus of our package reduction process.

Following the basic method of packet reduction described earlier, let’s first try “Delete products” to see what can be deleted directly. Using the volume analysis tools provided by Flutter, we can get a volume diagram directly:

We found that there were two libraries that were not used in the business, so we simply deleted the dependencies.

There are also some optimizations that can help reduce code size:

  • Configure linter to prevent improper syntax, such as display type conversions, and large try catches appended to code before compilation.
  • Obfuscated Dart code: 0.75MB (2.5%) ↓

In addition, we can remove some symbols to achieve packet reduction effect

  • No stack trace symbol: 1.8MB (6.2%) ↓
  • Delete the dSYM symbol table information file: 5.8MB (20%) ↓

Note: dSYM is a transfer file that stores the address mapping information of hexadecimal functions, including symbols we debugged, which is used to analyze the crash report file and parse out the correct error function information.

Noah product

Next, we’ll look at how to implement the “move product”, which requires a detailed analysis of the content in app. framework/App. We said it was an afterthought of Dart code AOT, and that’s right, because it consists of four snapshot libraries:

  • KDartIsolateSnapshotData: Isolate the snapshot data, this is the initial state, the Dart pile and contain Isolate their own information.
  • KDartIsolateSnapshotInstructions: Isolate snapshot commands, including by Dart of Isolate AOT directive.
  • KDartVmSnapshotData: Dart VM snapshot data and initial status of the Dart heap shared between the isolate.
  • KDartVmSnapshotInstructions: the Dart VM snapshot command, containing all the Dart VM isolate Shared between the general routine of AOT instructions.

Details can be found in the official Wiki: github.com/flutter/flu…

There can be many ISOLates in the same process, but the heaps of two isolates cannot be shared. The Dart VM development team had this interaction in mind for a long time and designed a VM Isolate, which acts as a bridge for interaction between isolates running in UI threads. The following figure shows the relationship between the ISOLATES in the Dart VM:

Therefore, the AOT Snapshot corresponding to the ISOLATE is kDartIsolateSnapshot, which is divided into command segment and data segment. AOT Snapshot corresponding to VM Isolate is kDartVmSnapshot, which is also divided into command segment and data segment.

Based on the above analysis, the structure of app. framework can be further divided into the following figure:

We know that App Store approval regulations do not allow dynamic delivery of executable binaries, so for the above 4 snapshots, we can only deliver the contents of data segments (kDartIsolateSnapshotData and kDartVmSnapshotData). And instruction content (kDartIsolateSnapshotInstructions and kDartVmSnapshotInstructions) is still to remain on the product.

So where do we detach this snapshot library?

During the data loading phase of the Dart VM startup, change the snapshot library read path in Settings, as shown in the following figure:

The specific implementation of the modified Flutter package clipping Scheme (iOS) is not explained in this paper. The code modification is described in detail in the article Q Sound Live Flutter Package Clipping Scheme.

1.2.2 App.framework/flutter_assets

Flutter_assets are local static resources used by the Flutter Module. We cannot delete these assets but “move” them. There are two options for removing artifacts — the usual option is to modify the Flutter_assets path in Settings during the data load phase of the Dart VM startup to enable remote loading, which is normally used to remove Flutter_assets.

Is there any way to remove Flutter_assets without modifying the Flutter Engine code? Yes, you can use the combination of CDN image + disk caching + preloading to achieve the same effect, the steps are as follows:

  1. Encapsulate an Image component, and choose to use local graph or network graph according to compilation mode. That is, local graph is used for rapid development in the development environment, and CDN graph is used in the production environment.
  2. CI was reformed to remove the pictures in flutter_assets concurrent cloth package to CDN during continuous integration.
  3. The extension enhances the Image component by introducing cached_network_image to support disk caching.
  4. When the Flutter module is loaded, useprecacheImageMethods CDN images were preloaded.

This solution is a bit cumbersome and context-sensitive, so it is recommended that the Flutter Engine be modified to remotely load Flutter_assets.

1.2.3 Flutter. The framework/icudtl. Dat

Icudtl. dat is an international support data file. It is not recommended to delete it directly, but to modify the icudtl.dat path (ICu_data_path) in Settings during data loading during Dart VM startup to implement remote loading:

1. Flutter. Framework/Flutter

The engine changes

This part is the compiled binaries of the Flutter Engine (C++), and is the largest part of the product by volume. Currently we refer to bytedance’s “how to reduce the size of the Flutter package by nearly 50%”. There are two optimizations that can be made:

  1. Compiler optimization
  2. The engine cut out

The Flutter Engine is compiled using LLVM, where link-time Optimization (LTO) has a Clang Optimization Level compilation parameter as shown below (in Buildroot) :

We changed the Engine compilation parameter for iOS from -OS to -oz, which resulted in a 700KB reduction.

Engine tailoring also has two parts:

  1. Skia: Remove some parameters to reduce the size of 200KB without affecting performance.
  2. BoringSSL: If client proxy requests are used, the Dart HttpClient module is not required. This section can be completely removed and the proxy requests will perform better than HttpClient. This section can be reduced by 500KB

Add: in github.com/flutter/flu… Another aspect of compiler optimization, function compiler optimization, is mentioned in the. For the same addition function, the Dart implementation has 36 instructions after compilation, while objective-C only has 11 instructions. Among the 36 instructions, there are 8 head and 6 tail alignment instructions that can be removed, as well as 5 middle stack overflow checks that can be removed. ** The 36 instructions compiled for Dart can be optimized into 13 instructions. ** Here need to wait for Google official to optimize.

Engine compiles

The tools used to build the Flutter Engine are described below:

  • Gclient: source code library management tool, originally used by Chromium, it can manage source code and the corresponding dependencies, through gClient to obtain all source code and dependencies required by compilation.
  • Gn: Responsible for generating ninja build files required for ninja compilation, especially for Flutter, which spans multiple operating system platforms and multiple CPU architectures, gn will need to generate many different ninja build files.
  • Ninja: Compilation tool, responsible for the final compilation.

Setting up the Engine development environment – Flutter Wiki

The specific compilation is divided into SA. First create a.gclient file to pull the source code and all corresponding dependencies, as shown in the following figure:

Second, execute gClient sync to download the dependency.

Note that all of the above modifications are dependencies (such as buildroot, Skia, etc.) rather than source code. Therefore, we need to fork a Flutter engine. After modifying the dependencies, we need to obtain the commit number of the dependencies and fill it into the ENGINE DEPS file. After committing the code, get the latest commit number from the engine repository and fill it in the.gclient file.

The third step is to compile the engine using a configuration file generated by NINJA and gn. For any platform you want to build engine, use GN to generate a configuration file and ninja will execute the build. As shown below:

As a result, we have several custom Engines (for different platform architectures) that can be used simply by replacing the engines in the native Flutter SDK.

After the above steps of package reduction for each product content, our final product architecture is shown in the figure below:

1.3 Effect of package reduction

There are several ways to check the volume of iOS App, and the sizes are different:

The first way is to look at the analysis report after the ipA is built locally. The analysis report provides two volumes, but note that they are not encrypted:

  1. Installation package volume: i.e. unencrypted, download size
  2. Volume after decompression: that is, unencrypted volume occupied

However, after uploading the App Store, it will be encrypted. Therefore, if you want to know the volume that users finally see, you need to upload the App Store and check the report. The report here also provides two volumes, as shown below:

Respectively is:

  1. Download Size
  2. Install Size

The last thing users see in the App Store is the Install Size.

Note: One exception is when you log into the App Store using a Web browser to see the Size of your App. The Download Size is displayed at that time, because Apple doesn’t think you’re interested in the Size of your App.

We uploaded the App Store using the blank project as the host project to check the Install Size and found that the Size of the App decreased from 18.7MB to 11.8MB.

2. The Android

On the Android side, the package reduction solution is relatively simple, because there are no restrictions on the App Store’s approval rules, you can roughly remove all products and distribute them dynamically. We still look at Flutter package reduction on Android side from product composition, package reduction scheme and package reduction effect.

2.1 Product Composition

The build process of the Flutter Module (Release) on Android is the same as that on iOS. Dart source code and Engine are also included:

The final product, Flutter. Gradle, contains:

  1. libapp.so
  2. flutter.jar

Jar contains libflutter. So, icudtl.dat, and some Java files. Libflutter. Finally, some Java is exposed to the business side to invoke Flutter.

Then the key product composition is shown in the following table:

2.2 Packet reduction Scheme

Libflutter. So is an engine product. We can still tailor it, but it is no longer necessary because Flutter products can be delivered dynamically on Android. The steps are as follows:

  1. Remove libapp.so, libflutter. So, flutter_assets and publish them to the cloud
  2. Dynamic loading is achieved by customizing the flutterLoader. Java logic in flutter. Jar to load the library path of the custom location

The concrete code is no longer demonstrated.

2.3 Effect of package reduction

Measuring the volume of APK before and after package reduction using blank engineering as host, it was found that the 6.2MB volume of Flutter products could be completely subtracted.

The above is the two-terminal Flutter package reduction scheme. The content is relatively simple and the effect is obtained step by step by referring to the footsteps of predecessors. Therefore, readers are strongly advised to read the two articles at the end of the following for further study to deepen their understanding.

Reference article:

  1. Q Sound Live Flutter Package Clipping Scheme (iOS)
  2. How to Reduce the Size of a Flutter Bag by nearly 50%