Airing

Airing

哲学系学生 / 小学教师 / 程序员,个人网站: ursb.me
github
email
zhihu
medium
tg_channel
twitter_id

Flutter Product Analysis and Package Reduction Solutions

In the hybrid development scenario, the slightly large package increment of Flutter has always been criticized by everyone. However, Google has clearly stated that Flutter will not support dynamic loading, and currently, the official Flutter SDK does not provide a customized solution. Therefore, if you want to slim down, you can only do it yourself.

The so-called package reduction requires knowing what the product contains. What parts of the product can be reduced? How do we add back the parts that are removed? Therefore, this article will discuss the principles and solutions for Flutter package reduction on both iOS and Android platforms, focusing on "product analysis" and "package reduction solutions."

Let's start with the iOS side.

Note: The data and code snippets in this article are sourced from a Flutter Module based on Flutter 1.17.1, built in Release (AOT Assembly) Mode, without any compression.

1. iOS Section#

1.1 Product Composition#

We know that using flutter build ios-framework can build a Flutter Module into a Framework for iOS host integration. This integration method is referred to as product integration, and this "product" is the Flutter product, which consists of the following parts:

  1. App.framework
    • App: This is the AOT product of Dart business code
    • flutter_assets: Flutter static resource files
  2. Flutter.framework
    • Flutter: The compiled product of the Flutter Engine
    • icudtl.dat: Internationalization support data file

After generating the product, we can display the size of each part in the terminal. Finally, the structure of the iOS Flutter product is summarized in the following diagram:

image

It is important to note that the size displayed in Mac Finder may be larger, as the conversion factor is 1000 instead of 1024. We need to use the command line to obtain the displayed size and then manually calculate the actual size.

Additionally, the size of the Engine product we chose is based on the profile mode (arm64+arm32). Due to a bug in Flutter 1.17.1 release, bitcode cannot be compressed, resulting in a size of 351.47 MB, which affects the analysis. For specific reasons, see: Flutter app size is too big · Issue #45519.

1.2 Package Reduction Solutions#

There are two basic methods for package reduction:

  1. Remove products: Directly delete the unused parts from the product.
  2. Move products: Temporarily remove parts that can be moved to be delivered remotely, while modifying the product loading logic to allow Flutter to support dynamically loading parts delivered remotely.

We will implement product package reduction based on the product structure summarized earlier, starting with the App part in App.framework.

1.2.1 App.framework/App#

Before discussing the solution, let's first look at how the App in App.framework is constructed, as shown in the following diagram:

image

First, the frontend_server compiles the Dart source code into an intermediate product dill. We can achieve the same compilation effect by running the following command:

image

app.dill is the binary bytecode, and by using string app.dill, we can find that it is actually the product of merged Dart code:

image

The Hot Reload provided by Dart in development mode is achieved by compiling the changed code through frontend_server to obtain the new kernel (app.dill.incremental.dill), which is then submitted to Dart VM Update via WS for a complete tree rebuild, thus achieving Hot Reload.

image

Then, it will be compiled through gen_snapshot on both platform sides to obtain the IL instruction set and optimized code, finally outputting the assembly product. The assembly product is obtained through the xcrun tool to get a single-architecture App product, and finally, through lipo, we get the final dual ARM architecture App product. Therefore, the size of the App under App.framework we are displaying is for dual architecture.

ARMv7: iOS devices before iPhone 5s.
ARM64: iOS devices from iPhone 5s onwards.

Next, we will explain how to reduce the size of this product from the perspectives of removing and moving products.

Remove Products#

image

This part's size is the product of Dart code after AOT, which is relatively large and is a key focus during our package reduction process.

According to the previously mentioned basic methods of package reduction, let's first try "removing products" to see what can be directly deleted. Using the size analysis tool provided by Flutter, we can directly obtain the size chart:

image

We find that there are indeed two libraries that are not used in the business, and we can directly delete the dependencies.

In addition, there are some optimizations that can help us reduce code size:

  • Configure linter to prohibit unreasonable syntax: such as explicit type conversions, which can add a large number of try-catch statements before compilation, leading to increased code size.
  • Obfuscate Dart code: 0.75MB (2.5%) ↓

Furthermore, we can also delete some symbols to achieve package reduction:

  • Do not use stack trace symbols: 1.8MB (6.2%) ↓
  • Remove dSYM symbol table information file: 5.8MB (20%) ↓

Note: dSYM is a transit file that saves hexadecimal function address mapping information, containing the symbols we debug, used to analyze crash report files and parse out the correct error function information.

Move Products#

Next, let's see how to implement "moving products," which requires a specific analysis of the contents of App.framework/App. We previously mentioned that it is the product of Dart code after AOT, which is correct because it mainly consists of four AOT snapshot libraries (snapshots):

  • kDartIsolateSnapshotData: Isolate snapshot data, which is the initial state of the Dart heap and contains isolate-specific information.
  • kDartIsolateSnapshotInstructions: Isolate snapshot instructions, containing AOT instructions executed by Dart isolates.
  • kDartVmSnapshotData: Dart VM snapshot data, the initial state of the Dart heap shared between isolates.
  • kDartVmSnapshotInstructions: Dart VM snapshot instructions, containing AOT instructions for common routines shared between all Dart isolates in the VM.

For details, see the official Wiki introduction: https://github.com/flutter/flutter/wiki/Flutter-engine-operation-in-AOT-Mode

There can be many Isolates in the same process, but the heaps of two Isolates cannot be shared. The Dart VM development team has long considered the interaction issue, so they designed a VM Isolate, which serves as a bridge for interaction between Isolates running on the UI thread. The relationship between isolates in the Dart VM is shown in the following diagram:

image

Therefore, the AOT Snapshot corresponding to the isolate is kDartIsolateSnapshot, which is further divided into instruction and data segments; the AOT Snapshot corresponding to the VM Isolate is kDartVmSnapshot, which is also divided into instruction and data segments.

Based on the above analysis, we can further break down the structure of App.framework as shown in the following diagram:

image

We know that App Store review guidelines do not allow dynamically delivered executable binary code, so for the above four snapshots, we can only deliver the content of the data segments (kDartIsolateSnapshotData and kDartVmSnapshotData), while the content of the instruction segments (kDartIsolateSnapshotInstructions and kDartVmSnapshotInstructions) must remain in the product.

So, where do we separate this snapshot library?

During the data loading phase when the Dart VM starts, as shown in the following diagram, we can modify the reading path of the snapshot library in the settings:

image

The specific implementation after modification will not be explained in this article. A detailed introduction to the code modifications can be found in “Q Sound Live Flutter Package Trimming Solution (iOS)”.

1.2.2 App.framework/flutter_assets#

image

flutter_assets are the local static resources used in the Flutter Module. For this part, we cannot "delete" it, only "move" it. We have two solutions to move products—the conventional solution is still to modify the flutter_assets path in the settings during the data loading phase when the Dart VM starts to achieve remote loading. In normal circumstances, we can use this method to remove flutter_assets.

Is there a way to remove flutter_assets without modifying the Flutter Engine code? Yes, we can use a combination of CDN images + disk caching + preloading to achieve the same effect. The steps are as follows:

  1. Encapsulate an Image component that selects between local and network images based on the compilation mode, using local images for rapid development in the development environment and CDN images for the production environment.
  2. Modify CI to remove flutter_assets during continuous integration and publish the images within the package to the CDN.
  3. Extend the capabilities of the Image component by introducing cached_network_image to support disk caching.
  4. When the Flutter module loads, use the precacheImage method to preload CDN images.

This solution is somewhat cumbersome and requires distinguishing between environments, so it is still recommended to modify the Flutter Engine to achieve remote loading of flutter_assets.

1.2.3 Flutter.framework/icudtl.dat#

image

icudtl.dat is the internationalization support data file. It is not recommended to delete it directly; instead, like the above move products solution, modify the icudtl.dat path (icu_data_path) in the settings during the data loading phase when the Dart VM starts to achieve remote loading:

image

1.2.4 Flutter.framework/Flutter#

image

Engine Modification#

This part is the compiled binary product of the Flutter Engine (C++), which occupies the largest part of the product. Currently, based on ByteDance's sharing “How to Reduce Flutter Package Size by Nearly 50%”, the optimizable parts are as follows:

  1. Compilation Optimization
  2. Engine Trimming

The Flutter Engine uses LLVM for compilation, and there is a Clang Optimization Level compilation parameter for link-time optimization (LTO), as shown in the following diagram (in buildroot):

image

We will change the iOS platform's Engine compilation parameter from -Os to -Oz, which can ultimately reduce the size by about 700 KB.

image

Additionally, there are two parts of the engine that can be trimmed:

  1. Skia: Remove some parameters, which can reduce the size by 200KB without affecting performance.
  2. BoringSSL: If using client proxy requests, the Dart HttpClient module is not needed, and this part can be completely removed, with proxy requests performing better than HttpClient, which can reduce the size by 500KB.

Note: Another aspect of compilation optimization, namely function compilation optimization, is mentioned in https://github.com/flutter/flutter/issues/40345. For the same addition function, the Dart implementation has 36 instructions after compilation, while Objective-C has only 11 instructions. Among the 36 instructions, there are 8 alignment instructions at the beginning and 6 at the end that can be removed, and 5 stack overflow checks in the middle can also be removed, meaning the 36 instructions compiled by Dart can be optimized to 13 instructions. This requires optimization from Google.

Engine Compilation#

After making the modifications, we need to compile the engine. First, let's introduce the tools needed for compiling the Flutter Engine:

  • gclient: A source code management tool originally used by Chromium, it can manage source code and corresponding dependencies. We use gclient to obtain all the source code and dependencies needed for compilation.
  • gn: Responsible for generating the build files needed for ninja compilation. Especially for Flutter, which spans multiple operating systems and CPU architectures, gn generates many different ninja build files.
  • ninja: The compilation tool responsible for the final compilation work.

For a detailed introduction to the compilation tools, see the Flutter official Wiki: Setting up the Engine development environment - Flutter wiki

The specific compilation process consists of three steps. First, create a .gclient file to pull the source code and all corresponding dependencies, as shown in the following diagram:

image

The second step is to execute gclient sync to download the dependencies.

It is important to note that the above modifications are dependencies (such as buildroot, skia, etc.) rather than source code. Therefore, we need to fork a copy of the Flutter engine, make the necessary changes to the dependencies, obtain the corresponding commit numbers, and fill them into the engine's DEPS file. After submitting the code, we get the latest commit number of the engine repository and fill it into the .gclient file.

The third step is to use ninja along with the configuration files generated by gn to compile the engine. To compile the engine for a specific platform architecture, generate a configuration using gn, and then execute the compilation with ninja. As shown in the following diagram:

image

Ultimately, we will obtain several customized Engines (for different platform architectures), and using them is straightforward—just replace the Engine in the local Flutter SDK.

After the above steps for package reduction processing on various product contents, our final product structure is shown in the following diagram:

image

1.3 Package Reduction Effect#

The size of the iOS App can be checked in several ways, and the sizes obtained are different:

The first method is to check the analysis report after building the local ipa, which provides two sizes, but it is important to note that both are unencrypted:

  1. Installation package size: the unencrypted download size.
  2. Unzipped size: the unencrypted occupied size.

However, after uploading to the App Store, everything will be encrypted. Therefore, to know the size that users ultimately see, we need to upload to the App Store and check the report, which will also provide two sizes, as shown in the following diagram:

image

They are:

  1. Download Size
  2. Install Size

The size that users ultimately see in the App Store is the Install Size.

Note: There is one exception, which is when using a web browser to log into the App Store to check the App size; at that time, the displayed size is the Download Size because Apple believes that you are not currently concerned about the installation occupied size.

Using a blank project as the host project uploaded to the App Store to check the Install Size, we found that the App size decreased from 18.7MB to 11.8MB.

2. Android Section#

The package reduction solution on the Android side is relatively simple because there are no App Store review guideline restrictions, allowing for a more straightforward removal of all products and dynamic delivery. We will again look at the product composition, package reduction solutions, and package reduction effects for the Android side of Flutter.

2.1 Product Composition#

First, let's look at the compilation process of the Android side Flutter Module (Release). Like iOS, it consists of Dart source code and Engine two parts:

image

The final product flutter.gradle includes:

  1. libapp.so
  2. flutter.jar

Among them, flutter.jar contains libflutter.so, icudtl.dat, and some Java files, while libflutter.so is the engine product, and icudtl.dat is still the internationalization support file. The final Java files expose the interfaces for business-side calls to Flutter.

The key product composition is summarized in the following table:

image

2.2 Package Reduction Solutions#

libflutter.so is the engine product, and we can still customize trimming, but the necessity is not as great because Flutter products on the Android side can be completely dynamically delivered. The steps are as follows:

  1. Move libapp.so, libflutter.so, flutter_assets, etc., to the cloud.
  2. Customize the logic in FlutterLoader.java within flutter.jar to load the libraries from a custom location, thus achieving dynamic loading.

Specific code demonstrations will not be provided.

2.3 Package Reduction Effect#

Using a blank project as the host, we measured the size of the APK before and after package reduction, finding that the 6.2MB Flutter product size could be completely eliminated.

image

This concludes the Flutter package reduction solutions for both platforms. The content is relatively simple, and the effects were achieved step by step by following in the footsteps of predecessors. Therefore, readers are strongly encouraged to extend their reading to the two articles at the end for further learning and deeper understanding.

Reference Articles:

  1. “Q Sound Live Flutter Package Trimming Solution (iOS)”
  2. “How to Reduce Flutter Package Size by Nearly 50%”
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.