Flutter - iOS编译加速
# 一、前言
在项目完全重构成纯 Flutter
之后 ,iOS
端在 i7 Mac Mini
构建机上的打包时间差不多在 12分钟
左右,而在升级了 Xcode 16
之后,构建机的打包时间有了质的 “提升”
,来到了 25分钟
,换成 M1
来了也压不住,甚至更久~
这种情况在退回 Xcode 15
是可以解决的,但是这并不是长久之计,因为苹果早晚会强制要求升级的,好在申请了台 M4 Mac Mini
来打包,时间来到了 15 分钟
,不过随着业务功能不断迭代,构建时间也慢慢增加,目前来到了 17、18分钟
,但一旦哪天对 M4
构建机进行维护,让 i7
和 M1
顶上时,再加上多个打包任务并行,完成打包的时间动不动就得 半小时
起步,真的很令人绝望~
这里先给出优化后的打包时间
构建机 | 优化前(min) | 优化后(min) |
---|---|---|
i7 | 25+ | 14+ |
M4 | 16+ | 8+ |
- 优化前 :
Profile
+源码依赖
原生插件 - 优化后 :
Release
+二进制依赖
原生插件 - 环境 :
Xcode 16
# 二、编译模式对比
这里我拿了一个业务组件来做测试,分别使用 Xcode 15
和 Xcode 16
对 Profile
和 Release
两种模式来观察编译用时
版本 | Profile (s) | Release (s) |
---|---|---|
Xcode 15 | 389 | 384.6 |
Xcode 16 | 952.3 | 477.4 |
可以看到升级到 Xcode 16
后,两种模式的编译时间都比使用 Xcode 15
的要久,特别是 Profile
模式下的编译时间更离谱,是 Release
的 2倍
多~
而我们的项目为了方便,是以编译模式进行环境区分的。
Profile
: 测试包使用,对应kProfileMode
Release
: 上架包使用,对应kReleaseMode
# 三、调整环境判断
基于现状,只能调整项目中对环境的区分逻辑,改用 Dart Define
将环境参数传入。
这里使用 --dart-define-from-file
传递文件的方式
fvm spawn 3.24.5 build ipa --release --export-options-plist=path/to/ad_hoc.plist --dart-define-from-file=path/to/test.env
test.env
文件以键值对的方式设置环境变量
APP_ENV=test
取值方式如下,注意,一定要加上 const
!
/// dart define 环境变量
String get appEnv => const String.fromEnvironment('APP_ENV');
判断是否为 release
enum AppBuildMode {
release,
debug,
test,
}
AppBuildMode? fetchAppEnvType() {
switch (appEnv.toLowerCase()) {
case "debug":
return AppBuildMode.debug;
case "test":
return AppBuildMode.test;
case "release":
return AppBuildMode.release;
default:
return null;
}
}
bool isRelease() {
final envType = fetchAppEnvType();
if (envType == null) {
// 没有使用 dart define 设置环境变量
return kReleaseMode;
} else {
return AppBuildMode.release == envType;
}
}
# 四、浅探索耗时
当然,我们也可以尝试去探索一下,到底是哪里耗时这么久。
通过 Xcode
自身去查看编译耗时会发现最长的是 Run Script
,其主要负责编译 Flutter
侧的代码。
注:这里的时间是 Xcode 16 + Release 下的
但是展开详细内容会发现一点有用的信息都没有,无法定位到具体问题。
经过对 flutter_tools
的代码进行阅读后发现,可以通过设置环境变量 VERBOSE_SCRIPT_LOGGING
来使其加上 --verbose
参数,进而将打包过程中的一些信息打印出来。
具体操作: Runner
-> Build Phases
-> Run Script
中补充一句 export VERBOSE_SCRIPT_LOGGING=1
# 补充这一句
export VERBOSE_SCRIPT_LOGGING=1
/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build
再次编译就可以看到详细的 flutter
命令打包信息,可以将其导出后慢慢查看。
下面是摘出的主要耗时记录和文件大小
Profile
# Xcode 15
[ +2 ms] executing: xcrun cc -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-15.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.5.sdk -c /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/snapshot_assembly.S -o /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/snapshot_assembly.o
[+165207 ms] executing: xcrun clang -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-15.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.5.sdk -dynamiclib -Xlinker -rpath -Xlinker @executable_path/Frameworks -Xlinker -rpath -Xlinker @loader_path/Frameworks -fapplication-extension -install_name @rpath/App.framework/App -o /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/App.framework/App /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/snapshot_assembly.o
[ +289 ms] ...
[ +1 ms] executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-profile/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/80c2b96b2938ac2118bcd57be8744d2f/app.dill
[+96580 ms] ...
# Xcode 16
[ ] executing: xcrun cc -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-16.0.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk -c /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/snapshot_assembly.S -o /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/snapshot_assembly.o
[+596589 ms] executing: xcrun clang -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-16.0.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk -dynamiclib -Xlinker -rpath -Xlinker @executable_path/Frameworks -Xlinker -rpath -Xlinker @loader_path/Frameworks -fapplication-extension -install_name @rpath/App.framework/App -o /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/App.framework/App /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/snapshot_assembly.o
[ +290 ms] ...
[ +1 ms] executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-profile/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/503cc01726bac6c42836b48ae4a747ed/app.dill
[+92259 ms] ...
# Xcode 15、Xcode 16 一样
ls -lh
total 853368
drwxr-xr-x@ 3 lxf staff 96B 3 7 15:48 App.framework
drwxr-xr-x@ 3 lxf staff 96B 3 7 15:48 App.framework.dSYM
-rw-r--r--@ 1 lxf staff 323M 3 7 15:38 snapshot_assembly.S
-rw-r--r--@ 1 lxf staff 93M 3 7 15:48 snapshot_assembly.o
Release
# Xcode 15
[ ] executing: xcrun cc -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-15.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.5.sdk -c /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/snapshot_assembly.S -o /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/snapshot_assembly.o
[+92077 ms] executing: xcrun clang -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-15.4.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS17.5.sdk -dynamiclib -Xlinker -rpath -Xlinker @executable_path/Frameworks -Xlinker -rpath -Xlinker @loader_path/Frameworks -fapplication-extension -install_name @rpath/App.framework/App -o /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/App.framework/App /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/snapshot_assembly.o
[ +245 ms] ...
[ +1 ms] executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-release/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/a403cb206ef9086380afa3baff59c37e/app.dill
[+88256 ms] ...
# ========== 华丽的分割线 ========== #
# Xcode 16
[ ] executing: xcrun cc -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-16.0.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk -c /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/snapshot_assembly.S -o /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/snapshot_assembly.o
[+246277 ms] executing: xcrun clang -arch arm64 -miphoneos-version-min=12.0 -isysroot /Applications/Xcode-16.0.0.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk -dynamiclib -Xlinker -rpath -Xlinker @executable_path/Frameworks -Xlinker -rpath -Xlinker @loader_path/Frameworks -fapplication-extension -install_name @rpath/App.framework/App -o /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/App.framework/App /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/snapshot_assembly.o
[ +237 ms] ...
[ +1 ms] executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-release/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/6294397816f76932cad621f56d6b967b/app.dill
[+88139 ms] ...
# Xcode 15、Xcode 16 一样
ls -lh
total 572808
drwxr-xr-x@ 3 lxf staff 96B 3 7 17:08 App.framework
drwxr-xr-x@ 3 lxf staff 96B 3 7 16:02 App.framework.dSYM
-rw-r--r--@ 1 lxf staff 213M 3 7 17:04 snapshot_assembly.S
-rw-r--r--@ 1 lxf staff 67M 3 7 17:08 snapshot_assembly.o
你可能会觉得最耗时的是 xcrun clang
,但其实每一行前面的中括号内的时间,是上一行的命令的耗时,即 xcrun cc
最耗时,而其它命令的执行时间是差不多的。
xcrun cc
命令是用于将 Flutter
生成的汇编代码(snapshot_assembly.S
)编译为目标文件(snapshot_assembly.o
),不知道苹果使用的 clang
版本是有什么问题,在 Profile
下的编译时长是 Release
下的 2倍
多,它就是造成编译时间变长的主要原因,到这我就没继续往下研究了,有兴趣的小伙伴可以尝试研究看看。
除此之外,汇编文件 snapshot_assembly.S
的大小相差 100M+
,我们可以在日志中找到生成汇编代码的 gen_snapshot_*
命令,如下所示
executing: /Users/lxf/fvm/versions/3.24.5/bin/cache/artifacts/engine/ios-release/gen_snapshot_arm64 --deterministic --snapshot_kind=app-aot-assembly --assembly=/Users/lxf/.../.dart_tool/flutter_build/0385e340094e836ea63c75553c018e82/arm64/snapshot_assembly.S /Users/lxf/.../.dart_tool/flutter_build/0385e340094e836ea63c75553c018e82/app.dill
给 gen_snapshot_*
命令加上 --trace-compiler
标志并重新运行,让其提供每个函数的编译时间,并记录到 result.txt
中,精简命令如下
gen_snapshot_* --trace-compiler ... app.dill > result.txt 2>&1
result.txt
中的内容长这个样子
Precompiling optimized function: 'dart:core_StateError_StateError.' @ token 21950, size 52
--> 'dart:core_StateError_StateError.' entry: 0x108d00090 size: 56 time: 935 us
Precompiling optimized function: 'dart:core_RangeError_RangeError.' @ token 9976, size 94
--> 'dart:core_RangeError_RangeError.' entry: 0x108d000e0 size: 72 time: 133 us
...
根据 result.txt
中的耗时(time
)进行从大到小排序,并输出到 sorted_result.txt
中
grep '^-->' result.txt | awk '{for(i=1;i<=NF;i++) if($i=="time:") print $(i+1), $0}' | sort -nr | cut -d' ' -f2- > sorted_result.txt
排序后我们就可以清晰的知道哪些方法是比较耗时的,大家自行判断是否优化即可。
经过对比两个 sorted_result.txt
后发现,一些方法在 Profile
中存在而 Release
中没有,即发生了 Tree Shaking
。
在 Flutter
中,Tree Shaking
是一种优化技术,用于删除未使用的代码,以减小应用的大小并提高性能。对于不同的构建模式,Tree Shaking
的行为有所不同:
模式 | 描述 |
---|---|
Debug | 不会进行 Tree Shaking 。因为 Debug 模式主要用于开发和调试,保留所有代码和调试信息,以便于开发者进行调试。 |
Profile | 会进行部分 Tree Shaking 。主要用于性能分析,尽可能地优化代码,同时保留一些调试信息,以便开发者能分析性能问题。 |
Release | 会进行全面的 Tree Shaking 。会删除未使用的代码,并进行其他优化,以确保应用的体积尽可能小,并且性能最佳。 |
关于构建模式的详细说明,可以看官方文档 https://docs.flutter.dev/testing/build-modes (opens new window)
因此,如果我们希望最大限度地减少应用的体积并提高性能,建议在
Release
模式下构建Flutter
应用。
将 Profile
模式切到 Release
模式后的打包时间如下
构建机 | Profile (min) | Release (min) |
---|---|---|
i7 | 25+ | 18+ |
M4 | 16+ | 9+ |
可以看到,切换编译模式已经很大程度地优化了编译时长,不过我们还可以再进一步优化。
# 五、二进制依赖
二进制依赖
是 iOS
端老生常谈的优化点了,通过直接使用编译好的库或模块,从而避免编译的时间和资源消耗。
因此,原生插件越多,编译速度就越慢,二进制依赖的优化效果越好,二进制依赖的优化效果越好,编译速度就越快,所以编译越慢,编译越快 ~
在这里我使用的是 Rugby
这个工具。
# 安装
curl -Ls https://swiftyfinch.github.io/rugby/install.sh | bash
安装完成后输出如下内容
🏈 Rugby has been installed ✓
/Users/lxf/.rugby/clt is not in your $PATH
Add it manually to your shell profile.
For example, if you use zsh, run this command:
$ echo '\nexport PATH=$PATH:~/.rugby/clt' >> ~/.zshrc
Than open a new window or tab in the terminal for applying changes.
根据提示,将 rugby
添加到环境变量中。
完成后新开个终端,执行如下命令验证 rugby
是否可以被正常使用
rugby --version
# 输出
2.10.2
# 使用
在执行完 pod install
后,再执行 rugby cache
即可将原生插件从源码依赖转成二进制依赖了
rugby cache \
--arch arm64 \
--sdk ios \
--except chat_bottom_container realm dart_native \
--config Release
这里通过 --except
将一些不做二进制依赖的包过滤掉了。
当这些参数太多之后,命令会变得很长,不好看,可以将这些参数整理到 plans.yml
文件中
profile:
- command: cache
sdk: ios
config: Profile
except:
- chat_bottom_container
- realm
- dart_native
release:
- command: cache
sdk: ios
config: Release
except:
- chat_bottom_container
- realm
- dart_native
然后改为 rugby plan
去执行,并且指定使用 plans.yml
中的 release
rugby plan release -p /User/lxf/.../plans.yml
不过需要注意的是,如果你再次执行 pod install
将会还原为源码依赖!rugby
的修改就会失效~
而我们平时执行的 flutter build ipa
命令,其内部是有可能会去执行 pod install
的,那如何避免呢?
经过 flutter_tools
的源码阅读,发现它会做如下判断
- 涉及的文件是否存在
- 对比
pod_inputs.fingerprint
中的各项MD5
值 - 对比
Podfile.lock
与Pods/Manifest.lock
内容
pod_inputs.fingerprint
位于 build/ios
目录,内容如下
{
"files": {
"/Users/lxf/.../ios/Runner.xcodeproj/project.pbxproj": "21b527dc18081de6eabe26c6a4e851b2",
"/Users/lxf/.../ios/Podfile": "25baa69590b287fd88a578ae5fa2f964",
".../flutter/packages/flutter_tools/bin/podhelper.rb": "29abcfc3297c225fc1d1ae2380787cd6"
}
}
所以现在很明确,我们需要调整打包步骤
flutter pub get/upgrade
cd ios && pod install
- 切成二进制依赖
- 自己生成
pod_inputs.fingerprint
- 拷贝
Podfile.lock
至Pods/Manifest.lock
flutter build ipa
其中第 3
~ 第 5
步我已经做了封装在我的 https://github.com/LinXunFeng/condor (opens new window) 项目中,使用如下
# Condor
# 安装 condor
brew tap LinXunFeng/tap && brew install condor
# 指定编译模式
设置环境变量 CONDOR_BUILD_MODE
,对应 plans.yml
里的 profile
和 release
export CONDOR_BUILD_MODE=release
也可以使用 --mode
参数来指定模式
condor optimize-build --mode release
# 二进制依赖与同步文件
进入到 Flutter
项目的根目录,执行如下命令
cd path/to/your/flutter_project
condor optimize-build --config path/to/rugby/plans.yml
如果你想指定 fvm
安装的且非全局默认的 flutter
,则可以加上 --flutter
参数
condor optimize-build --config path/to/rugby/plans.yml --flutter "fvm spawn 3.24.5"
最后执行打包命令即可。
# 六、最后
希望苹果下一个版本的 Xcode
可以解决这个问题吧,不然的话,emmm,我也不会升级电脑的~

- 01
- Flutter - 子部件任意位置观察滚动数据11-24
- 02
- Flutter - 危!3.24版本苹果审核被拒!11-13
- 03
- Flutter - 轻松搞定炫酷视差(Parallax)效果09-21