前几天帮朋友处理了一个老 Flutter 项目的上架问题。项目从 2021 年就开始维护,Flutter 版本还停在 2.5.x,Xcode 升到 14.2 之后,本地 Archive 一切正常,但一点 Upload to App Store Connect,一分钟内就弹出 ITMS-90502 Invalid Bundle,点开详情,里面明晃晃写着 "The asset at ... contains bitcode"。这个报错在 2022 年以后特别常见,基本是旧版 Flutter 撞上新版 Xcode 的典型事故。我这次排查和修复的完整过程,从 bitcode 的来龙去脉到三条排查命令,再到四步实操步骤,一次说清楚。适合手里有老 Flutter 项目、升级 Xcode 之后上架受阻的开发者,也适合看到"bitcode"三个字就发懵的 iOS 新手。
1. 先搞清楚,这个 contains bitcode 报错到底是什么
1.1 苹果给了半成品,又把它收回去
bitcode 是 LLVM 编译器的中间表示,翻译成人话,它既不是源代码,也不是最终机器码,而是"半成品"。以前开发者把 App 传给 App Store 时,可以连这份半成品一起交上去,苹果服务器拿到之后再针对不同设备架构重新"加工"成可执行文件。这个机制最初用在 App Thinning 和 watchOS 这类场景上,理论上一份包能省不少体积。
但问题在于,苹果后来发现自己不太需要这套东西了。Xcode 14 开始,bitcode 在 iOS 平台被正式标记为废弃,新工程的 Build Settings 里根本看不到这个开关,苹果的服务端也不再接收带 bitcode 的 App。于是产生了一个很尴尬的历史遗留问题:新工具链默认不生成 bitcode,但旧工程里可能还留着ENABLE_BITCODE = YES的配置,而打包工具链发现你开着开关,就会认真地把 bitcode 写进产物里。结果就是,你高高兴兴点了 Upload,苹果后端看了一眼包,直接退货并附上一句 "contains bitcode"。
注意一点:这个报错不一定出现在 Archive 阶段。很多项目 Archive 正常,Xcode 也没有红色错误,真正翻车是在 Upload 那一下,或者上传之后收到 App Store Connect 邮件。所以如果你只盯着编译日志找问题,大概率一无所获。
1.2 旧版 Flutter 为什么是"重灾区"
旧版 Flutter 项目踩这个坑,原因是多层的。
第一层是项目模板本身。Flutter 早期生成的 iOS 工程,是在 Xcode 11、12 时代设计的,那时候 Xcode 默认配置里ENABLE_BITCODE = YES并不稀奇。项目创建之后,这个开关就一直留在 project.pbxproj 里,哪怕你后来升级了 Xcode,它也不会自己消失。
第二层是 Flutter 引擎和 Dart AOT 产物。Flutter 在 iOS 上会把 Dart 代码预编译成原生二进制,放在 App.framework 里。如果构建时工程的 bitcode 开关是打开的,LLVM 工具链会顺手在这个二进制里塞入__LLVM段。我实际拆过包,Flutter 2.5.x 左右的 App.framework,用 otool 能看到明显的segname __LLVM,这就是报错里指向的资产。
第三层是依赖库。老项目里往往带着一堆 2020、2021 年发布的第三方 SDK,CocoaPods 安装时,如果 podspec 里启用了 bitcode,或者整个 Pods 工程继承了项目的构建设置,这些 framework 也会带 bitcode。报错路径指向的常常不是一个文件,而是一批。
所以你会发现,这个问题的本质不是某个配置写错了,而是"老配置 + 新工具链 + 旧依赖"三股力量搅在一起。单独的 Flutter 也好、Xcode 也好,单独看都没毛病,合在一起就炸了。
2. 动手改之前,先做一次体检
2.1 版本搭配检查:先看门当户对
拿到一个报错的 Flutter 项目,我习惯先不慌着改配置,先看版本组合。
flutter --version xcodebuild -version这两个命令一跑,心里大概就有数了。常见组合的风险大致这样:
| Flutter 版本 | 常见 Xcode 搭档 | 升级到 Xcode 14+ 后 bitcode 风险 |
|---|---|---|
| Flutter 1.x | Xcode 11 / 12 | 高,项目模板本身默认开 bitcode |
| Flutter 2.0 – 2.5 | Xcode 12 / 13 | 高,引擎产物里常见 __LLVM 段 |
| Flutter 2.8 – 2.10 | Xcode 13 / 14 | 中,情况有所好转 |
| Flutter 3.x | Xcode 14 / 15 | 低,官方已默认关闭 bitcode |
这里不是让你立刻升级 Flutter,而是判断问题的优先级。如果项目用的是 2.5.x 这种版本,大概率就是 bitcode 历史债;如果已经是 3.x 还报 ITMS-90502,那就要多花点时间检查是不是某个第三方 SDK 在搞鬼。版本检查花不了两分钟,但能帮你确定后续排查方向。
2.2 两条命令找出到底谁带了 bitcode
网上很多文章让你直接改开关,但改了不一定好,因为不知道包里到底哪个文件带了 bitcode。我的习惯是先用 otool 把"案发现场"找出来。
检查build目录下的 Flutter 产物:
cd ios # 检查 App.framework(Dart AOT 产物) xcrun otool -l build/ios/Release-iphoneos/App.framework/App 2>/dev/null | grep -A 2 "__LLVM" # 检查 Flutter.framework(引擎二进制) xcrun otool -l build/ios/Release-iphoneos/Flutter.framework/Flutter 2>/dev/null | grep -A 2 "__LLVM"如果输出里出现segname __LLVM,就说明这个文件里带了 bitcode。检查完主工程产物,再把 Pods 目录里所有 framework 扫一遍:
cd ios find . -name "*.framework" -type d | while read fw; do bin_name=$(basename "$fw" .framework) bin_path="$fw/$bin_name" if [ -f "$bin_path" ]; then if xcrun otool -l "$bin_path" 2>/dev/null | grep -q "__LLVM"; then echo "发现 bitcode: $bin_path" fi fi done这个脚本会遍历所有 framework,找到带__LLVM段的文件。我见过的情况里,报错路径指向最多的是App.framework/App,其次是某个老版本 SDK 的 framework。先把目标锁定,后面处理起来就有针对性。
2.3 检查工程和 Pods 里的遗留配置
文件层面确认之后,再翻配置。老项目的 project.pbxproj 里,大概率还留着一行:
ENABLE_BITCODE = YES;但注意,新版本 Xcode 的 Build Settings 界面里搜索 bitcode 可能什么都搜不到,因为新版默认隐藏了这个废弃选项。这时候别慌,直接用文本编辑器打开ios/Runner.xcodeproj/project.pbxproj,搜索ENABLE_BITCODE,看看到底还有几个YES。
另外要检查 Podfile 里有没有人手动设过 bitcode。有些项目的 Podfile 末尾会有一段 post_install 脚本,里面可能写了ENABLE_BITCODE相关配置。如果这一段本身写的值是YES,或者干脆没写,Pod 构建时就会继承工程设置,一样会出事。
记住一个关键点:bitcode 问题从来不是"一个文件的事情",它可能同时存在于工程配置、Pods 配置和第三方二进制里。前面三步检查做完,你才敢说自己真的知道问题在哪。
3. 现场实操:四步把一个老项目救回来
3.1 第一步:把工程和 Pods 里所有 bitcode 开关全部关掉
先处理工程本身的开关。最稳妥的方法是打开 Xcode,选中 Runner target,Build Settings 里如果能看到 Bitcode 相关选项,直接设为 NO。搜不到也没关系,直接改 project.pbxproj 里的配置。
# 备份,永远要备份 cp ios/Runner.xcodeproj/project.pbxproj ios/Runner.xcodeproj/project.pbxproj.bak # 全局替换 ENABLE_BITCODE = YES 为 NO sed -i '' 's/ENABLE_BITCODE = YES;/ENABLE_BITCODE = NO;/g' ios/Runner.xcodeproj/project.pbxproj改完之后再 grep 一遍,确认没有漏网的YES。这里要提醒一句,project.pbxproj 里可能不止一处ENABLE_BITCODE,项目有多 target 的情况下,几个 target 的配置都会出现,sed 全局替换是效率最高的办法,但前提是你已经备份了文件。
然后处理 Pods。光改主工程不够,我强烈建议在 Podfile 末尾加一段统一的关闭脚本:
post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['ENABLE_BITCODE'] = 'NO' end end end这段脚本会在每次pod install时,把所有 Pod 工程的 target 都强制设为不启用 bitcode,相当于给所有依赖统一打了一次补丁。不管是你自己集成的 SDK 还是三方 pod,只要是通过 CocoaPods 管理的,都会生效。实测下来,这个脚本是解决 bitcode 遗留问题最省心的方式。
3.2 第二步:彻底清理构建缓存,重新生成 Dart AOT 产物
改完配置只是第一步。我见过太多人改完开关立刻 Archive,结果还是报 contains bitcode,原因就是构建缓存和 AOT 产物没有重新生成。
Flutter 在 iOS 上会把 Dart 编译成原生二进制,这个产物是之前用旧配置生成的,你光在 Xcode 里 Clean 也不会自动重编。正确的操作顺序是:
flutter pub get flutter clean cd ios rm -rf Pods rm -f Podfile.lock pod install --repo-update cd .. flutter build ios --release --no-codesign这段命令做了几件事:flutter clean清掉 Flutter 侧的构建缓存,rm -rf Pods把旧的依赖二进制全部删掉,pod install --repo-update重新生成 Pods 目录并应用刚才的 post_install 脚本,最后flutter build ios --release --no-codesign用关闭 bitcode 的新配置重新生成 App.framework 等产物。
为什么要这么彻底?因为 bitcode 标志是写进二进制文件里的,旧的 Com.apple.dt.bitcode 构建记录如果不清掉,Xcode 在增量编译时会觉得"文件没变,直接复用",于是你改的开关根本没起作用。这一步是很多人忽略的关键。
3.3 第三步:重新 Archive 并正确处理上传
构建命令跑通之后,打开 Xcode,选中 Runner scheme,Product → Archive,生成一个新的归档。
归档完成之后进入 Organizer,点击 Distribute App → App Store Connect → Upload。如果你用的是 Xcode 13 及更早版本,在这个上传界面里可能会看到一个 "Include bitcode" 勾选框,绝对不要勾。新版 Xcode 14+ 已经没有这个选项了,不用管。
这里还要提一下 Transporter。我在处理那个朋友的项目时,Xcode 直接 Upload 挂了,后来改用 Transporter 上传导出的 IPA,反而能看到更详细的错误信息。如果你遇到这种情况,可以在 Organizer 里先导出 IPA,然后用 Transporter 上传,错误提示会比 Xcode 弹窗清晰很多。
上传成功的标志不是你点了 Upload,而是 App Store Connect 后台的"活动"页签里出现"正在处理"的构建版本。看到这个状态,才算真正提交成功。
3.4 第四步:还是报错的话,考虑升级 Flutter 版本
如果按照前三步操作之后仍然报 contains bitcode,大概率是 Flutter 引擎版本太老,引擎二进制本身带着__LLVM段。这时候最根除的办法就是升级 Flutter。
我的建议是先从低风险方案开始:把 Flutter 升到当前小版本线里的最新 patch 版本,比如 Flutter 2.5.x 系列尽量升到 2.5.3 以上,很多时候官方的预编译引擎已经悄悄修正了这类问题。如果项目用了比较激进的第三方库导致无法小版本升级,再考虑大版本升级,比如从 2.x 线迁到 3.x 线。
升级前一定要把当前分支打 tag 或者建一个备份分支。Flutter 大版本升级可能牵扯到 Dart 语法、第三方插件的兼容层,不是一次flutter upgrade就能无痛完成的。升级完成后,重复第二步的三条命令,重新生成全部产物,再做一次完整的 otool 检查。一般情况下,升级到 Flutter 3.x 之后,bitcode 相关报错会彻底消失。
4. 避坑指南:我踩过的和你可能还要踩的坑
4.1 改完开关还一直报错?多半是忘了重建 AOT 产物
我第一次处理这个报错时,也犯过"只改开关、不重新构建"的错。当时在 Build Settings 里把 ENABLE_BITCODE 改成了 NO,顺手 Archive,上传,结果照样 ITMS-90502。
排查了大半天,最后用 otool 一看,App.framework 里的__LLVM段还在,才反应过来问题出在 Flutter 侧没有重新生成 AOT 产物。Xcode 的 Archive 并不会触发 Flutter 重新编译 Dart,除非你主动运行了flutter build ios或者在 Xcode 里执行了 clean build。
所以这里有一个非常实用的检查清单:改完配置之后,至少要做两件事——运行flutter clean,然后跑一次完整的flutter build ios --release --no-codesign。绝对不能直接沿用旧的 build 目录去 Archive,那样改了多少配置都没用。
4.2 上传成功却收到 "Invalid Bundle" 邮件怎么办
还有一种情况更隐蔽:Xcode 上传流程全部走完,界面显示成功,Transporter 也没报错,但几个小时后收到 App Store Connect 的邮件,标题是 "Invalid Bundle",正文里写着 "contains bitcode"。
这通常意味着你的本地验证没有发现问题,但苹果服务端在接收时做了更严格的检查。这种情况下不要试图"再传一次同一个 IPA",因为服务器那边已经认定这一份包不合格,你再传一次结果一样。
正确做法是回到项目,用 otool 再扫一遍所有 framework,确认没有任何__LLVM段,然后重新执行第七步里的清理与构建流程,再生成一份全新的 Archive 和 IPA,最后重新上传。我在实际工作中还发现,邮件提示里的路径往往不是完整路径,它会写 "The asset at Payload/app.app/Frameworks/Flutter.framework/Flutter contains bitcode" 这样的格式。看到这个路径不要慌,它就是对症下药的最直接线索。
4.3 遇到过的相关报错速查表
| 报错或现象 | 是什么情况 | 处理思路 |
|---|---|---|
| ERROR ITMS-90502 Invalid Bundle ... contains bitcode | 上传包里的二进制含__LLVM段,被苹果后端拒绝 | 关闭所有 ENABLE_BITCODE,清理重建,重新上传 |
| Archive 时警告 App Store distribution is no longer supported with bitcode | Xcode 14+ 发现工程还开着 bitcode | 把 Build Settings 里的开关设为 NO,或直接改 pbxproj |
| 某 framework 单独被报 contains bitcode | Pods 里的第三方二进制自带 bitcode | 在 Podfile 里统一关闭,或用 bitcode_strip 处理 |
| 上传后 App Store Connect 显示"无效构建版本" | 上传成功但后台审包失败 | 用 Transporter 查看原始错误,按错误码处理 |
| 清理重传后仍然报 Invalid Bundle | 构建缓存没清干净,或签名和 Info.plist 问题 | 清空 DerivedData,检查签名、图标、版本号 |
这张表是我处理这类问题时的速查参考,核心逻辑一句话:先定位是哪个文件带 bitcode,再决定是改配置还是剥离二进制。
4.4 两个让后续打包更省心的小习惯
处理完这个项目之后,我自己养成了两个习惯。
第一个是把 Podfile 里的 bitcode 关闭脚本常驻,也就是前面那几行 post_install 代码。不管新接手什么项目,第一步先看一下 Podfile 里有没有这段,没有就顺手加上。加完之后哪怕以后再遇到老工程的 bitcode 问题,Pods 这一层基本不会再爆雷。
第二个是给项目锁定 Flutter 版本和 Xcode 版本。跨端框架的项目最怕的就是"谁想升级就升级",Flutter 引擎和 Xcode 工具链是强耦合的,一旦其中一个升级,另一个没跟上,上架时就会冒出各种诡异报错。我现在会在项目的 README 里写上"本工程基于 Flutter x.x.x + Xcode x.x 构建",并定期用flutter upgrade到稳定版后再做回归测试,而不是等到上架前最后一刻才升级工具链。
这两个习惯听起来简单,但真的能省掉大量发版夜的痛苦。很多问题不是不能解决,而是解决成本被拖延抬高了。
我个人在实际操作里的体会是:苹果的构建机制每年都在变,Flutter 这种跨端框架的预编译产物往往又落后半拍,两者之间出现兼容黑洞是迟早的事。遇到 contains bitcode 这类报错,冷静下来按"定位文件 → 关闭开关 → 彻底重建 → 正确上传"的顺序走,基本都能救回来。如果项目还要长期维护,早点把 Podfile 的关闭脚本和版本锁定固化下来,远比每次发版时手忙脚乱地修要踏实得多。