Flutter iOS Package Size Optimization: Analysis and Implementation
The article examines why adding Flutter to an iOS app inflates the binary, breaks down the framework’s largest parts—engine, AOT snapshot, assets, and ICU data—and details a three‑pronged strategy of compression, externalization and stripping, using custom build scripts to move or delete files and dynamically load data, achieving roughly a 12 MB size reduction.
1. Background
Flutter is an excellent cross‑platform solution that our QMusic team follows closely. While integrating Flutter into QMusic Live, the primary issue we faced was the increase in package size. This article analyzes the Flutter package size problem step by step, explores every possible optimization point, combines real‑project experience and engine source code, and finally provides a detailed implementation plan for size reduction. Readers are welcome to discuss Flutter‑related technologies.
1.1 Flutter Hybrid Development Mode
To add Flutter to an existing native app, there are two common integration methods:
Include the native project as a sub‑project of the Flutter project, managed entirely by Flutter (unified management mode).
Keep the native project unchanged and add the Flutter project as a separate module (three‑side separation mode), where Flutter is developed independently.
The unified management mode is easy to set up but has obvious drawbacks: heavy coupling among Android, iOS and Flutter code, and a significant increase in tool‑chain time, which reduces development efficiency. Therefore we adopt the three‑side separation mode, using AAR for Android and Pod for iOS to integrate Flutter in a lightweight manner.
1.2 Flutter Size‑Reduction Requirement
Introducing Flutter inevitably enlarges the app package. In the three‑side separation mode, the size increase on Android is the Flutter AAR, and on iOS it is the Flutter framework. To solve the size problem we need to optimize both the AAR and the framework.
1.3 Development Environment
Flutter 1.17.1 • channel stable
macOS 10.15.4
Xcode 11.4 (iOS & macOS development)
Python 2.7.16
2. iOS Framework Product Analysis
In our project the Flutter code is compiled into a framework, which is then linked into the iOS host app. To reduce the size we first analyze the framework contents.
2.1 Framework Structure
Using the tree command on the Release directory we see two frameworks:
Release
├── App.framework
│ ├── App // AOT snapshot data compiled from Dart business code (Mach‑O dynamic library)
│ ├── Info.plist
│ └── flutter_assets // resource files
└── Flutter.framework
├── Flutter // Flutter engine (Mach‑O dynamic library)
├── Headers
├── Info.plist
├── Modules
├── _CodeSignature
└── icudtl.dat // internationalization dataThe large files are highlighted in the diagram.
2.2 Framework Size Analysis (Release)
The following table lists the biggest files in the Release directory, which are the main targets for size optimization.
Name
Size
Description
App
7.3 M
Dart business code AOT compilation product
flutter_assets
2 M
Images, fonts and other resources
Flutter
11 M
Engine
icudtl.dat
884 k
Internationalization support file
Other third‑party plugins
800 k
e.g., Flutter_boost
3. iOS Size‑Reduction Strategy
From the previous analysis the four biggest components are App , flutter_assets , Flutter and icudtl.dat . We will examine each for possible reduction.
Optimization directions are divided into three categories:
Compress : data compression reduces framework size but has limited impact on the final app because the app package is already compressed.
Delete : remove unused parts.
Move : if removal is not possible, separate the component and load it dynamically at runtime.
3.1 App.framework / flutter_assets
The flutter_assets directory contains resources. If we do not want to bundle it into the app, we can move it out and download it on demand. Two methods are available:
Modify the Flutter‑tools build script so that flutter_assets is excluded during framework generation.
Post‑process the generated framework and strip flutter_assets via a custom script.
We currently use the second method because modifying the build script for a single asset is not cost‑effective.
Removing flutter_assets does not affect engine startup as long as the correct path is supplied to the engine. The engine checks for the assets directory in FlutterDartProject.mm (function DefaultSettingsForProcess ) and stores the path in settings.assets_path . Therefore the assets can reside inside the framework or be downloaded externally, provided the engine is informed of the correct location.
//FlutterDartProject.mm
// Checks to see if the flutter assets directory is already present.
if (settings.assets_path.size() == 0) {
NSString* assetsName = [FlutterDartProject flutterAssetsName:bundle];
NSString* assetsPath = [bundle pathForResource:assetsName ofType:@""];
if (assetsPath.length == 0) {
assetsPath = [mainBundle pathForResource:assetsName ofType:@""];
}
if (assetsPath.length == 0) {
NSLog(@"Failed to find assets path for \"%@\"", assetsName);
} else {
settings.assets_path = assetsPath.UTF8String;
// further checks omitted for brevity
}
}3.2 Flutter.framework / icudtl.dat
icudtl.dat stores the engine’s internationalization data. It is loaded during engine initialization via the icu_data_path field in Settings . By providing an external path we can move this file out of the framework.
//settings.h
bool icu_initialization_required = true;
std::string icu_data_path; // path to icudtl.dat //shell.cc (simplified)
if (settings.icu_initialization_required) {
if (settings.icu_data_path.size() != 0) {
fml::icu::InitializeICU(settings.icu_data_path);
} else if (settings.icu_mapper) {
fml::icu::InitializeICUFromMapping(settings.icu_mapper());
} else {
FML_DLOG(WARNING) << "Skipping ICU initialization in the shell.";
}
}3.3 App.framework / App (AOT snapshot)
The App binary contains the AOT‑compiled Dart code. Its large size comes from four symbols:
kDartIsolateSnapshotData
kDartVmSnapshotData
kDartIsolateSnapshotInstructions
kDartVmSnapshotInstructions
iOS does not allow the executable part ( *_Instructions ) to be loaded dynamically, but the data sections ( *_Data ) can be loaded at runtime. Therefore we can move kDartIsolateSnapshotData and kDartVmSnapshotData out of the binary and load them dynamically.
// nm output (simplified)
000000000065c008 b _kDartIsolateSnapshotBss
00000000003a1270 S _kDartIsolateSnapshotData
0000000000009000 T _kDartIsolateSnapshotInstructions
000000000065c000 b _kDartVmSnapshotBss
0000000000399400 S _kDartVmSnapshotData
0000000000004000 T _kDartVmSnapshotInstructionsOnly the data sections can be externalized. The generation of these sections is performed by the gen_snapshot tool. By customizing gen_snapshot we can write the data to external files instead of embedding them.
// image_snapshot.cc (excerpt)
void WriteTextToLocalFile(WriteStream* clustered_stream, bool vm) {
#if defined(TARGET_OS_MACOS_IOS)
auto OpenFile = [](const char* filename) {
bin::File* file = bin::File::Open(NULL, filename, bin::File::kWriteTruncate);
if (file == NULL) {
exit(255);
}
return file;
};
// Choose path based on architecture and vm/isolate flag
#if defined(TARGET_ARCH_ARM64)
bin::File* file = OpenFile(vm ? "./SnapshotData/arm64/VmSnapshotData.S"
: "./SnapshotData/arm64/IsolateSnapshotData.S");
#else
bin::File* file = OpenFile(vm ? "./SnapshotData/armv7/VmSnapshotData.S"
: "./SnapshotData/armv7/IsolateSnapshotData.S");
#endif
// Write the assembly representation to the file (omitted for brevity)
#endif
}
void AssemblyImageWriter::WriteText(WriteStream* clustered_stream, bool vm) {
#if defined(TARGET_OS_MACOS_IOS)
WriteTextToLocalFile(clustered_stream, vm);
#else
// original in‑framework embedding logic
#endif
}After extracting the .S files we compile them into raw data files:
# Example for armv7
xcrun cc -arch armv7 -c ./SnapshotData/armv7/IsolateSnapshotData.S -o ./SnapshotData/armv7/HeadIsolateData.dat
xcrun cc -arch armv7 -c ./SnapshotData/armv7/VmSnapshotData.S -o ./SnapshotData/armv7/HeadVMData.dat
# Strip the object file header (first 312 bytes)
tail -c +313 ./SnapshotData/armv7/HeadIsolateData.dat > ./SnapshotData/armv7/IsolateData.dat
tail -c +313 ./SnapshotData/armv7/HeadVMData.dat > ./SnapshotData/armv7/VMData.datThese IsolateData.dat and VMData.dat files are then loaded by the engine via modified ResolveVMData and ResolveIsolateData functions.
// dart_snapshot.cc (excerpt)
static std::shared_ptr
ResolveVMData(const Settings& settings) {
#if OS_IOS
if (settings.ios_vm_snapshot_data_path.empty()) {
return SearchMapping(settings.vm_snapshot_data,
settings.vm_snapshot_data_path,
settings.application_library_path,
DartSnapshot::kVMDataSymbol,
false);
} else {
return SetupMapping(settings.ios_vm_snapshot_data_path);
}
#else
return SearchMapping(settings.vm_snapshot_data,
settings.vm_snapshot_data_path,
settings.application_library_path,
DartSnapshot::kVMDataSymbol,
false);
#endif
}
static std::shared_ptr
ResolveIsolateData(const Settings& settings) {
#if OS_IOS
if (settings.ios_isolate_snapshot_data_path.empty()) {
return SearchMapping(settings.isolate_snapshot_data,
settings.isolate_snapshot_data_path,
settings.application_library_path,
DartSnapshot::kIsolateDataSymbol,
false);
} else {
return SetupMapping(settings.ios_isolate_snapshot_data_path);
}
#else
return SearchMapping(settings.isolate_snapshot_data,
settings.isolate_snapshot_data_path,
settings.application_library_path,
DartSnapshot::kIsolateDataSymbol,
false);
#endif
}3.4 Flutter.framework / Flutter (engine)
The engine binary ( Flutter ) is relatively large (≈11 M) and offers limited reduction potential. We can trim unused Skia or BoringSSL features (a few hundred kilobytes) and strip debug symbols:
# Strip symbols and generate dSYM
xcrun dsymutil -o $frameworkpath/Flutter.framework.dSYM $frameworkpath/Release/Flutter.framework/flutter
xcrun strip -x -S $frameworkpath/Release/Flutter.framework/flutter4. Practical Implementation
4.1 Engine Build Configuration
Below are sample build scripts for Debug, Profile and Release modes. They set up the appropriate GN arguments, invoke ninja , and lipo‑merge the resulting frameworks.
# ios_debug.sh (excerpt)
#!/bin/bash
export FLUTTER_SDK="your flutter install path"
# Build simulator
./flutter/tools/gn --unoptimized --runtime-mode debug --simulator
./flutter/tools/gn --unoptimized --ios --runtime-mode debug --simulator
ninja -C out/host_debug_sim_unopt -j 10
ninja -C out/ios_debug_sim_unopt -j 10
# Build armv7
./flutter/tools/gn --unoptimized --runtime-mode debug --ios-cpu arm
./flutter/tools/gn --unoptimized --ios --runtime-mode debug --ios-cpu arm
ninja -C out/host_debug_unopt_arm -j 10
ninja -C out/ios_debug_unopt_arm -j 10
# Build arm64 (similar steps omitted)
# Lipo merge and copy to Flutter SDK cache
lipo -create -output tmp/Flutter.framework/Flutter \
out/ios_debug_sim_unopt/Flutter.framework/Flutter \
out/ios_debug_unopt/Flutter.framework/Flutter \
out/ios_debug_unopt_arm/Flutter.framework/Flutter
cp -rf tmp/Flutter.framework "$FLUTTER_SDK"/bin/cache/artifacts/engine/ios-debug/4.2 Xcode Engine Debug Configuration
Open the generated products.xcodeproj inside src/out/ios_debug_unopt , drag it into the host Runner.xcworkspace , and add the following environment variables in Generated.xcconfig :
FLUTTER_FRAMEWORK_DIR=/Users/…/engine/src/out/ios_debug_unopt
LOCAL_ENGINE=ios_debug_unopt
FLUTTER_ENGINE=/Users/…/engine/src4.3 Modifying Engine Sources for Custom Data Paths
We add two new fields to Settings ( ios_vm_snapshot_data_path and ios_isolate_snapshot_data_path ) and fill them in DefaultSettingsForProcess based on resources bundled in the app.
// FlutterDartProject.mm (excerpt)
void initSettings(flutter::Settings& settings) {
#if FLUTTER_RUNTIME_MODE != FLUTTER_RUNTIME_MODE_DEBUG
// VMData.dat
NSString* vmDataPath = [[NSBundle mainBundle] pathForResource:@"VMData" ofType:@"dat"];
if (vmDataPath) settings.ios_vm_snapshot_data_path = vmDataPath.UTF8String;
// IsolateData.dat
NSString* isolateDataPath = [[NSBundle mainBundle] pathForResource:@"IsolateData" ofType:@"dat"];
if (isolateDataPath) settings.ios_isolate_snapshot_data_path = isolateDataPath.UTF8String;
// icudtl.dat
NSString* icuPath = [[NSBundle mainBundle] pathForResource:@"icudtl" ofType:@"dat"];
if (icuPath) settings.icu_data_path = icuPath.UTF8String;
#endif
}
static flutter::Settings DefaultSettingsForProcess(NSBundle* bundle = nil) {
auto settings = flutter::SettingsFromCommandLine(command_line);
initSettings(settings);
return settings;
}4.4 Flutter‑Side Packaging Script
The script ios_build_reduce.sh performs the following steps:
Build the Flutter iOS framework.
Compile the extracted *.S files into IsolateData.dat and VMData.dat for armv7 and arm64.
Move flutter_assets and icudtl.dat out of the framework.
Package the reduced artifacts into FlutterPackage.zip .
Strip debug symbols from the engine binary and generate a dSYM.
# ios_build_reduce.sh (excerpt)
# Build framework
flutter build ios-framework
# Compile data sections
xcrun cc -arch armv7 -c ./SnapshotData/armv7/IsolateSnapshotData.S -o ./SnapshotData/armv7/HeadIsolateData.dat
xcrun cc -arch armv7 -c ./SnapshotData/armv7/VmSnapshotData.S -o ./SnapshotData/armv7/HeadVMData.dat
tail -c +313 ./SnapshotData/armv7/HeadIsolateData.dat > ./SnapshotData/armv7/IsolateData.dat
tail -c +313 ./SnapshotData/armv7/HeadVMData.dat > ./SnapshotData/armv7/VMData.dat
# Similar steps for arm64 …
# Move assets and icudtl
mv $releasepath/App.framework/flutter_assets $frameworkpath/flutter_reduce/
mv $releasepath/Flutter.framework/icudtl.dat $frameworkpath/flutter_reduce/
# Zip reduced package
zip -r $frameworkpath/FlutterPackage.zip $frameworkpath/flutter_reduce
# Strip engine symbols
xcrun dsymutil -o $frameworkpath/Flutter.framework.dSYM $frameworkpath/Release/Flutter.framework/flutter
xcrun strip -x -S $frameworkpath/Release/Flutter.framework/flutter4.5 iOS Pod Integration
In the host Podfile we either integrate the Flutter source code or, when IsFlutterSourceCode = 0 , use two podspecs that point to the reduced frameworks:
# Podfile excerpt
IsFlutterSourceCode = 0
def FlutterModuleIntegration
if IsFlutterSourceCode == 1
# source integration (official method)
flutter_application_path = '../native_modules/mlive/'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
install_all_flutter_pods(flutter_application_path)
else
pod 'FlutterDebug', :configurations => ['Debug'], :path => 'LocalLib/Flutter'
pod 'FlutterRelease', :configurations => ['Release', 'AppStore', 'iAP'], :path => 'LocalLib/Flutter'
end
end5. Results
The table below summarizes the size reductions achieved by each optimization:
Name
Optimization Method
Size Savings
icudtl.dat
Download at runtime
≈884 k
flutter_assets
Download at runtime
≈2.1 M
App data segment
Download at runtime
≈2.8 M per architecture
Flutter engine
Strip debug symbols
≈6 M per architecture
Overall framework reduction
—
≈11.7 M total
By deleting unnecessary files, moving large assets out of the framework, stripping engine symbols, and customizing the engine to load data dynamically, the iOS Flutter integration size is reduced to roughly 10 M.
Tencent Music Tech Team
Public account of Tencent Music's development team, focusing on technology sharing and communication.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.