Flutter - iOS编译加速

欢迎关注微信公众号:FSA全栈行动 👋

一、前言

在项目完全重构成纯 Flutter 之后 ,iOS 端在 i7 Mac Mini 构建机上的打包时间差不多在 12分钟 左右,而在升级了 Xcode 16 之后,构建机的打包时间有了质的 "提升",来到了 25分钟,换成 M1 来了也压不住,甚至更久~

这种情况在退回 Xcode 15 是可以解决的,但是这并不是长久之计,因为苹果早晚会强制要求升级的,好在申请了台 M4 Mac Mini 来打包,时间来到了 15 分钟,不过随着业务功能不断迭代,构建时间也慢慢增加,目前来到了 17、18分钟,但一旦哪天对 M4 构建机进行维护,让 i7M1 顶上时,再加上多个打包任务并行,完成打包的时间动不动就得 半小时 起步,真的很令人绝望~

这里先给出优化后的打包时间

构建机 优化前(min) 优化后(min)
i7 25+ 14+
M4 16+ 8+
  • 优化前 : Profile + 源码依赖 原生插件
  • 优化后 : Release + 二进制依赖 原生插件
  • 环境 : Xcode 16

二、编译模式对比

这里我拿了一个业务组件来做测试,分别使用 Xcode 15Xcode 16ProfileRelease 两种模式来观察编译用时

版本 Profile (s) Release (s)
Xcode 15 389 384.6
Xcode 16 952.3 477.4

可以看到升级到 Xcode 16 后,两种模式的编译时间都比使用 Xcode 15 的要久,特别是 Profile 模式下的编译时间更离谱,是 Release2倍 多~

而我们的项目为了方便,是以编译模式进行环境区分的。

  • Profile : 测试包使用,对应 kProfileMode
  • Release : 上架包使用,对应 kReleaseMode

三、调整环境判断

基于现状,只能调整项目中对环境的区分逻辑,改用 Dart Define 将环境参数传入。

这里使用 --dart-define-from-file 传递文件的方式

shell 复制代码
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 文件以键值对的方式设置环境变量

ini 复制代码
APP_ENV=test

取值方式如下,注意,一定要加上 const

dart 复制代码
/// dart define 环境变量
String get appEnv => const String.fromEnvironment('APP_ENV');

判断是否为 release

dart 复制代码
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

shell 复制代码
# 补充这一句
export VERBOSE_SCRIPT_LOGGING=1

/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" build

再次编译就可以看到详细的 flutter 命令打包信息,可以将其导出后慢慢查看。

下面是摘出的主要耗时记录和文件大小

Profile

shell 复制代码
# 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] ...
shell 复制代码
# 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

shell 复制代码
# 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] ...
shell 复制代码
# 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_* 命令,如下所示

shell 复制代码
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 中,精简命令如下

shell 复制代码
gen_snapshot_* --trace-compiler ... app.dill > result.txt 2>&1

result.txt 中的内容长这个样子

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

shell 复制代码
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。 会删除未使用的代码,并进行其他优化,以确保应用的体积尽可能小,并且性能最佳。

关于构建模式的详细说明,可以看官方文档 docs.flutter.dev/testing/bui...

因此,如果我们希望最大限度地减少应用的体积并提高性能,建议在 Release 模式下构建 Flutter 应用。

Profile 模式切到 Release 模式后的打包时间如下

构建机 Profile(min) Release(min)
i7 25+ 18+
M4 16+ 9+

可以看到,切换编译模式已经很大程度地优化了编译时长,不过我们还可以再进一步优化。

五、二进制依赖

二进制依赖iOS 端老生常谈的优化点了,通过直接使用编译好的库或模块,从而避免编译的时间和资源消耗。

因此,原生插件越多,编译速度就越慢,二进制依赖的优化效果越好,二进制依赖的优化效果越好,编译速度就越快,所以编译越慢,编译越快 ~

在这里我使用的是 Rugby 这个工具。

安装

shell 复制代码
curl -Ls https://swiftyfinch.github.io/rugby/install.sh | bash

安装完成后输出如下内容

shell 复制代码
🏈 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 是否可以被正常使用

shell 复制代码
rugby --version

# 输出
2.10.2

使用

在执行完 pod install 后,再执行 rugby cache 即可将原生插件从源码依赖转成二进制依赖了

shell 复制代码
rugby cache \
  --arch arm64 \
  --sdk ios \
  --except chat_bottom_container realm dart_native \
  --config Release

这里通过 --except 将一些不做二进制依赖的包过滤掉了。

当这些参数太多之后,命令会变得很长,不好看,可以将这些参数整理到 plans.yml 文件中

yaml 复制代码
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

shell 复制代码
rugby plan release -p /User/lxf/.../plans.yml

不过需要注意的是,如果你再次执行 pod install 将会还原为源码依赖!rugby 的修改就会失效~

而我们平时执行的 flutter build ipa 命令,其内部是有可能会去执行 pod install 的,那如何避免呢?

经过 flutter_tools 的源码阅读,发现它会做如下判断

  1. 涉及的文件是否存在
  2. 对比 pod_inputs.fingerprint 中的各项 MD5
  3. 对比 Podfile.lockPods/Manifest.lock 内容

pod_inputs.fingerprint 位于 build/ios 目录,内容如下

json 复制代码
{
    "files": {
        "/Users/lxf/.../ios/Runner.xcodeproj/project.pbxproj": "21b527dc18081de6eabe26c6a4e851b2",
        "/Users/lxf/.../ios/Podfile": "25baa69590b287fd88a578ae5fa2f964",
        ".../flutter/packages/flutter_tools/bin/podhelper.rb": "29abcfc3297c225fc1d1ae2380787cd6"
    }
}

所以现在很明确,我们需要调整打包步骤

  1. flutter pub get/upgrade
  2. cd ios && pod install
  3. 切成二进制依赖
  4. 自己生成 pod_inputs.fingerprint
  5. 拷贝 Podfile.lockPods/Manifest.lock
  6. flutter build ipa

其中第 3 ~ 第 5 步我已经做了封装在我的 github.com/LinXunFeng/... 项目中,使用如下

Condor

安装 condor

shell 复制代码
brew tap LinXunFeng/tap && brew install condor

指定编译模式

设置环境变量 CONDOR_BUILD_MODE,对应 plans.yml 里的 profilerelease

shell 复制代码
export CONDOR_BUILD_MODE=release

也可以使用 --mode 参数来指定模式

arduino 复制代码
condor optimize-build --mode release

二进制依赖与同步文件

进入到 Flutter 项目的根目录,执行如下命令

shell 复制代码
cd path/to/your/flutter_project

condor optimize-build --config path/to/rugby/plans.yml

如果你想指定 fvm 安装的且非全局默认的 flutter,则可以加上 --flutter 参数

shell 复制代码
condor optimize-build --config path/to/rugby/plans.yml --flutter "fvm spawn 3.24.5"

最后执行打包命令即可。

六、最后

希望苹果下一个版本的 Xcode 可以解决这个问题吧,不然的话,emmm,我也不会升级电脑的~

如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有 iOS 技术,还有 AndroidFlutterPython 等文章, 可能有你想要了解的技能知识点哦~

相关推荐
Ya-Jun1 天前
性能优化实践:启动优化方案
android·flutter·ios·性能优化
sunly_1 天前
Flutter:组件10、倒计时
开发语言·javascript·flutter
桌角的眼镜1 天前
模拟开发授权平台
macos·ios·xcode
海盐泡泡龟1 天前
Vue中的过滤器知道多少?从是什么、怎么用、应用场景、原理分析、示例解释
前端·vue.js·flutter
怀君2 天前
Flutter——数据库Drift开发详细教程(二)
android·数据库·flutter
帅次2 天前
Flutter BottomNavigationBar 详解
android·flutter·ios·小程序·iphone·reactnative
明似水2 天前
解决 Flutter 在 iOS 真机上构建失败的问题
flutter·ios
仙魁XAN3 天前
Flutter 学习之旅 之 Flutter 和 Android 原生 实现数据交互的MethodChanel和EventChannel方式的简单整理
android·flutter·flutter module·aar·混合开发·flutter交互
BianHuanShiZhe3 天前
升级xcode15 报错Error (Xcode): Cycle inside Runner
ide·macos·xcode
leluckys4 天前
flutter 专题 六十一 支持上拉加载更多的自定义横向滑动表格
flutter