1、背景
Flutter作为一款优秀的跨平台方案,我们Q音团队一致保持高度关注,团队内部也一直在努力促进Flutter的应用框架建设。在Q音直播接入Flutter的过程中,需要解决的首要问题便是”Flutter包体积变大”。本文将一步步剖析Flutter的包体积问题,带领大家探寻每一个可能的包体积优化点,结合实际项目和引擎源码,最终给出详细的包体积优化实现方案。欢迎大家相互交流Flutter相关技术。
1.1 Flutter混合开发模式
一般的如果我们想在现有原生App中加入Flutter,需要通过以下两种方式对Flutter进行引入:
- 将原生工程作为 Flutter 工程的子工程,由 Flutter 统一管理。这种模式,就是统一管理模式,即讲Flutter作为子工程集成到项目中。
- 将 Flutter 工程作为原生工程共用的子模块,维持原有的原生工程管理方式不变。这种模式,就是三端分离模式,即Flutter单独作为一个端进行开发。
统一管理模式搭建简单,但是缺点明显,不仅三端(Android、iOS、Flutter)代码耦合严重,相关工具链耗时也随之大幅增长,导致开发效率降低。所以这里使用三端代码分离的模式来进行依赖治理,实现了 Flutter 工程的轻量级接入。即 Android 侧使用 aar集成Flutter产物、iOS 使用 pod集成Flutter产物。
1.2 Flutter瘦身需求
当App引入Flutter带来一个明显问题,包体积增大。对于三端分离模式,包体积增量在Android上即为Flutter的aar产物,在iOS上表现为Flutter的framwork产物。因此,要解决包体积问题,需要对aar和framework的体积进行优化。
1.3 本文内容涉及的开发环境
- Flutter 1.17.1 • channel stable
- Mac OS X 10.15.4
- Xcode - develop for iOS and macOS (Xcode 11.4)
- Python 2.7.16
2、iOS framework产物分析
我们在实际工程中使用的是产物集成方式,Flutter代码会被编译打包成一个framework,即Flutter业务代码将会以framework的形式带入iOS宿主App。那么如何去对这个framework进行体积优化呢?下面我们首先对framework的内容进行详细分析。
2.1 framework结构
以Release模式下Futter产物为例,使用tree
命令查看Release目录结构,我们可以看到iOS产物为两个framework,其中App.framework是Dart业务代码产物,Flutter.framework是从Flutter SDK中拷贝过来的引擎产物。下图中,我给体积占比大的文件添加了说明,其它Headers、和plist文件大小可以忽略不计。
Release
├── App.framework
│ ├── App //AOT Snapshot数据,由我们的Dart业务代码编译而成,Mach-O格式的动态链接库
│ ├── Info.plist
│ └── flutter_assets //资源文件存放
└── Flutter.framework
├── Flutter //Flutter引擎,Mach-O格式的动态链接库
├── Headers
├── Info.plist
├── Modules
├── _CodeSignature
└── icudtl.dat //国际化支持相关文件
2.2 Framework体积分析(Release模式下)
我们将Release目录下的大文件以表格形式列出,这些文件即是我们去做体积优化的方向。
名称 | 大小 | 说明 |
---|---|---|
App | 7.3M | Dart业务代码AOT编译产物 |
flutter_assets | 2M | 图片、字体等资源文件 |
Flutter | 11M | 引擎 |
icudtl.dat | 884k | 国际化支持相关文件 |
其他第三方插件 | 800K | Flutter_boost等第三方插件 |
3、iOS减包思路
上一节我们分析了framework产物的组成占比,并且列出了体积最大的4个文件:App
、flutter_assets
、Flutter
、icudtl.dat
。下面我们继续对这4个文件进行深入分析,看看是否有优化空间。
优化思路分为3个方向
- 缩。即自我数据压缩,这种方法能够减少framework的体积,但是对最终app打包出来的体积影响较小,因为打包也是进行了数据压缩。
- 删。删除无用部分,或者不需要用到的部分。
- 挪。如果不能删除,考虑是否能分离出来,通过下载的方式动态去加载分离的部分。
3.1 App.framework/flutter_assets
flutter_assets
目录存放的资源文件,如果不想flutter_assets
带入App,我们可以将其移出,在运行需要时动态下载。移除方法具体有以下两个方法。
- 修改flutter_tools编译打包脚本。在生成framework过程中,就将flutter_asserts移除保存到别处,即生成的framework天生与flutter_asserts就是分开的。优点是得到即可用,隐藏中间过程。
- 生成framework后进行二次处理。在生成framework后,通过脚本将framework中的flutter_asserts移除。优点是不需要修改打包工具flutter_tools源码,缺点是增加了脚本对framework的处理,拉长了工作流程。
当前使用的是第二种方法,直接对产物进行二次处理,只为一个flutter_assets修改打包源码有点得不偿失。
移除flutter_assets
后对引擎启动是否有影响?查看源码发现flutter_assets
在FlutterDartProject.mm的DefaultSettingsForProcess函数中被使用,初始化过程中会在app目录检查是否有flutter_assets
,并且将路径保存在Settings的assets_path变量中,供引擎后续使用。因此我们得出结论:flutter_assets
是放在Framework内部,还是动态下载下来的,对程序运行没有影响,只要将flutter_assets
的正确位置的告知引擎即可。
//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:@""];//主目录检查assetsPath
} if (assetsPath.length == 0) {
NSLog(@"Failed to find assets path for "%@"", assetsName);
} else {
settings.assets_path = assetsPath.UTF8String; // Check if there is an application kernel snapshot in the assets directory we could
// potentially use. Looking for the snapshot makes sense only if we have a VM that can use
// it.
if (!flutter::DartVM::IsRunningPrecompiledCode()) {
NSURL* applicationKernelSnapshotURL =
[NSURL URLWithString:@(kApplicationKernelSnapshotFileName)
relativeToURL:[NSURL fileURLWithPath:assetsPath]]; if ([[NSFileManager defaultManager] fileExistsAtPath:applicationKernelSnapshotURL.path]) {
settings.application_kernel_asset = applicationKernelSnapshotURL.path.UTF8String;
} else {
NSLog(@"Failed to find snapshot: %@", applicationKernelSnapshotURL.path);
}
}
}
}
代码语言:javascript复制//这里列出settings.h中的相关Flutter配置代码struct Settings {
Settings();
Settings(const Settings& other);
~Settings(); //add by allentywang. dart data path.
std::string ios_vm_snapshot_data_path; //自定义 vm data 路径
std::string ios_isolate_snapshot_data_path; //自定义 isolate data 路径
//end
...
std::string icu_data_path; //这个是Flutter.framework中icudtl.dat的路径
// Assets settings
std::string assets_path; //这个是App.framework中flutter.assets的路径};
3.2 Flutter.framework/icudtl.dat
Flutter.framework中的icudtl.dat
保存了引擎的国际化支持信息。通过查看源码发现,icudtl.dat
在引擎初始化时被加载,依赖的只有Setting中的配置路径icu_data_path
。因此这个也可以挪走。icudtl.dat
大小固定为800多k,还是比较可观的。同样和flutter_assets
一样,如果移走了icudtl.dat
,我们需要在引擎初始化时指定外部的icudtl.dat
路径。
//settings.h中
// The icu_initialization_required setting does not have a corresponding
// switch because it is intended to be decided during build time, not runtime.
// Some companies apply source modification here because their build system
// brings its own ICU data files.
bool icu_initialization_required = true;
std::string icu_data_path; //icu路径
MappingCallback icu_mapper;
代码语言:javascript复制//观察shell.cc源码,得出结论,可以将icudtl移除,只需要在初始化时重新指定settings中icu_data_path的路径即可if (settings.icu_initialization_required) { if (settings.icu_data_path.size() != 0) {
fml::icu::InitializeICU(settings.icu_data_path);//我们可以修改settings.icu_data_path路径,达到加载保存在外部的icu目的
} 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
我们App.Framework产物下有两个比较大的文件,一个是前面提到的flutter_assets
,另外一个就是App
。flutter_assets
的优化前面已经讲过了,可以通过移除,然后动态下载的方式进行优化。那么App
这个文件能否采用同样的方式呢?下面我们进行逐步分析。
3.3.1 App是什么?
App是dart代码编译出来的可执行文件,App体积还是比较大的,我们先对App内容进行分析,下面是使用nm命令显示的App内容
代码语言:javascript复制nm App
...
000000000038c440 t Precompiled_int_init__int64OverflowLimits_0150898_11508
000000000038a2b0 t Precompiled_int_parse_11499
000000000038a178 t Precompiled_int_tryParse_11498
0000000000392ae8 t Precompiled_num_parse_11570
0000000000392a74 t Precompiled_num_tryParse_11569
0000000000018c54 t Precompiled_pragma_get_name_131
000000000065c008 b _kDartIsolateSnapshotBss
00000000003a1270 S _kDartIsolateSnapshotData
0000000000009000 T _kDartIsolateSnapshotInstructions
000000000065c000 b _kDartVmSnapshotBss
0000000000399400 S _kDartVmSnapshotData
0000000000004000 T _kDartVmSnapshotInstructions
U dyld_stub_binder
注意这里我省略了相当多的符号,如果只想看Dart VM Data相关产物,可以使用xcrun strip
命令进行过滤。自己研究的过程中发现Flutter1.9
的版本没有这些Precompiled符号内容,原因是在打包脚本$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh
中做了如下处理,而高版本的Flutter去掉了这些处理,保留了符号信息。
# 生成 dSYM 文件
RunCommand xcrun dsymutil -o "${build_dir}/dSYMs.noindex/App.framework.dSYM" "${app_framework}/App"
StreamOutput " ├─Stripping debug symbols..."
# 剥离调试符号表
RunCommand xcrun strip -x -S "${derived_dir}/App.framework/App"
StreamOutput "done"
过滤掉符号信息后,App中只剩下下面4个东西
- kDartIsolateSnapshotData
- kDarVmSnapshotData
- kDartIsolateSnapshotInstructions
- kDartVmSnapshotInstructions
这4个东西是什么?探究过程我就省略不细说了,这里直接说结论。kDartIsolateSnapshotData、kDarVmSnapshotData为可执行文件App
的数据段,kDartIsolateSnapshotInstructions、kDartVmSnapshotInstructions为代码段,由于iOS系统的限制,整个App
可执行文件不可以动态下发。但是kDartIsolateSnapshotData、kDartVmSnapshotData为数据段,它们在加载时不存在限制,可以动态加载。因此得出结论kDartIsolateSnapshotData和kDarVmSnapshotData可以挪到App外部,进行动态加载。
3.3.2 App内的kDartIsolateSnapshotData、kDarVmSnapshotData是如何生成的?
前面我们讲到kDartIsolateSnapshotData、kDarVmSnapshotData可以从App中移除,改为动态加载。那么它们是怎么被生成到App中的?我们又如何把它们从App中分离呢?
- 首先介绍一下Flutter虚拟机的运行模式。以iOS为例,Debug模式下Flutter的Dart虚拟机是JIT运行模式,JIT直接运行源码或者app.dill ,这也是Flutter热重载的原理。而在Release下,Flutter的Dart虚拟机是AOT运行模式,直接运行编译期编译好的机器码App。我们只能对Release模式下的App做文章,因为Debug模式下App包含很少的东西,里面没有可运行代码(这也是Debug的App.framework/App非常小的原因,使用nm查看App,发现里面什么都没有)。
- AOT产物是如何生成的?查阅了Flutter源码和相关资料,我们发现Dart代码会使用gen_snapshot工具来编译成.S文件,然后通过xcrun工具来进行汇编和链接最终生成App。整个编译过程关键分为两步,一个是gen_sanpshot 编译,另外一个是xcrun编译。其中
gen_snapshot
由Flutter engine提供,属于DartVm代码部分,我们完全可以定制gen_sanpshot
来改变App组成,进而达到减包的目的。
因此想要移除kDartIsolateSnapshotData、kDarVmSnapshotData,我们必须从gen_sanpshot
入手
3.3.3 Dart源码的编译与加载
数据段编译过程:这里借用网上Dart源码编译成App.framework的流程图
数据段运行时加载过程:App运行前,Dart 虚拟机需要加载保存在App中的数据和代码,为了得到可供虚拟机运行的DartVMData,引擎初始化时按照下面步骤依次调用相关代码来完成DartVMData的创建。下面我从引擎源码上追踪了数据段的完整加载过程。
- 第一步,DartVMData::Create 根据Setting中的配置,调用VMSnapshotFromSettings、IsolateSnapshotFromSettings std::shared_ptr<const DartVMData> DartVMData::Create( Settings settings, fml::RefPtr<DartSnapshot> vm_snapshot, fml::RefPtr<DartSnapshot> isolate_snapshot) { if (!vm_snapshot || !vm_snapshot->IsValid()) { // Caller did not provide a valid VM snapshot. Attempt to infer one // from the settings. vm_snapshot = DartSnapshot::VMSnapshotFromSettings(settings);//创建VM Snapshot if (!vm_snapshot) { FML_LOG(ERROR) << "VM snapshot invalid and could not be inferred from settings."; return {}; } } if (!isolate_snapshot || !isolate_snapshot->IsValid()) { // Caller did not provide a valid isolate snapshot. Attempt to infer one // from the settings. isolate_snapshot = DartSnapshot::IsolateSnapshotFromSettings(settings);//创建Isolate Snapshot if (!isolate_snapshot) { FML_LOG(ERROR) << "Isolate snapshot invalid and could not be inferred " "from settings."; return {}; } } return std::shared_ptr<const DartVMData>(new DartVMData( std::move(settings), // std::move(vm_snapshot), // std::move(isolate_snapshot) // ));//生成DartVMData}
- 第二步,VMSnapshotFromSettings、IsolateSnapshotFromSettings 这里的作用是完成数据段和代码段的重建 fml::RefPtr<DartSnapshot> DartSnapshot::VMSnapshotFromSettings( const Settings& settings) { TRACE_EVENT0("flutter", "DartSnapshot::VMSnapshotFromSettings"); auto snapshot = fml::MakeRefCounted<DartSnapshot>(ResolveVMData(settings), //数据段重建 ResolveVMInstructions(settings) //代码段重建 ); if (snapshot->IsValid()) { return snapshot; } return nullptr; }
- 第三步,ResolveVMData 调用SearchMapping创建符号Mappping 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 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 // DART_SNAPSHOT_STATIC_LINK}
- 第四步,SearchMapping 调用fml::NativeLibrary::Create读取文件 static std::shared_ptr<const fml::Mapping> SearchMapping( MappingCallback embedder_mapping_callback, const std::string& file_path, const std::vector<std::string>& native_library_path, const char* native_library_symbol_name, bool is_executable) { // Ask the embedder. There is no fallback as we expect the embedders (via // their embedding APIs) to just specify the mappings directly. if (embedder_mapping_callback) { return embedder_mapping_callback(); } // Attempt to open file at path specified. if (file_path.size() > 0) { if (auto file_mapping = GetFileMapping(file_path, is_executable)) { return file_mapping; } } // Look in application specified native library if specified. for (const std::string& path : native_library_path) { auto native_library = fml::NativeLibrary::Create(path.c_str());//通过NativeLibrary加载文件 auto symbol_mapping = std::make_unique<const fml::SymbolMapping>( native_library, native_library_symbol_name); if (symbol_mapping->GetMapping() != nullptr) { return symbol_mapping; } } // Look inside the currently loaded process. { auto loaded_process = fml::NativeLibrary::CreateForCurrentProcess(); auto symbol_mapping = std::make_unique<const fml::SymbolMapping>( loaded_process, native_library_symbol_name); if (symbol_mapping->GetMapping() != nullptr) { return symbol_mapping; } } return nullptr; }
- 第五步,NativeLibrary 最终调用dlopen去获取符号内容 NativeLibrary::NativeLibrary(const char* path) { ::dlerror(); handle_ = ::dlopen(path, RTLD_NOW);//实际上调用dlopen if (handle_ == nullptr) { FML_DLOG(ERROR) << "Could not open library '" << path << "' due to error '" << ::dlerror() << "'."; } }
最终结论:我们可以移除kDartIsolateSnapshotData、kDarVmSnapshotData放到App外部,然后通过修改ResolveVMData,在引擎初始化时,从外部文件中重建Dart虚拟机所需要的数据结构,达到缩减App体积的目的。
3.4 FLutter.framework/Flutter
Flutter.framework/Flutter为引擎产物,其大小是固定的,但是初始占比比较大。这部分能优化的空间很小,主要是通过裁剪引擎不需要的功能,减少体积。编译引擎时可以选择性编译skia和boringssl,收益大概只有几百K。
除此之外可以对Flutter的符号进行分离。
代码语言:javascript复制#将flutter的符号,存为.dSYM文件
xcrun dsymutil -o $frameworkpath/Flutter.framework.dSYM $frameworkpath/Release/Flutter.framework/flutter
#然后strip掉符号
xcrun strip -x -S $frameworkpath/Release/Flutter.framework/flutter
4、实践
前面都是在分析如何去iOS产物进行优化,接下来我们真正开始动手减包之路。
4.1 引擎编译配置
这部分涉及到的引擎编译部分知识,可自行参考相关资料,这里不再进行细节描述。以下列出相关编译脚本,和需要注意的事项。
在引擎源码目录下创建以下脚本
- debug模式
#!/bin/bash
#ios_debug.sh
#需要在环境变量中添加export FLUTTER_SDK=“your flutter install path”
#引擎编译很耗内存,电脑性能吃紧的把10调低一点
#构建iOS debug 引擎
Echo “当前目录在:”
pwd
# 构建模拟器
Echo "构建iOS debug 模拟器"
./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
Echo ""
# 构建armv7
Echo "构建iOS debug arm"
./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
Echo ""
# 构建arm64
Echo "构建iOS debug arm64"
./flutter/tools/gn --unoptimized --runtime-mode debug --ios-cpu arm64
./flutter/tools/gn --unoptimized --ios --runtime-mode debug --ios-cpu arm64
ninja -C out/host_debug_unopt -j 10
ninja -C out/ios_debug_unopt -j 10
Echo ""
#归档
Echo "开始归档"
rm -rf tmp/*
flutter_lipo=./arch_file
buidmode=ios
cp -rf out/ios_debug_unopt/Flutter.framework tmp/
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
#cd tmp
#zip -r Flutter.framework.zip Flutter.framework
#cd ..
#mkdir -p "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"
#cp -f tmp/Flutter.framework.zip "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/
cp -rf tmp/Flutter.framework "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/
#copy gen_snapshot
cp -f out/ios_debug_unopt/clang_x64/gen_snapshot "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/gen_snapshot_arm64
cp -f out/ios_debug_unopt_arm/clang_x64/gen_snapshot "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/gen_snapshot_armv7
Echo "归档完毕"
- Profile模式
#!/bin/bash
#ios_profile.sh
#需要在环境变量中添加export FLUTTER_SDK=“your flutter install path”
#构建iOS debug 引擎
Echo “当前目录在:”
pwd
# 构建arm
Echo "构建iOS profile arm"
#./flutter/tools/gn --runtime-mode profile --ios-cpu arm
#ninja -C out/host_profile_arm -j 6
./flutter/tools/gn --ios --runtime-mode profile --ios-cpu arm
ninja -C out/ios_profile_arm -j 6
echo ""
# 构建arm64
Echo "构建iOS profile arm64"
#./flutter/tools/gn --runtime-mode profile --ios-cpu arm64
#ninja -C out/host_profile -j 6
./flutter/tools/gn --ios --runtime-mode profile --ios-cpu arm64
ninja -C out/ios_profile -j 6
echo ""
Echo "开始归档"
rm -rf tmp/*
flutter_lipo=./arch_file
buidmode=ios-profile
cp -rf out/ios_profile/Flutter.framework tmp/
lipo -create -output tmp/Flutter.framework/Flutter
out/ios_profile/Flutter.framework/Flutter
out/ios_profile_arm/Flutter.framework/Flutter
"${flutter_lipo}"/flutter-profile-x86_64
#cd tmp
#zip -r Flutter.framework.zip Flutter.framework
#cd ..
#mkdir -p "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"
#cp -f tmp/Flutter.framework.zip "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/
cp -rf tmp/Flutter.framework "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/
#copy gen_snapshot
cp -f out/ios_profile/clang_x64/gen_snapshot "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/gen_snapshot_arm64
cp -f out/ios_profile_arm/clang_x64/gen_snapshot "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/gen_snapshot_armv7
- Release模式
#!/bin/bash
#ios_release.sh
#需要在环境变量中添加export FLUTTER_SDK=“your flutter install path”
#构建iOS Release 引擎
Echo “当前目录在:”
pwd
# 构建arm
Echo "构建iOS release armv7"
#./flutter/tools/gn --runtime-mode release --ios-cpu arm
#ninja -C out/host_release_arm -j 6
./flutter/tools/gn --ios --runtime-mode release --ios-cpu arm
ninja -C out/ios_release_arm -j 6
echo ""
# 构建arm64
Echo "构建iOS release arm64"
#./flutter/tools/gn --runtime-mode release --ios-cpu arm64
#ninja -C out/host_release -j 6
./flutter/tools/gn --ios --runtime-mode release --ios-cpu arm64
ninja -C out/ios_release -j 6
echo ""
Echo "合并归档"
rm -rf tmp/*
flutter_lipo=./arch_file
buidmode=ios-release
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
"${flutter_lipo}"/flutter-release-x86_64
#cd tmp
#zip -r Flutter.framework.zip Flutter.framework
#cd ..
#mkdir -p "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"
#cp -f tmp/Flutter.framework.zip "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/
cp -rf tmp/Flutter.framework "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/
#copy gen_snapshot
cp -f out/ios_release/clang_x64/gen_snapshot "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/gen_snapshot_arm64
cp -f out/ios_release_arm/clang_x64/gen_snapshot "${FLUTTER_SDK}"/bin/cache/artifacts/engine/"${buidmode}"/gen_snapshot_armv7
Echo "归档完毕"
其他详细编译参数,使用./flutter/tools/gn —help命令查看。
另外flutter引擎默认不支持bitcode,如果需要支持,需要在编译脚本后添加—bitcode
代码语言:javascript复制#例如,添加--bitcode,最终的引擎产物将包含bitcode
./flutter/tools/gn --ios --runtime-mode release --ios-cpu arm --bitcode
4.2 xcode配置引擎调试环境
- 以Debug模式为例,找到引擎源码目录src/out/ios_debug_unopt下的products.xcodeproj。该工程为引擎源码对应的iOS工程
- 打开flutter工程下的./iOS/Runner.xcworkspace。该目录由Flutter SDK 中的flutter_tools自动生成,保存了运行flutter所需要的iOS宿主模板Runner工程
- 把products.xcodeproj拖入到Runner项目中,并在Generated.xcconfig中添加相关环境变量
#以使用ios_debug_unopt引擎为例,添加如下代码,即可调试引擎
FLUTTER_FRAMEWORK_DIR=/Users/wangtengyu/allenwork/engine/src/out/ios_debug_unopt
LOCAL_ENGINE=ios_debug_unopt
FLUTTER_ENGINE=/Users/wangtengyu/allenwork/engine/src
4.3 减包引擎源码修改
4.3.1 添加自定义配置,Settings
Settings是一个重要的配置结构体,位于src/flutter/common/settings.h中,这个公共头文件被大量使用。我们可以在settings.h添加我们自己的代码,来实现想要的功能。
为了自定义DartVMData加载路径,我们在settings结构体中添加了2个string成员用来保存vm和isolate数据文件路径。
代码语言:javascript复制struct Settings {
Settings();
Settings(const Settings& other);
~Settings(); //add by allentywang. dart data path.
std::string ios_vm_snapshot_data_path; // vm data path
std::string ios_isolate_snapshot_data_path; // isolate data path
//end
...
...
}
4.3.2 修改Dart编译工具,gen_snapshot
Dart业务代码使用gen_snapshot工具编译到App中,在程序运行时通过引擎内部的虚拟机加载App中的Dart编译代码。下面从写入和读取这两个方面介绍,如何分离Dart编译产物的数据段。
重定向App数据段的写入
代码语言:javascript复制//文件:image_snapshot.cc//添加头文件#include "bin/file.h"#include <iostream>//添加写入函数//add by allen. 把stream写到本地文件void WriteTextToLocalFile(WriteStream* clustered_stream, bool vm){#if defined(TARGET_OS_MACOS_IOS) //add by allentywang
auto OpenFile = [](const char* filename){
Syslog::Print("open file : %sn", filename);
bin::File* file = bin::File::Open(NULL, filename, bin::File::kWriteTruncate); if (file == NULL) {
Syslog::PrintErr("Error: Unable to write file: %sn", filename);
Dart_ExitScope();
Dart_ShutdownIsolate(); 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 filen");
Dart_ExitScope();
Dart_ShutdownIsolate(); exit(255);
}
};#if defined(TARGET_ARCH_ARM64)
printf("this is arm64n");
bin::File *file = OpenFile(vm ? "./SnapshotData/arm64/VmSnapshotData.S" : "./SnapshotData/arm64/IsolateSnapshotData.S");#else//#if defined(TARGET_ARCH_ARM)
printf("this is armv7n");
bin::File *file = OpenFile(vm ? "./SnapshotData/armv7/VmSnapshotData.S" : "./SnapshotData/armv7/IsolateSnapshotData.S");#endif //end of TARGET_ARCH_ARM64
bin::RefCntReleaseScope rs(file);
StreamingWriteStream stream = StreamingWriteStream(512 * KB, StreamingWriteCallback, file);
uword buffer = reinterpret_cast<uword>(clustered_stream->buffer());
intptr_t length = clustered_stream->bytes_written();
uword start = buffer;
uword end = buffer length; auto const end_of_words =
Utils::RoundDown(end, sizeof(compiler::target::uword)); for (auto cursor = reinterpret_cast< compiler::target::uword* >(start);
cursor < reinterpret_cast< compiler::target::uword* >(end_of_words);
cursor ) { #if defined(TARGET_ARCH_IS_64_BIT)
stream.Print(".quad 0x%0.16" Px "n", *cursor); #else
stream.Print(".long 0x%0.8" Px "n", *cursor); #endif
} if (end != end_of_words) { auto start_of_rest = reinterpret_cast< const uint8_t* >(end_of_words);
stream.Print(".byte "); for (auto cursor = start_of_rest;
cursor < reinterpret_cast< const uint8_t* >(end); cursor ) { if (cursor != start_of_rest) stream.Print(", ");
stream.Print("0x%0.2" Px "", *cursor);
}
stream.Print("n");
}#endif //end of TARGET_OS_MACOS_IOS}
...//修改数据段写入
void AssemblyImageWriter::WriteText(WriteStream* clustered_stream, bool vm) {
...#if defined(TARGET_OS_MACOS_IOS)
WriteTextToLocalFile(clustered_stream, vm);//ios下写到外部文件中,WriteTextToLocalFile为上面我们自己的写入函数#else
const char* data_symbol =
vm ? "_kDartVmSnapshotData" : "_kDartIsolateSnapshotData";
assembly_stream_.Print(".globl %sn", data_symbol);
Align(kMaxObjectAlignment);
assembly_stream_.Print("%s:n", data_symbol);
uword buffer = reinterpret_cast<uword>(clustered_stream->buffer());
intptr_t length = clustered_stream->bytes_written();
WriteByteSequence(buffer, buffer length);
Syslog::Print("write file : %sn", data_symbol);#endif //end of TARGET_OS_MACOS_IOS}
注意:要根据不同架构,分别保存VmSnapshotData.S和IsolateSnapshotData.S。架构区分宏定义为TARGET_ARCH_ARM
上面我们分离出了VmSnapshotData.S和IsolateSnapshotData.S,接下来需要将其编译成机器代码IsolateData.dat和VMData.dat
代码语言:javascript复制#以armv7的VmSnapshotData.S和IsolateSnapshotData.S为例,arm64类同
armv7=./SnapshotData/armv7
echo "编译数据段"
xcrun cc -arch armv7 -c $armv7/IsolateSnapshotData.S -o $armv7/HeadIsolateData.dat
xcrun cc -arch armv7 -c $armv7/VmSnapshotData.S -o $armv7/HeadVMData.dat
# 去掉多余头部
tail -c 313 $armv7/HeadIsolateData.dat > $armv7/IsolateData.dat
tail -c 313 $armv7/HeadVMData.dat > $armv7/VMData.dat
IsolateData.dat和VMData.dat为我们最终想要分离的数据段
自定义App数据段的重建
根据前面3.3.3节的分析,重建数据段,我们可以从ResolveVMData入手,下面是修改的代码。
代码语言:javascript复制//文件:dart_snapshot.cc//新增自定义重建函数//add by allentywang. create data Mappingstd::shared_ptr<const fml::Mapping> SetupMapping(const std::string &path) { // Check if the path exists and it readable directly.
auto fd = fml::OpenFile(path.c_str(), false, fml::FilePermission::kRead); // Check the path relative to the current executable.
if (!fd.is_valid()) { auto directory = fml::paths::GetExecutableDirectoryPath(); if (!directory.first) { return nullptr;
}
std::string path_relative_to_executable = fml::paths::JoinPaths({directory.second, path});
fd = fml::OpenFile(path_relative_to_executable.c_str(), false,
fml::FilePermission::kRead);
} if (!fd.is_valid()) { return nullptr;
}
std::initializer_list<fml::FileMapping::Protection> protection = {fml::FileMapping::Protection::kRead}; auto file_mapping = std::make_unique<fml::FileMapping>(fd, std::move(protection)); if (file_mapping->GetSize() != 0) { return file_mapping;
} return nullptr;
}//end//修改原来的ResolveVMData和ResolveIsolateData函数。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 //add by allentywang
if (settings.ios_vm_snapshot_data_path.empty()) {//ios
printf("ResolveVMData from localn"); 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
);
} else { printf("ResolveVMData from settings.ios_vm_snapshot_data_pathn"); return SetupMapping(settings.ios_vm_snapshot_data_path);
}#else
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#endif // DART_SNAPSHOT_STATIC_LINK}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_IOSif (settings.ios_isolate_snapshot_data_path.empty()) { printf("ResolveVMData from localn"); 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 { printf("ResolveVMData from settings.ios_isolate_snapshot_data_pathn"); return SetupMapping(settings.ios_isolate_snapshot_data_path);
}#elsereturn 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#endif // DART_SNAPSHOT_STATIC_LINK}
要使代码生效,我们还需要指定正确的settings.ios_isolate_snapshot_data_path
路径,这个路径变量就是之前在settings
结构体中新增的配置。接下来路径指定需要在DefaultSettingsForProcess
函数中完成。
4.3.4 修改引擎配置的加载,DefaultSettingsForProcess
引擎初始化时,会调用DefaultSettingsForProcess
函数,来初始化Settings
配置。由于我们删除了framework内部的flutter_assets
和icudtl.dat
,分离了App数据段IsolateData.dat
和VMData.dat
。接下来我们需要对这4个产物路径进行重新设置,让引擎能够顺利初始化。代码如下
// 文件:FlutterDartProject.mm//新增函数initSettingvoid initSettings(flutter::Settings &settings){ printf("%s %s:%dn", __FUNCTION__, __FILE__, __LINE__); // 检查资源目录,如果找到对应资源,则保存路径到Settings#if (FLUTTER_RUNTIME_MODE != FLUTTER_RUNTIME_MODE_DEBUG)//debug不做任何处理,只针对profile和release
printf("settings.icu_data_path = %sn", settings.icu_data_path.c_str()); // flutter_assets
printf("settings.assets_path = %sn", settings.assets_path.c_str());#if 1 //defined(TARGET_ARCH_ARM64) 架构区分在中有相关宏定义
// VMData.dat
NSString *vmDataPath = [[NSBundle mainBundle] pathForResource:@"VMData" ofType:@"dat"]; if (vmDataPath == nil)
{
NSLog(@"can not found VMData.dat from resource, download from server?");
}else{
NSLog(@"check vmDataPath from resource: %@", vmDataPath);
settings.ios_vm_snapshot_data_path = vmDataPath.UTF8String;
} // IsolateData.dat
NSString *isolateDataPath = [[NSBundle mainBundle] pathForResource:@"IsolateData" ofType:@"dat"]; if (vmDataPath == nil)
{
NSLog(@"can not found IsolateData.dat from resource, download from server?");
}else{
NSLog(@"check isolateDataPath from resource: %@", isolateDataPath);
settings.ios_isolate_snapshot_data_path = isolateDataPath.UTF8String;
}#else
printf("arch is armv7n");#endif
// icudtl.dat
NSString *icudtlPath = [[NSBundle mainBundle] pathForResource:@"icudtl" ofType:@"dat"]; if (icudtlPath == nil)
{
NSLog(@"can not found icudtl.dat from resource, download from server?");
}else{
NSLog(@"check icudtl from resource: %@", icudtlPath);
settings.icu_data_path = icudtlPath.UTF8String;
} // flutter_assets// settings.assets_path = assetsPath.UTF8String;
// end#endif}
... static flutter::Settings DefaultSettingsForProcess(NSBundle* bundle = nil) { auto command_line = flutter::CommandLineFromNSProcessInfo(); // Precedence:
// 1. Settings from the specified NSBundle.
// 2. Settings passed explicitly via command-line arguments.
// 3. Settings from the NSBundle with the default bundle ID.
// 4. Settings from the main NSBundle and default values.
NSBundle* mainBundle = [NSBundle mainBundle];
NSBundle* engineBundle = [NSBundle bundleForClass:[FlutterViewController class]]; bool hasExplicitBundle = bundle != nil; if (bundle == nil) {
bundle = [NSBundle bundleWithIdentifier:[FlutterDartProject defaultBundleIdentifier]];
} if (bundle == nil) {
bundle = mainBundle;
} auto settings = flutter::SettingsFromCommandLine(command_line);
initSettings(settings);//获取到settings后,调用initSettings函数对setting进行修改,将4个路径改到实际对应的位置
...
}
完成后,重新编译引擎,替换引擎即可。
4.4 Flutter侧产物打包脚本
注意脚本中使用CloseBitcode
函数关闭了bitcode,让flutter build ios-framework
命令生成的framework不带bitcode。因为xcode工程默认是开启bitcode的,而前面我们的引擎产物没有加--bitcode
参数,不关闭xcode的bitcode选项是无法编译成功的。
脚本过程说明就省略了,这里直接贴上代码
代码语言:javascript复制#ios_build_reduce.sh
#检查路径是否存在,不存在就退出脚本
AssertExists() {
if [[ ! -e "$1" ]]; then
if [[ -h "$1" ]]; then
echo "The path $1 is a symlink to a path that does not exist"
else
echo "The path $1 does not exist"
fi
exit -1
fi
return 0
}
#要关闭bitcode功能,必须修改.ios下的pods配置
CloseBitcode () {
str1="#this is generate from ios_build_reduce.sh to set 'ENABLE_BITCODE' = 'NO' for all targets of pods build setting n
post_install do |installer|n
installer.pods_project.targets.each do |target|n
target.build_configurations.each do |config|n
config.build_settings['ENABLE_BITCODE'] = 'NO'n
endn
endn
end"
echo "$str1" >> $1
}
#运行脚本,开启VERBOSE_SCRIPT_LOGGING时提供命令记录
RunCommand() {
if [[ -n "$VERBOSE_SCRIPT_LOGGING" ]]; then
echo "♦ $*" #show script
fi
"$@" #run
return $? #return last result
}
#VERBOSE_SCRIPT_LOGGING="开启脚本记录"
echo "iOS Flutter 产物集成 start!!!!!!"
cd ..
#bitcode需要手动配置.ios下的Pods工程,不能clean
flutter clean
echo "flutter clean done !!!"
flutter pub get
echo "flutter pub get done !!!"
#初始化目录
armv7=./SnapshotData/armv7
arm64=./SnapshotData/arm64
mkdir -p $armv7
mkdir -p $arm64
frameworkpath='build/ios/framework'
#rm $frameworkpath/FlutterPackage.zip
#rm -rf $frameworkpath/flutter_reduce
mkdir -p $frameworkpath/flutter_reduce/arm64
mkdir -p $frameworkpath/flutter_reduce/armv7
#修改podfile
podfile='./.ios/Podfile'
AssertExists "$podfile"
CloseBitcode "$podfile"
#编译framework
flutter build ios-framework
echo "flutter-framework 打包成功 !!!"
debugpath='build/ios/framework/Debug'
releasepath='build/ios/framework/Release'
libpath='../../ios/LocalLib/Flutter'
rm -rf "$libpath/Debug"
rm -rf "$libpath/Release"
echo "编译数据段"
xcrun cc -arch armv7 -c $armv7/IsolateSnapshotData.S -o $armv7/HeadIsolateData.dat
xcrun cc -arch armv7 -c $armv7/VmSnapshotData.S -o $armv7/HeadVMData.dat
# 去掉多余头部
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
cp -rf $armv7/IsolateData.dat $frameworkpath/flutter_reduce/armv7
cp -rf $armv7/VMData.dat $frameworkpath/flutter_reduce/armv7
cp -rf $arm64/IsolateData.dat $frameworkpath/flutter_reduce/arm64
cp -rf $arm64/VMData.dat $frameworkpath/flutter_reduce/arm64
echo "移除flutter_asserts"
mv $releasepath/App.framework/flutter_assets $frameworkpath/flutter_reduce/
echo "移除icudtl.dat"
mv $releasepath/Flutter.framework/icudtl.dat $frameworkpath/flutter_reduce/
echo "压缩分离产物FlutterPackage.zip"
zip -q -r $frameworkpath/FlutterPackage.zip $frameworkpath/flutter_reduce
echo "framework优化体积:"
printf "未压缩总量:%s----%sn" `du -sh $frameworkpath/flutter_reduce`
printf " t├─%s----%sn" `du -sh $frameworkpath/flutter_reduce/armv7`
printf " t├─%s----%sn" `du -sh $frameworkpath/flutter_reduce/arm64`
printf " t├─%s----%sn" `du -sh $frameworkpath/flutter_reduce/icudtl.dat`
printf " t├─%s----%sn" `du -sh $frameworkpath/flutter_reduce/flutter_assets`
RunCommand printf "压缩后总量:%s----%sn" `du -sh $frameworkpath/FlutterPackage.zip`
printf "n"
printf "flutter(armv7、arm64) strip之前:%s----%sn" `du -sh $frameworkpath/Release/Flutter.framework/flutter`
AssertExists "${frameworkpath}"
xcrun dsymutil -o $frameworkpath/Flutter.framework.dSYM $frameworkpath/Release/Flutter.framework/flutter
echo "分离符号:${frameworkpath}/Flutter.framework.dSYM"
xcrun strip -x -S $frameworkpath/Release/Flutter.framework/flutter
printf "flutter(armv7、arm64) strip之后:%s----%sn" `du -sh $frameworkpath/Release/Flutter.framework/flutter`
4.5 iOS侧Pod集成
iOS宿主工程Podfile添加如下代码。使用FlutterDebug.podspec和FlutterRelease.podspec来引入flutter的framework产物
代码语言:javascript复制# 是否源码集成, 0:否 / 1:是
IsFlutterSourceCode = 0
# 集成 FlutterModule
def FlutterModuleIntegration
if IsFlutterSourceCode == 1
# 源码集成(官方方案)
flutter_application_path = '../native_modules/mlive/'
load File.join(flutter_application_path, '.ios', 'Flutter', 'podhelper.rb')
install_all_flutter_pods(flutter_application_path)
else
# 区分debug/release场景使用不同的调试包
pod 'FlutterDebug' ,:configurations => ['Debug'] ,:path => 'LocalLib/Flutter'
pod 'FlutterRelease' ,:configurations => ['Release', 'AppStore', 'iAP'] ,:path => 'LocalLib/Flutter'
end
end
4.6 最终效果
名称 | 优化方式 | 减包收益 |
---|---|---|
icudtl.dat | 下载 | 884k |
flutter_assets | 下载 | 2.1M |
App数据段 | 下载 | 单架构平均2.8M |
flutter | strip调试符号 | 单架构平均6M |
framework总计收益 | —- | 11.7M |
size Report最终收益 | —- | —- |
总结:我们通过删除不必要的文件、移走部分文件改为下发、去掉Flutter的符号文件、引擎大小优化等措施,使iOS接入Flutter的体积成本降到10M。
5、参考文章
- 字节跳动- 【如何缩减接近 50% 的 Flutter 包体积】:https://juejin.im/post/5de8a32c51882512664affa4
QQ音乐招聘Android/ios客户端开发,点击左下方“查看原文”投递简历~
也可将简历发送至邮箱:tmezp@tencent.com