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.
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.
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 pubspec.yaml
and pubspec.lock
.
Flutter supports common modes like Debug, Release, and Profile with its own features.
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 App.framework
and Flutter.framework
(iOS) by adding a shell to the BuildPhase. Flutter also adds flutter.jar
and 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.
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, kDartVmSnapshotdData
, kDartVmSnapshotInstructions
, kDartIsolateSnapshotData
, and 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 App.framework
. The 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 App.framework
and Flutter.framework
under Frameworks.
The Flutter-related rendering, event, and communication processing logics are shown below:
The call stack of the main function in Dart is shown below:
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:
1. Flutter.framework
Framework supports JIT in Debug mode but does not support JIT in Release mode.
2. App.framework
In AOT, 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 Runner.app/flutter_assets/snapshot_blob.bin
:
The call stack of the main entry in Debug mode is listed below:
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.
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 Dart_Initialize(Dart_api.h)
. The 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 libflutter.so
in flutter.jar
. A set of classes and interfaces that embed Flutter into Android, such as FlutterMain
, FlutterView
, and 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.
Let's use 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 versionCode
in packageinfo
and the lastUpdateTime
. After the copy is completed, the information is listed below:
The 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.
Similar to the differences between Debug mode and Release mode in iOS, their differences in Android mainly include the following two parts:
1. flutter.jar
The difference of flutter.jar in Android is the same as that in iOS.
2. App Code
App code is located in snapshot_blob.bin
of 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.
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.
The flutter.jar
, libflutter.so
(located under flutter.jar
), gen_snapshot
, flutter.gradle
, and 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"
touch "$FLUTTER_ROOT/bin/cache/.dartignore"
"$FLUTTER_ROOT/bin/internal/update_dart_sdk.sh"
echo Building flutter tool...
if [[ "$TRAVIS" == "true" ]] || [[ "$BOT" == "true" ]] || [[ "$CONTINUOUS_INTEGRATION" == "true" ]] || [[ "$CHROME_HEADLESS" == "1" ]] || [[ "$APPVEYOR" == "true" ]] || [[ "$CI" == "true" ]]; then
PUB_ENVIRONMENT="$PUB_ENVIRONMENT:flutter_bot"
fi
export PUB_ENVIRONMENT="$PUB_ENVIRONMENT:flutter_install"
if [[ -d "$FLUTTER_ROOT/.pub-cache" ]]; then
export PUB_CACHE="${PUB_CACHE:-"$FLUTTER_ROOT/.pub-cache"}"
fi
while : ; do
cd "$FLUTTER_TOOLS_DIR"
"$PUB" upgrade --verbosity=error --no-packages-dir && break
echo Error: Unable to 'pub upgrade' flutter tool. Retrying in five seconds...
sleep 5
done
"$DART" --snapshot="$SNAPSHOT_PATH" --packages="$FLUTTER_TOOLS_DIR/.packages" "$SCRIPT_PATH"
echo "$revision" > "$STAMP_PATH"
fi
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.
3. 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 Flutter.framework
, gen_snapshot
, xcode_backend.sh
, and flutter (flutter_tools)
.
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 FLUTTER_BUILD_MODE
in 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 Flutter.framework
(ARM64).
2. Modify Flutter to enable flutter_tools
to be reconstructed each time and then modify build_aot.dart
and 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_/_i386
of gen_snapshot
.
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 build_aot.dart
) to 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.
5. Modify Info.plist
in Flutter.framework
and remove it
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
Similarly, the App.framework
must also be processed this way to prevent it from being affected by App Thining after official operations.
flutter_tools
You can refer to the following steps when learning about the detailed execution logic of Flutter during APK construction in Debug mode.
flutter_tools
packages/flutter_tools
in a Dart project. Modify flutter_tools.dart
based on the obtained parameters and set the command line dart app to start 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 flutter/bin/internal/engine.version
).
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.
5. Use 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 Flutter.framework
and gen_snapshot
under flutter/bin/cache/artifacts/engine/ios
.
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 Flutter.framework
and 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 gen_snapshot
and flutter.jar
under flutter/bin/cache/artifacts/engine/android
.
Exploring Flutter-Native Hybrid Engineering Decoupling in Xianyu
56 posts | 4 followers
FollowXianYu Tech - August 6, 2020
XianYu Tech - May 11, 2021
XianYu Tech - August 10, 2021
Alibaba Clouder - May 17, 2021
XianYu Tech - May 13, 2021
sdlu - November 4, 2019
56 posts | 4 followers
FollowAlibaba Cloud (in partnership with Whale Cloud) helps telcos build an all-in-one telecommunication and digital lifestyle platform based on DingTalk.
Learn MoreBuild superapps and corresponding ecosystems on a full-stack platform
Learn MoreWeb App Service allows you to deploy, scale, adjust, and monitor applications in an easy, efficient, secure, and flexible manner.
Learn MoreExplore Web Hosting solutions that can power your personal website or empower your online business.
Learn MoreMore Posts by XianYu Tech