Flutter is an open source mobile cross-platform UI development kit that not only works with existing project code, but also allows you to quickly develop high-quality cross-platform apps using Dart. Based on the compilation principle of Flutter, this paper discusses the package size optimization scheme of Flutter access.
1. The status quo
In my current project, Flutter is developed in the way of product integration, that is, Flutter is developed separately as a terminal, and then the Android and iOS terminals are connected to the product files compiled by Flutter terminal respectively. The product files are then introduced into Native engineering in the form of components, and the Android terminal is SO file. The iOS side uses CocoaPod to import the XCFramework.
IOS Flutter package size
Framework | The file name | The size of the | note |
App.xcframework | App | 7.2 M | AOT Snapshot data, size depends on the amount of business code, dynamic link library |
flutter_assets | 868KB | Image resources, fonts, etc | |
Flutter.xcframework | Flutter | 7.5 M | The Size of the Flutter engine depends on the Flutter version, basically fixed, dynamically linked library |
icudtl.dat | 898KB | Internationalization supports related data files | |
Three party libraries | plugins | 468KB | Tripartite plug-ins and static libraries, etc |
When APP integrates Flutter, it will inevitably increase the package size. Currently, Android can dynamically deliver Flutter components using the plug-in framework, so the impact on the package size is almost zero. However, on iOS, executable files cannot be dynamically delivered due to the limitation of the iOS system, so it cannot be processed like Android.
As can be seen from the table above, the total size of the whole Flutter products reaches 16.2m, while the current universal iOS installation package size of the project is only 44M, and Flutter products account for 36% of the package size, which is in urgent need of optimization.
2. Optimization scheme
In early 2020, byteDance technology Salon shared a discussion on how to Reduce the size of a Flutter package by Nearly 50%. There are three main methods mentioned in the discussion: Delete, shrink, and move. “Delete” means to delete useless codes and resources, “shrink” means to compress image resources, etc., and “move” means to remove unnecessary data and resources from products and deliver them dynamically.
In addition to the three methods mentioned in the article, there is another “control”, that is, to control the increase of package size. Therefore, I wrote a plug-in that can use each other’s resource files between the Flutter and Native, and also control the increase of the size of the Flutter to a certain extent.
In general, the iOS side needs to handle the following things:
- Separate App. Framework
flutter_assets
- Separate Flutter. Framework
icudtl.dat
- Separate data segment artifacts from app. framework
- The separation of
flutter_assets
,icudtl.dat
, data segment packaging compression - Reextract and load the data when needed
The separation of Flutter_assets and icudtl.dat is simple and can be removed directly by scripting:
#Remove flutter_assets
# $res_releaseFramework for product's location, usually a / {FLUTTER_ROOT} / build/ios/framework/Release
# $flutter_dataCollection catalogue for products that need to be separated
mv $res_release/App.xcframework/ios-arm64_armv7/App.framework/flutter_assets $flutter_data
mv $res_release/Flutter.xcframework/ios-armv7_arm64/Flutter.framework/icudtl.dat $flutter_data
Copy the code
The separation of data segments is complex, and many changes have been made to the way that data segments are written to Flutter 2.0. We will explain in detail how and why the separation of AOT compilation products is possible.
3. Product analysis of App. Xcframewok
3.1 Composition of App. Xcframework
In release mode, the iOS end product is AOT (Ahead Of Time compilation) compilation product, similar to C++ code, which needs to be compiled into special binary in advance before it can be loaded and run. Its core advantage is fast speed, using compiled binary code, can improve the speed of loading and execution. However, binary code requires execution permission and cannot be dynamically updated in iOS.
The composition of binary products is as follows:
nm App.framework/App ... Other symbols... 0000000000544008 b _kDartIsolateSnapshotBss 00000000002c0920 S _kDartIsolateSnapshotData 0000000000009000 T _kDartIsolateSnapshotInstructions 0000000000544000 b _kDartVmSnapshotBss 00000000002b8720 S _kDartVmSnapshotData 0000000000005000 T _kDartVmSnapshotInstructions U dyld_stub_binderCopy the code
Symbolic analysis:
kDartIsolateSnapshotBss
: ISOLATE Static variable data segmentkDartIsolateSnapshotData
: ISOLATE Data segment, which contains data needed for each ISOLATE to runkDartIsolateSnapshotInstructions
: ISOLATE instruction section, which contains code instructions for each ISOLATE to runkDartVmSnapshotBss
Dart VM static variable data segmentkDartVmSnapshotData
: Indicates the data required for running the Dart VMkDartVmSnapshotInstructions
: Code instructions required to run the Dart VM
Among them, the Data segments related to BSS cannot be dynamically delivered because they are static variables, and the instruction segments related to instructions cannot be dynamically delivered because the executable state of instructions cannot be arbitrarily marked due to the limitation of iOS system. Only the Data segments related to Data can be unrestricted during loading and can be separated.
3.2 Generation of app.xcFramework
Before splitting the data segment, we should look at how the file was generated to figure out how to do it. To generate app.xcFramework, you need to execute the following command:
flutter build ios-framework --release --no-debug --no-profile
Copy the code
To see how it works, look at the overall sequence diagram:
This command does several things:
- Parses command parameters to find the corresponding command for compiling iOS Framework
- Get the current build information and select the build mode
- use
FlutterBuildSystem
Build AOT artifacts FlutterBuildSystem
Each compilation step is defined as onetarget
, so compiling is to make each onetarget
performbuild
Command to complete their own compilation tasks- in
build ios-framework
In the mode of,target
For aoT-Assembly mode, he will eventually callGenSnapshot
This binary executable is used to generate aOT artifacts - when
GenSnapshot
After the product is returned, it is packaged into framework operations and finally output to the corresponding path
After genSnapshot executes the run command, the following process is performed:
GenSnapshot
performmain
Function to start the Dart VIRTUAL machine- Once the virtual machine is started, the compilation function is selected based on the required compilation product types, of which there are seven:
- kCore
- kCoreJIT
- kApp
- kAppJIT
- kAppAOTAssembly
- kAppAOTElf
- kVMAOTAssembly
IOS uses
kAppAOTAssembly
, will callCreateAndWritePrecompiledSnapshot
Method is precompiled and then calledDart_CreateAppAOTSnapshotAsAssembly
Generate snapshot products - Is used when the Snapshot product is generated
FullSnapshotWriter
, will first write VMSnapshot data, including recording header information, version information, and then write VM data segment and command segment - then
FullSnapshotWriter
The IsolateSnapshot data, including header information and version information, will be written to the IsolateSnapshot data, and then the data and instruction segments of the data Isolate will be written to the IsolateSnapshot data - And the data section and instruction section write, is actually called
AssemblyImageWriter
thewrite
Method, which will write in turnbss
(static data segment),Text
(Command segment),ROData
(Read-only data segment) - When all data has been written, the final snapshot compilation is returned and generated, and then sent back to the parent for further packaging into the framework.
So what we need to separate from the product is the ROData read-only data segment.
3.3 Loading of app.xcFramework artifacts
Once we understand how the Flutter engine is constructed and generated, we also need to understand how the Flutter engine loads the code and data segments stored in app.xcFramework /App. Otherwise, the separation will cause the engine to fail to load the data and the so-called separation will be meaningless.
FlutterEngine
You need to initialize one during initializationFlutterDartProject
.FlutterDartProject
Need to read the default Settings, which includeassetsPath
,icudataPath
,applicationFrameworkPath
And so on.FlutterEngine
Execute after initializationrun
The command starts the engine, which requires starting the Dart virtual machine- When creating the Dart VM,
dart_snapshot
Needs to be read from a global setting fileVMSnapshot
withIsolateSnapshot
Data segment and instruction segment location, and form a file mapping - Start the Flutter engine only after the Dart VM is created.
So the Dart virtual machine is still created with the opportunity to redirect the location of the data segment, which opens up the possibility of separating and loading the product correctly, and then separating the product.
4. App. Xcframework product separation
4.1 Compilation of the Flutter engine
GenSnapshot corresponding execution method of the source code for flutter_engine/third_party/dart/runtime/bin/gen_snapshot. Cc, so if we want to modify genSnapshot product, This means that we need to modify the source code of the Flutter engine.
The configuration of the build environment for the Flutter engine and the build procedure can be found on the Github Wiki page of Flutter.
The simulator, ARM64 and ARMV7 architecture need to be compiled when compiling the engine. Because Flutter2.0 uses xcFramework, which includes the simulator architecture, there is no way to build flutter without compiling the simulator architecture. Here is the compilation script I used:
Ps: If you do not want to change the engine to affect the current FlutterSDK, you can use FVM (Flutter Version Manager) to version manage flutter.
ios_flutter_engine.sh
#Compile emulator architecture
echo "start complie iOS debug simulator flutter engine"
./flutter/tools/gn --runtime-mode debug --simulator
./flutter/tools/gn --ios --runtime-mode debug --simulator
ninja -C out/ios_debug_sim -j 20
#The compilation engine is a memory hog, so I'm using MAC Pro to compile, so I set it to 20
#It can be adjusted to about 10 or 5 depending on the performance of your computer
echo "simulator flutter engine complied succeed"
#Compile the ARM64 architecture
echo "start complie iOS release arm64 flutter engine"
./flutter/tools/gn --ios --runtime-mode release --ios-cpu arm64
ninja -C out/ios_release -j 20
echo "arm64 flutter engine complied succeed"
#Compile the ARMV7 architecture
echo "start complie iOS release armv7 flutter engine"
./flutter/tools/gn --ios --runtime-mode release --ios-cpu arm
ninja -C out/ios_release_arm -j 20
echo "armv7 flutter engine complied succeed"
#Merge armV7 and ARM64 architectures
echo "merge framework"
rm -rf tmp/*
cp -rf out/ios_release/Flutter.framework tmp/
lipo -create -output tmp/Flutter.framework/Flutter \
out/ios_release/Flutter.framework/Flutter \
out/ios_release_arm/Flutter.framework/Flutter
#Replace the compiled gen_snapshot file with the gen_snapshot used by the existing FlutterSDK
# ${FLUTTER_PATH}Is the location of FlutterSDKcp -rf tmp/Flutter.framework "${FLUTTER_PATH}"/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-armv7_arm64/ cp -rf out/ios_debug_sim/Flutter.framework "${FLUTTER_PATH}"/bin/cache/artifacts/engine/ios-release/Flutter.xcframework/ios-x86_64-simulator/ cp -f out/ios_release/clang_x64/gen_snapshot "${FLUTTER_PATH}"/bin/cache/artifacts/engine/ios-release/gen_snapshot_arm64 cp -f out/ios_release_arm/clang_x64/gen_snapshot "${FLUTTER_PATH}"/bin/cache/artifacts/engine/ios-release/gen_snapshot_armv7 echo "complie end"Copy the code
Bitcode is not supported by the flutter engine by default. To enable bitcode, add the –bitcode directive
./flutter/tools/gn --ios --runtime-mode release --ios-cpu arm --bitcode
Copy the code
After successful compilation, the compiled gen_snapshot is replaced with the engine specified for FlutterSDK.
4.2 Separating Snapshot ROData
After compiling the Flutter engine successfully, we can start to modify the compilation steps of gen_snapshot to separate the generated data segments. The author uses the writeROData method in The AssemblyImageWriter to write data segments. So we need to redirect the writing of the data segment in this method:
// file: image_snapshot.cc
// AssemblyImageWriter Writes a method to ROData
void AssemblyImageWriter::WriteROData(NonStreamingWriteStream* clustered_stream,
bool vm) {
ImageWriter::WriteROData(clustered_stream, vm);
if (!EnterSection(ProgramSection::Data, vm, ImageWriter::kRODataAlignment)) {
return;
}
#if defined(TARGET_OS_MACOS_IOS)
WriteRODataToLocalFile(clustered_stream, vm); // Write to external files in ios. WriteRODataToLocalFile is a function that writes ROData to a file, as described below
#else
WriteBytes(clustered_stream->buffer(), clustered_stream->bytes_written());
#endif // TARGET_OS_MACOS_IOS
ExitSection(ProgramSection::Data, vm, clustered_stream->bytes_written());
}
Copy the code
In iOS, AssemblyImageWriter will not call the WriteBytes method, which writes the data to assembly_stream and finally to snapshot, but the WriteRODataToLocalFile method, Inside we need to redirect it, write it to a file and print it out.
// file: image_snapshot.cc
#include "bin/file.h"
#include <iostream>
void WriteRODataToLocalFile(NonStreamingWriteStream* 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) {
Syslog::PrintErr("Error: Unable to open file: %s\n", filename);
Dart_ExitScope(a);Dart_ShutdownIsolate(a);exit(255);
}
return file;
};
auto StreamingWriteCallback = [](void* callback_data,
const uint8_t* buffer,
intptr_t size) {
bin::File* file = reinterpret_cast < bin::File* >(callback_data);
if(! file->WriteFully(buffer, size)) {
Syslog::PrintErr("Error: Unable to write snapshot file\n");
Dart_ExitScope(a);Dart_ShutdownIsolate(a);exit(255); }};/ / ARM64 architecture
#if defined(TARGET_ARCH_ARM64)
// Define your own output path
bin::File *file = OpenFile(vm ? "{SNAPSHOT_SAVE_PATH}/arm64/VmSnapshotData.S" : "./SnapshotData/arm64/IsolateSnapshotData.S");
#else
/ / ARMV7 architecture
// Define your own output path
bin::File *file = OpenFile(vm ? "{SNAPSHOT_SAVE_PATH}/armv7/VmSnapshotData.S" : "./SnapshotData/armv7/IsolateSnapshotData.S");
#endif //end of TARGET_ARCH_ARM64
bin::RefCntReleaseScope rs(file);
StreamingWriteStream stream = StreamingWriteStream(512 * KB, StreamingWriteCallback, file);
intptr_t length = clustered_stream->bytes_written(a);// Get the length of the data segment
auto const start = reinterpret_cast<const uint8_t*>(clustered_stream->buffer()); // Get the starting position of the data segment
auto const end = start + length; // Get the end of the data segment
auto const end_of_words = start + Utils::RoundDown(length, compiler::target::kWordSize); // Get the end position of the last word in the data segment
// enumerate and write each word
/ / here is equal to the 'AssemblyImageWritter: : WriteBytes' method, this method is the data during the period of writing assembly_stream
The implementation of the two methods needs to be consistent
for (auto cursor = reinterpret_cast<const compiler::target::word*>(start);
cursor < reinterpret_cast<const compiler::target::word*>(end_of_words);
cursor++) {
word value = *cursor;
stream.Printf("%s 0x%.*" Px "\n", kWordDirective,
2 * compiler::target::kWordSize, value);
// Write different commands according to the architecture: '.quad XXXXX 'for 64-bit and '.long XXXXX' for 32-bit
}
if(end ! = end_of_words) {// Write the end
stream.Printf("%s", kSizeDirectives[kInt8SizeLog2]);
for (auto cursor = end_of_words;
cursor < end; cursor++) {
stream.Printf("%s 0x%.2x", cursor ! = end_of_words ?"," : "",
*cursor);
}
stream.Printf("\n");
}
#endif //end of TARGET_OS_MACOS_IOS
}
Copy the code
In the WriteRODataToLocalFile method, the file is written to StreamingWriteStream, and the architecture is separated. Finally, the file isolatesNapShotData. S and vmSnapshotdata. S are generated. The write method implementation needs to be the same as the WriteBytes method, which will result if inconsistent. Assembly language error in S file.
Here I will save the separated assembly file as {SNAPSHOT_SAVE_PATH}/armv7/ vmSnapshotdata. S, this is the save path I defined, you can modify according to your own needs, but must ensure that the directory exists, otherwise it will report the error of not finding the directory. It is recommended to use a compilation script to coordinate with the directory. The compilation script I used will be described below.
After the isolatesnapShotData. S and vmSnapshotData. S files are separated, they are only assembly language and cannot be used directly by Flutter. They need to be re-compiled into machine code to restore the binary form in app. framework/App:
echo ">>>>>> Compile SnapshotData" # armv7 xcrun cc -arch armv7 -c $armv7/IsolateSnapshotData.S -o $armv7 / HeadIsolateData. Dat xcrun cc - arch armv7 - c $armv7 / VmSnapshotData. S - o $armv7 / HeadVMData dat # remove excess head tail - c +313 $armv7/HeadIsolateData.dat > $armv7/IsolateData.dat tail -c +313 $armv7/HeadVMData.dat > $armv7/VMData.datCopy the code
After compiling to binary, there will be more header data, which needs to be removed by the tail instruction, otherwise it will not be recognized when loading. Finally, the generated isolateData. dat and VMdata. dat are the data segments we want.
4.3 Reloading data segments
Now that we have separated the data segments, how do we load the separated data segments? As mentioned above, dart_snapshot needs to obtain the data segment path from the setting file, so we need to add a new attribute to the setting file to store the path of the separated data segment:
[flutter_engine/src/flutter/common/setting.h]
// file: setting.h
// snapshot data path
std::string ios_vm_snapshot_data_path; // vm data path
std::string ios_isolate_snapshot_data_path; // isolate data path
Copy the code
Then we need to load dart_snapshot using our separate data segment path:
[flutter_engine/src/flutter/runtime/dart_snapshot.cc]
// file: dart_snapshot.cc
// Complete data segment reconstruction according to the separated data path
std::shared_ptr<const fml::Mapping> SnapshotDataMapping(const std::string &path) {
auto fd = fml::OpenFile(path.c_str(), false, fml::FilePermission::kRead);
if(! fd.is_valid()) {
auto directory = fml::paths::GetExecutableDirectoryPath(a);if(! directory.first) {return nullptr;
}
std::string path_to_executable = fml::paths::JoinPaths({directory.second, path});
fd = fml::OpenFile(path_to_executable.c_str(), false, fml::FilePermission::kRead);
}
if(! fd.is_valid()) {
return nullptr;
}
// The load succeeded
std::initializer_list<fml::FileMapping::Protection> protection = {fml::FileMapping::Protection::kRead};
// Map files
auto file_mapping = std::make_unique<fml::FileMapping>(fd, std::move(protection));
if (file_mapping->GetSize() != 0) {
return file_mapping;
}
return nullptr;
}
// Process the reconstruction of VMSnapshot data segment
static std::shared_ptr<const fml::Mapping> ResolveVMData(
const Settings& settings) {
#if DART_SNAPSHOT_STATIC_LINK
return std::make_unique<fml::NonOwnedMapping>(kDartVmSnapshotData, 0);
#else // DART_SNAPSHOT_STATIC_LINK
#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 SnapshotDataMapping(settings.ios_vm_snapshot_data_path); // The separate VM data segment path passed in the setting file
}
#else
// The original reconstruction method
return SearchMapping(
settings.vm_snapshot_data, // embedder_mapping_callback
settings.vm_snapshot_data_path, // file_path
settings.application_library_path, // native_library_path
DartSnapshot::kVMDataSymbol, // native_library_symbol_name
false // is_executable
);
#endif // OS_IOS
#endif // DART_SNAPSHOT_STATIC_LINK
}
// Process the reconstruction of IsolateSnapshot data segments
static std::shared_ptr<const fml::Mapping> ResolveIsolateData(
const Settings& settings) {
#if DART_SNAPSHOT_STATIC_LINK
return std::make_unique<fml::NonOwnedMapping>(kDartIsolateSnapshotData, 0);
#else // DART_SNAPSHOT_STATIC_LINK
#if OS_IOS
if (settings.ios_isolate_snapshot_data_path.empty()) {
return SearchMapping(
settings.isolate_snapshot_data, // embedder_mapping_callback
settings.isolate_snapshot_data_path, // file_path
settings.application_library_path, // native_library_path
DartSnapshot::kIsolateDataSymbol, // native_library_symbol_name
false // is_executable
);
} else {
return SnapshotDataMapping(settings.ios_isolate_snapshot_data_path); // Path to the isolated ISOLATE data segment passed in the setting file
}
#else
// The original reconstruction method
return SearchMapping(
settings.isolate_snapshot_data, // embedder_mapping_callback
settings.isolate_snapshot_data_path, // file_path
settings.application_library_path, // native_library_path
DartSnapshot::kIsolateDataSymbol, // native_library_symbol_name
false // is_executable
);
#endif // OS_IOS
#endif // DART_SNAPSHOT_STATIC_LINK
}
Copy the code
In the above code, we redefined a SnapshotDataMapping method, which will open the file from the given path and complete the reconstruction of the data segment. Therefore, in the iOS environment, we used the reconstruction method using ResolveVMData and ResolveIsolateData. Instead of the original reconstruction method, we achieved the purpose of using the separated data segment.
Finally, we need to expose the interface at the top level so that we can pass in the path of the data segment, so we add a new initialization method in the FlutterDartProject and a new FlutterSettingModel class, Path to the separated data segment, assets, icudat.dat:
[flutter_engine/src/flutter/shell/platform/darwin/ios/framework/Headers/FlutterDartProject.h]
// Add the FlutterSettingModel class to set the path of icudata and assets
FLUTTER_DARWIN_EXPORT
@interface FlutterSettingModel: NSObject
@property (nonatomic.copy.nullable) NSString *assetsPath; / / assets path
@property (nonatomic.copy.nullable) NSString *icuDataPath; // icudat.dat file path
@property (nonatomic.copy.nullable) NSString *vmDataPath; // Vm data segment path
@property (nonatomic.copy.nullable) NSString *isolateDataPath; // isolate Data segment path
@end
@interface FlutterDartPrject : NSObject./** * Add an initialization method, passing in FlutterSettingModel, to provide the path of data segments, etc. * @param settingModel Sets data, which can have the path of separated data segments, resource files, etc. */
- (instancetype)initWithPrecompiledDartBundle:(nullable NSBundle*)bundle flutterSetting:(FlutterSettingModel *)settingModel; .@end
Copy the code
Also add the FlutterSettingModel implementation and add the newly added initialization method implementation to the FlutterDartProject:
[flutter_engine/src/flutter/shell/platform/darwin/ios/framework/SourceFlutterDartProject.mm]
@implementation FlutterSettingModel
@end
@implementation FlutterDartProject. - (instancetype)initWithPrecompiledDartBundle:(nullable NSBundle *)bundle flutterSetting:(FlutterSettingModel *)settingModel {
if (self = [self initWithPrecompiledDartBundle:bundle]) {
#if(FLUTTER_RUNTIME_MODE ! = FLUTTER_RUNTIME_MODE_DEBUG)// This command is not used in debug mode
if (settingModel.vmDataPath.length > 0) {
_settings.ios_vm_snapshot_data_path = settingModel.vmDataPath.UTF8String;
}
if (settingModel.isolateDataPath.length > 0) {
_settings.ios_isolate_snapshot_data_path = settingModel.isolateDataPath.UTF8String;
}
if (settingModel.assetsPath.length > 0) {
_settings.assets_path = settingModel.assetsPath.UTF8String;
}
if (settingModel.icuDataPath.length > 0) {
_settings.icu_data_path = settingModel.icuDataPath.UTF8String;
}
#endif // FLUTTER_RUNTIME_MODE
}
return self;
}
Copy the code
Here I added the path without setting in DEBUG mode, because in DEBUG mode, the compiled product is in JIT mode, and app.xcFramework does not contain relevant data, so it cannot be used. This ensures that packages printed in DEBUG mode are still available, and only AOT product separation occurs in RELEASE mode.
Finally, all modifications are complete. Execute the compilation engine script to replace the engine used by Flutter.
4.4 Build scripts for Flutter products
In the above modification, we separated the data segment to a specific path. We also want to separate assets and icudat.dat from the production and store them together with our separated data segment, which can be uploaded to the cloud or compressed locally. Therefore, we need a unified compilation script to complete these transactions for us:
The first is the method definition:
#Updates the podSpec version number
updatePodsepcVersion(){
podPath=${1}
while read -r line
do
if [[ "$line" =~ .version ]]; then
array=(${line//"'"/ })
index=`expr ${#array[@]} - 1`
lastVersion=${array[$index]}
echo "${line} index=${index} lastVersion=${lastVersion}"
current_version=$(echo ${lastVersion} | awk -F. -v OFS=. 'NF==1{print ++$NF}; NF>1{if(length($NF+1)>length($NF))$(NF-1)++; $NF=sprintf("%0*d", length($NF), ($NF+1)%(10^length($NF))); print}')
echo "current_version = ${current_version}"
gsed -i "s/${line}/s.version = '${current_version}'/g" $podPath
fi
done < $podPath
}
#Collect all plugins and slim down the product
pluginCollect(){
res_plugins_build=${1}
target_plugins=${2}
files=$(ls $res_plugins_build)
for filename in $files; do
sourcePath=$res_plugins_build/${filename}
targetPath=$target_plugins/${filename}
frameworkName=$(echo ${filename//.xcframework/})
if [ -e $sourcePath ]; then
if [ -e $sourcePath ]; then
if [ $frameworkName != "App" -a $frameworkName != "Flutter" ]; then
cp -rf $sourcePath $targetPath
xcrunBitcode_strip $targetPath/ios-arm64_armv7/${frameworkName}.framework $frameworkName
fi
fi
fi
done
}
#The product was slimmed down, only arm64 architecture was used (because our project only supported ARM64), and bitcode was removed
xcrunBitcode_strip() {
framework_path=${1}
framework_name=${2}
cd ${framework_path}
lipo ${framework_name} -thin arm64 -output ${framework_name}
rm -rf arm64
xcrun bitcode_strip -r ${framework_name} -o ${framework_name}
}
#Compress separated data segments and other data
packUpROData() {
echo ">>>>>> zip snapshotData"
zip -q -r $target_dir/FlutterSnapshot.zip $flutter_reduce
mv $target_dir/FlutterSnapshot.zip $target_dir/FlutterSnapshot
}
#Upload data such as separated data segments to the server
uploadROData() {
echo ">>>>>> Will upload snapshotData"
#Here according to their own needs to upload
}
Copy the code
Several methods are defined here:
updatePodsepcVersion
: Because the product is integrated using CocoaPods, the podSpec version number of the product is updated every time the compiled script is executed to be able topod update
pluginCollect
This method is used to collect all frameworks except app. xcFramework and Flutter. Xcframework into plugins and slim them downxcrunBitcode_strip
: This method makes the product only use the ARM64 architecture, because our project currently only supports ARM64, you can modify it according to your needs, and it will be implemented at lastbitcode_strip
, removing bitcodepackUpROData
This method is used to perform the compression method, which compresses all data that needs to be separateduploadROData
This method will upload the compressed ZIP file to the server for download and use, and the implementation can be implemented as needed
Then there is the overall compilation process:
#Start building Flutter iOS
#I used FVM for version managementFVM use 2.0.3 FVM flutter --version
#Release the flutter build locklockFile="$FLUTTER_HOME/cache/lockfile" if [[ -a "$lockFile" ]]; then echo ">>>>>> Contains Lockfile !!" ; rm -f "$lockFile" fi fvm flutter clean fvm flutter packages get#Delete historical compilation artifacts
rm -rf build
#Definition of various directory locations
res_application=$PWD
res_build=$PWD/build
res_dir=$PWD/.ios/Flutter
res_flutter_plugins=$PWD/.flutter-plugins
res_source_dir=$res_dir/FlutterPluginRegistrant/Classes
res_release=$res_build/ios/framework/Release
target_dir=$PWD/LPEDU_Flutter_iOS
target_release_dir=$target_dir/Flutter_Release
target_release_plugins_dir=$target_release_dir/Plugins
target_podspec=$target_dir/LPEDU_Flutter_iOS.podspec
target_branch=master
#Create a directory location that separates data segmentsarmv7=./SnapshotData/armv7 arm64=./SnapshotData/arm64 flutter_reduce=./SnapshotData/flutter_reduce mkdir -p $armv7 mkdir -p $arm64 mkdir -p $flutter_reduce if [ -d "$target_release_dir" ]; then rm -rf $target_release_dir fi mkdir -p $target_release_dir cd $res_application#Executing build commands
fvm flutter build ios-framework --release --no-debug --no-profile
#Compile the separated data segment into the available binary
xcrun cc -arch armv7 -c $armv7/IsolateSnapshotData.S -o $armv7/HeadIsolateData.dat
xcrun cc -arch armv7 -c $armv7/VmSnapshotData.S -o $armv7/HeadVMData.dat
#Remove excess head
tail -c +313 $armv7/HeadIsolateData.dat > $armv7/IsolateData.dat
tail -c +313 $armv7/HeadVMData.dat > $armv7/VMData.dat
xcrun cc -arch arm64 -c $arm64/IsolateSnapshotData.S -o $arm64/HeadIsolateData.dat
xcrun cc -arch arm64 -c $arm64/VmSnapshotData.S -o $arm64/HeadVMData.dat
tail -c +313 $arm64/HeadIsolateData.dat > $arm64/IsolateData.dat
tail -c +313 $arm64/HeadVMData.dat > $arm64/VMData.dat
mkdir -p $flutter_reduce/arm64
# mkdir -p $flutter_reduce/armv7
#Not using ARMV7 snasphotData
# cp -rf $armv7/IsolateData.dat $flutter_reduce/armv7
# cp -rf $armv7/VMData.dat $flutter_reduce/armv7
cp -rf $arm64/IsolateData.dat $flutter_reduce/arm64
cp -rf $arm64/VMData.dat $flutter_reduce/arm64
#Separate the assets folder and icudat. Dat and place them in the Flutter_reduce directory as well
mv $res_release/App.xcframework/ios-arm64_armv7/App.framework/flutter_assets $flutter_reduce
mv $res_release/Flutter.xcframework/ios-armv7_arm64/Flutter.framework/icudtl.dat $flutter_reduce
#Defines the current version number, which is updated with the updatePodsepcVersion methodCurrent_version ='0.0.1' CD $target_DIR cp -rf $res_release/ app. xcframework $target_release_dir/ app. xcframework cp -rf $res_release/Flutter.xcframework $target_release_dir/Flutter.xcframework if [ -d "$target_release_plugins_dir" ]; then rm -rf $target_release_plugins_dir fi mkdir -p $target_release_plugins_dir
#Collect the plugin artifacts and update the podSpec artifacts
pluginCollect $res_release $target_release_plugins_dir
updatePodsepcVersion $target_podspec
xcrunBitcode_strip $target_release_dir/Flutter.xcframework/ios-armv7_arm64/Flutter.framework Flutter
xcrunBitcode_strip $target_release_dir/App.xcframework/ios-arm64_armv7/App.framework App
#Remove symbol table
xcrun dsymutil -o $target_release_dir/Flutter.xcframework/ios-armv7_arm64/Flutter.framework.DSYM
xcrun strip -x -S $target_release_dir/Flutter.xcframework/ios-armv7_arm64/Flutter.framework/flutter
rm -rf $target_dir/FlutterSnapshot.bundle
mkdir -p $target_dir/FlutterSnapshot
cd $res_application
#Compressed data
packUpROData
#Upload data
uploadROData $current_version
#Create fluttersnapshot. bundle to store the zip file for separated data
mv $target_dir/FlutterSnapshot $target_dir/FlutterSnapshot.bundle
rm -rf ./SnapshotData
lastCommit="version $current_version"
#Commit artifacts on Git
cd $target_dir
git add .
git commit -m "$lastCommit"
git push origin ${target_branch}
cd -
#Clear your workspace
if [ -d "$target_dir" ]; then
rm -rf $target_dir
fi
Copy the code
The whole compilation script is quite long and has several main points:
-
FVM is used for Flutter versioning, so all Flutter commands are preceded by FVM commands, which can be removed according to your actual situation
-
Create a directory location that separates data segments. It must be the same as the path defined in the image_snapshot.cc file, otherwise the directory will not be found
-
Assets and icudat.dat are also separated from the product, and placed together in the directory where the separated data segment resides, and then packaged and compressed together
-
Separate data can be loaded in two ways, depending on the project Flutter:
- Local compressed package: The product is delivered together with the local package
- Cloud compressed package: this package is not delivered with the product, but is decompressed and loaded after being downloaded from the cloud
-
Update the podspec of the artifact and finally push it to the Git repository
-
Since bundles are used to store compressed data in THE FORM of XCFramework, the podSpec product of Flutter should add the following statement to ensure that xcFramework and bundle files are introduced correctly:
s.resource = "FlutterSnapshot.bundle"
s.vendored_frameworks = 'Flutter_Release/**/*.{xcframework}'
Copy the code
4.5 Loading and Using Separated Data Segments on iOS
Finally, we need to properly initialize the engine on the iOS side and use our split product. Here we use the split product using local compression as an example:
Obtain the path of the compressed package and decompress it
NSURL *flutterBundleURL = [[NSBundle mainBundle] URLForResource:@"FlutterSnapshot" withExtension:@"bundle"];
if (flutterBundleURL) {
NSBundle *flutterBundle = [NSBundle bundleWithURL:flutterBundleURL];
self.bundleSnapshotPath = [flutterBundle pathForResource:@"FlutterSnapshot" ofType:@"zip"];
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentURL = [fileManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].firstObject;
self.snapshotSavePath = [documentURL URLByAppendingPathComponent:[NSString stringWithFormat:@"Flutter"]].relativePath;
if(! [fileManager fileExistsAtPath:self.snapshotSavePath]) {
[fileManager createDirectoryAtPath:self.snapshotSavePath withIntermediateDirectories:YES attributes:nil error:nil];
}
if (self.bundleSnapshotPath && [fileManager fileExistsAtPath:self.bundleSnapshotPath]) { // Use compressed packages
[SSZipArchive unzipFileAtPath:self.bundleSnapshotPath toDestination:self.snapshotSavePath delegate:self]; }}Copy the code
Create a settingModel based on the decompression path
#pragma mark - SSZipArchiveDelegate
- (void)zipArchiveDidUnzipArchiveAtPath:(NSString *)path zipInfo:(unz_global_info)zipInfo unzippedPath:(NSString *)unzippedPath
{
[self _setupFlutterWithSettingModel:[self _settingModelWithPath:unzippedPath]];
}
- (nullable FlutterSettingModel *)_settingModelWithPath:(NSString *)path
{
NSFileManager *fileManager = [NSFileManager defaultManager];
if ([fileManager fileExistsAtPath:path]) {
FlutterSettingModel *settingModel = [FlutterSettingModel new];
if defined(__arm64__)
settingModel.vmDataPath = [NSString stringWithFormat:@"%@/SnapshotData/flutter_reduce/arm64/VMData.dat", path];
settingModel.isolateDataPath = [NSString stringWithFormat:@"%@/SnapshotData/flutter_reduce/arm64/IsolateData.dat", path];
#else
return nil;
#endif
settingModel.assetsPath = [NSString stringWithFormat:@"%@/SnapshotData/flutter_reduce/flutter_assets", path];
settingModel.icuDataPath = [NSString stringWithFormat:@"%@/SnapshotData/flutter_reduce/icudtl.dat", path];
return settingModel;
}
return nil;
}
Copy the code
Start the engine using FlutterSettingModel
FlutterDartProject *project = [[FlutterDartProject alloc] initWithPrecompiledDartBundle:nil flutterSetting:settingModel];
self.flutterEngine = [[FlutterEngine alloc] initWithName:@"flutter-engine" project:project];
Copy the code
At this point, the loading step of Flutter product separation is completed
5. To summarize
The size of the Flutter package can be effectively reduced on iOS by separating the compiled data segments:
The name of the | The original size | The optimized | note |
App.xcframework | 7.2 M | 3.7 M | Separate data segments and Assest folders |
Flutter.xcframework | 8.5 M | 7.6 M | Remove symbol table and separate icudat.dat |
Total size of Flutter | 16.2 M | 11.8m (Cloud Delivered data segment) 13.5m (Locally compressed data segment) |
The final results are satisfactory, with cloud delivered data segment bringing about 27% and local compressed data segment bringing about 17%.
Follow my blog for more content
6. References
- flutter-platform-image
- Setting up the Engine development environment
- Introduction to Dart VM
- Review | how to cut close to 50% of the Flutter Flutter salon package volume
- A handy guide to separating flutter ios build products
- The Flutter machine code generates gen_snapshot