An In-Depth Understanding of Flutter Compilation Principles and Optimizations
By Zhengwu, from Xianyu Technology Team
There are always many questions for developers. What is Flutter? What language does it use for programming? What parts are included? How is it compiled and run on the device? How does Flutter achieve quick Hot Reload in Debug mode and native experience in Release mode? What is the difference between the Flutter project and the Android/iOS project? How is their relationship? How is Flutter embedded into Android/iOS? How does Flutter’s rendering and event delivery mechanism work? Does Flutter support hot updates? Is it true that Flutter does not officially provide ARMv7 support for iOS? How can you fix the bug in the engine when using Flutter? How can you locate, modify, and make it take effect if the construction fails?
The answers to all these questions lie in the global observation on the whole process of Flutter, including design, development, construction, and final operation. This article introduces the relevant principles, customization, and optimization of Flutter using the ‘hello_flutter” project as an example.
Introduction to Flutter
The Flutter architecture is mainly divided into three layers: Framework, Engine, and Embedder.
Based on Dart, the Framework includes Material Design-style and Cupertino-style (for iOS) Widgets, text/image/button and other basic Widgets, rendering, animation, gestures, and other components. The core code of this layer lies in the Flutter package in the Flutter repository, and io, async, ui, and other packages in the sky_engine repository. The dart:ui repository provides interfaces between the Flutter framework and the engine.
Based on C++, the Engine mainly includes Skia, Dart, and Text. Skia is an open-source two-dimensional graphics library that provides general APIs for a variety of software and hardware platforms. It has served as a graphics engine for Google Chrome, Chrome OS, Android, Mozilla Firefox, Firefox OS, and many other products. Platforms, including Windows 7+, macOS 10.10.5+, iOS 8+, Android 4.1+, and Ubuntu 14.04+, are also supported. Dart mainly includes Dart Runtime, Garbage Collection (GC), as well as Just In Time (JIT) in Debug mode. In Release and Profile modes, the Ahead Of Time (AOT) compilation compiles code into native ARM code, not JIT compilation. Text refers to text rendering, which acts on libtxt library (for font selection and lines separation) derived from Minikin. HartBuzz is used for glyph selection and molding. Skia, as the rendering/GPU backend, uses FreeType on Android and Fuchsia and CoreGraphics on iOS to render fonts.
The Embedder is an embedding layer that embeds Flutter into various platforms. Its tasks mainly include rendering surface settings, thread settings, and plug-ins. Platforms, such as iOS, only provide a “canvas” with a low platform-related layer of Flutter. All the remaining rendering-related operations occur inside Flutter. Thus, Flutter has excellent cross-terminal consistency.
Flutter Project Structure
This article uses Flutter beta v0.3.1 for introduction and description, with the corresponding engine commit 09d05a389. The Flutter project structure of the
hello_flutter project is listed below:
In the image above,
ios contains part of iOS code and uses CocoaPods to manage dependencies. The
android contains part of Android code and uses Gradle to manage dependencies. The
lib contains part of Dart code and uses pub to manage dependencies. Cocoapods in iOS corresponds to Podfile and Podfile.lock while pub corresponds to
Flutter supports common modes like Debug, Release, and Profile with its own features.
- Debug Mode: It corresponds to JIT mode of Dart, which is also called check mode or slow mode. This mode supports devices and simulators (iOS/Android) and permits assertion, including all debugging information, service extension, observatory, and other debugging aids. It is optimized for rapid development and operation, but not for execution speed, packet size, and deployment. In Debug mode, the compilation uses JIT technology to support the popular sub-second hot reload.
- Release Mode: It corresponds to AOT mode of Dart, with the aim of deployment to end users. This mode only supports physical devices, not simulators. All assertions and debugging tools are restricted, and debugging information is removed as much as possible. It is optimized for quick start, quick execution, and packet size. All debugging aids and service extensions are prohibited.
- Profile Mode: It is similar to Release mode while providing more support for service extensions. It also supports tracking and minimizing the dependencies required to use the tracking information. For example, the Observatory can connect to processes. Profile mode does not support simulators because judgments on simulators do not represent true performance.
Since there is no difference in compilation principles between Profile and Release modes, this article only discusses Debug mode and Release mode.
The iOS/Android project under Flutter is still essentially a standard iOS/Android project. Flutter only generates and embeds
Flutter.framework (iOS) by adding a shell to the BuildPhase. Flutter also adds
vm/isolate_snapshot_data/instr (Android) through gradle to compile Flutter-related code and embed them into a native App. Therefore, this article mainly discusses the construction and operation principles introduced by Flutter. Although the compilation targets include ARM, x64, x86, and ARM64, they are similar in principles. Thus, only ARM will be discussed here. Unless explicitly stated, Android defaults to ARMv7.
Compilation and Operation of Flutter Code (iOS)
Compilation in Release Mode
The construction procedure of Dart code in iOS projects under Flutter in Release mode is listed below:
In the image above,
gen_snapshot is the Dart compiler, applying technologies like Tree Shaking. Tree Shaking is similar to the dependency tree logic and can generate a minimum packet to disable the reflection feature supported by Dart in Flutter. The
gen_snapshot is applied to generate machine code in the assembly form and then the final
App.framework through compilation toolchains, such as xcrun. In other words, all Dart codes, including business code and third-party packet code, will eventually become
App.framework. It is the same for the Flutter framework code that Dart codes require.
Tree Shaking is located in the
gen_snapshot. For the corresponding logic, please see
engine/src/third_party/dart/runtime/vm/compiler/aot/precompiler.cc. The final corresponding symbols of Dart code in
App.framework are listed below:
App.framework contains four parts,
kDartIsolateSnapshotInstructions, just like products in the Android release. Why does iOS use the four parts of App.framework instead of Android's four files? Due to iOS system limitations, the Flutter engine cannot mark a memory page as executable at runtime, but Android can.
Flutter.framework corresponds to the engine part and embedder part of the Flutter architecture.
Flutter.framework is located in the
/bin/cache/artifacts/engine/ios of the Flutter repository and is pulled from the Google repository by default. When customized modifications are required, users can use the Ninja building system to generate
Flutter.framework by downloading the engine source code.
The final products of the Flutter-related code are
App.framework (generated by Dart code) and
Flutter.framework (engine). From the perspective of the Xcode project,
Generated.xcconfig describes the configuration information of the Flutter-related environment. The
xcode_backend.sh added in the Build Phases of the Runner project settings implement the copying and embedding of
Flutter.framework and the compilation and embedding of
Flutter.framework is copied from the engine of the Flutter repository to the Flutter directory under the root directory of the Runner project. The Flutter-related information generated in
Runner.app is listed below:
In the image above,
flutter_assets is the relevant resource, and the code is
Flutter.framework under Frameworks.
Operation in Release Mode
The Flutter-related rendering, event, and communication processing logics are shown below:
The call stack of the main function in Dart is shown below:
Compilation in Debug Mode
The compilation and structure of Flutter in Debug mode are similar to what is in Release mode. There are two main differences between these two modes:
Framework supports JIT in Debug mode but does not support JIT in Release mode.
App.framework is the local machine code corresponding to Dart code. In JIT,
App.framework has several simple APIs, and its Dart code is located in the
snapshot_blob.bin file. The snapshots in this section are script snapshots and contain simple tokenized source code. All comments and blank characters are removed. In addition, constants are normalized, and there is no machine code, Tree Shaking operation, or code obfuscation. The symbol table in
App.framework is listed below:
The following information appears when running the strings command on
The call stack of the main entry in Debug mode is listed below:
Compilation and Operation of Flutter Code (Android)
Apart from some platform-related features, Android and iOS also share something in common. Release mode corresponds to AOT, and Debug mode corresponds to JIT. Only their differences are discussed in this article.
Compilation in Release Mode
In Release mode, the construction procedure of Dart code in an Android project under Flutter is shown below:
In the image above,
vm/isolate_snapshot_data/instr is an ARM command, which is loaded during runtime by the engine. The engine marks the
vm/isolate_snapshot_instr as executable. Runtime and other services, such as garbage collection, are involved in
vm_ and used to initialize Dart VM. For the call entry, see
isolate_ corresponds to our app code and is used to create a new isolate. For the call entry, see
Dart_CreateIsolate(dart_api.h). Similar to
Flutter.framework in iOS,
flutter.jar includes the code in the engine, such as
flutter.jar. A set of classes and interfaces that embed Flutter into Android, such as
FlutterNativeView, are also included.
flutter.jar is located in
/bin/cache/artifacts/engine/android of the Flutter repository and is pulled from the Google repository by default. When customized modifications are required, users can use the Ninja building system to generate
flutter.jar by downloading the engine source code.
isolate_snapshot_data/instr as an example. The information after running the disarm command is listed below:
Its APK structure is listed below:
Once it is installed, APK determines whether to copy the assets in APK based on the judgment of a ts. The judgment is made based on a combination of the
packageinfo and the
lastUpdateTime. After the copy is completed, the information is listed below:
isolate/vm_snapshot_data/instr is located in the local data directory of the app. These writable parts can be downloaded and replaced to complete the entire replacement and update of the app.
Operation in Release Mode
Compilation in Debug Mode
Similar to the differences between Debug mode and Release mode in iOS, their differences in Android mainly include the following two parts:
The difference of flutter.jar in Android is the same as that in iOS.
2. App Code
App code is located in
flutter_assets. The difference is also the same as that in iOS.
After introducing the Flutter compilation principles in iOS and Android, this article will focus on how to customize Flutter and engines to complete the customization and optimization. Considering the rapid iteration of Flutter, the current problems may be solved in the future. Therefore, this section doesn’t emphasize problems to be solved but illustrates solutions based on different types of problems.
Customization and Optimization Related to Flutter Construction
Except for the components of the three layers mentioned above, Flutter is a very complex system that includes many other components. For example, there are components, such as the Flutter Android Studio (Intellij) plug-in, pub repository management, and others. However, customization and optimization are often related to the toolchain of Flutter. The specific codes are located in the
flutter_tools packet in the Flutter repository. So, the following section describes the customization process with examples.
libflutter.so (located under
flutter (flutter_tools) are involved.
1. Define the target in Android as armeabi
This part of the code is related to the construction, and the logic is located under
flutter.gradle. If the app supports ARMv7 or ARM64 through armeabi, the default logic of Flutter needs to be modified:
Due to the features of gradle, this part of the code takes effect immediately after being directly modified.
2. Set Android to use the first launchable-activity by default at startup
This part of the code is related to
flutter_tools. The modifications are listed below:
The key here lies in how to make the modification take effect instead of how to modify it. In principle, commands, such as
flutter run/build/analyze/test/upgrade, execute the script called
flutter (flutter_repo_dir/bin/flutter). Then, commands run
flutter_tools.snapshot through the script and Dart. The
flutter_tools.snapshot is generated through
packages/flutter_tools. The logic is listed below:
if [[ ! -f "SNAPSHOT_PATH" ]] || [[ ! -s "STAMP_PATH" ]] || [[ "(cat "STAMP_PATH")" != "revision" ]] || [[ "FLUTTER_TOOLS_DIR/pubspec.yaml" -nt "$FLUTTER_TOOLS_DIR/pubspec.lock" ]]; then
rm -f "$FLUTTER_ROOT/version"
echo Building flutter tool...
if [[ "$TRAVIS" == "true" ]] || [[ "$BOT" == "true" ]] || [[ "$CONTINUOUS_INTEGRATION" == "true" ]] || [[ "$CHROME_HEADLESS" == "1" ]] || [[ "$APPVEYOR" == "true" ]] || [[ "$CI" == "true" ]]; then
if [[ -d "$FLUTTER_ROOT/.pub-cache" ]]; then
while : ; do
"$PUB" upgrade --verbosity=error --no-packages-dir && break
echo Error: Unable to 'pub upgrade' flutter tool. Retrying in five seconds...
"$DART" --snapshot="$SNAPSHOT_PATH" --packages="$FLUTTER_TOOLS_DIR/.packages" "$SCRIPT_PATH"
echo "$revision" > "$STAMP_PATH"
The figure above shows that
flutter_tools need to be reconstructed. You can generate
flutter_repo_dir/bin/cache/flutter_tools.stamp to do so. By doing so,
flutter_tools can be generated again. Shielding the
if/fi judgment can also reconstruct
flutter_tools, and it will be generated each time.
Using Flutter in Release Mode in Debug mode with an Android project
If Flutter lag appears during R&D, the reason may lie in the logic or the mode. This problem may occur when Flutter is in Debug mode. In this case, developers can build an APK in Release mode or forcibly change Flutter to the Release mode. The code is listed below:
Related objects include
1. Optimization on recompilation caused by the repeated replacement of
Flutter.framework during construction
This part of the code is related to the construction, and the logic is located in
xcode_backend.sh. The configuration (
Generated.xcconfig configuration) will be referred to every time to find and replace
Flutter.framework to ensure Flutter always gets the correct
Flutter.framework. This leads to the recompilation of some codes that depend on the Framework. The code modifications are listed below:
2. Using Flutter in Release mode in Debug mode with an iOS project
It can be achieved by modifying
Generated.xcconfig to Release mode and
FLUTTER_FRAMEWORK_DIR to the corresponding path.
3. Support for ARMv7
For the original article, please visit this link.
Flutter unofficially supports ARMv7 in iOS. However, since there is no official support, users must modify the relevant logic. The modifications are listed below:
1. The default logic can generate
2. Modify Flutter to enable
flutter_tools to be reconstructed each time and then modify
mac.dart. Change the iOS-related ARM64 to ARMv7 and then change
gen_snapshot to the i386 architecture.
The following command can be executed to generate
gen_snapshot under the i386 architecture:
./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=arm
ninja -C out/ios_debug_arm
There lies an implicit logic: Architectures of arch of the target
gen_snapshot and the final
App.framework should be consistent to construct CPU-related predefined macros like
x86_64 :arrow_right: x86_64 :arrow_right: ARM64 or i386 :arrow_right: i386 :arrow_right: ARMv7
3. On iPhone 4S, the
EXC_BAD_INSTRUCTION(EXC_ARM_UNDEFINED) error can be caused by unsupported SDIV instruction, which is generated by
gen_snapshot. The error can be corrected by adding the parameter,
--no-use-integer-division (located in
gen_snapshot. The logic is shown in the figure below:
4. Based on the
Flutter.framework generated by paragraphs #1 and #2,
lipo create generates a
Flutter.framework that supports ARMv7 and ARM64.
Flutter.framework and remove it
App.framework must also be processed this way to prevent it from being affected by App Thining after official operations.
You can refer to the following steps when learning about the detailed execution logic of Flutter during APK construction in Debug mode.
- Understand the command line parameters of
packages/flutter_toolsin a Dart project. Modify
flutter_tools.dartbased on the obtained parameters and set the command line dart app to start debugging.
Engine Customization and Debugging
Let’s imagine that customization and business development are conducted based on Flutter beta v0.3.1. SDK is not upgraded within a certain time to ensure stability. At this time, Flutter has modified a bug in v0.3.1 on master, which is marked as
fix_bug_commit. How can you track and manage this situation?
1. Flutter beta v0.3.1 specifies that the corresponding engine commit is 09d05a389 (see
3. The code obtained in step 2 is master code, while the corresponding code repository of the specific commit (09d05a389) is expected. Therefore, it’s necessary to pull a new branch from this commit, custom_beta_v0.3.1.
4. Based on custom_beta_v0.3.1 (commit: 09d05a389), run gclient sync to obtain all the engine codes corresponding to Flutter beta v0.3.1.
git cherry-pick fix_bug_commit to synchronize the master's changes to custom_beta_v0.3.1. If the changes depend a lot on the latest changes, the compilation may fail.
6. For iOS-related modifications, execute the following code:
./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=arm
ninja -C out/ios_debug_arm./flutter/tools/gn --runtime-mode=release --ios --ios-cpu=arm
ninja -C out/ios_release_arm./flutter/tools/gn --runtime-mode=profile --ios --ios-cpu=arm
ninja -C out/ios_profile_arm./flutter/tools/gn --runtime-mode=debug --ios --ios-cpu=arm64
ninja -C out/ios_debug./flutter/tools/gn --runtime-mode=release --ios --ios-cpu=arm64
ninja -C out/ios_release./flutter/tools/gn --runtime-mode=profile --ios --ios-cpu=arm64
ninja -C out/ios_profile
By doing so, products of ARM/ARM64 and debug/release/profile for iOS can be generated. The generated products can replace
If you need to debug the
Flutter.framework source code, the command during construction is listed below:
./flutter/tools/gn --runtime-mode=debug --unoptimized --ios --ios-cpu=arm64
ninja -C out/ios_debug_unopt
The engine source code can be debugged by replacing
gen_snapshot in Flutter with generated products.
7. For Android-related modifications, execute the following code:
./flutter/tools/gn --runtime-mode=debug --android --android-cpu=arm
ninja -C out/android_debug./flutter/tools/gn --runtime-mode=release --android --android-cpu=arm
ninja -C out/android_release./flutter/tools/gn --runtime-mode=profile --android --android-cpu=arm
ninja -C out/android_profile
By doing so, products of ARM and debug/release/profile for Android can be generated. The generated products can replace
- Flutter’s Modes
- iOS Builds Supporting ARMv7
- Contributing to the Flutter Engine
- Flutter System Architecture
- Symbolicating Production Crash Stacks