这可能是所有Flutter老项目上架时被卡住最久的一道坎。我用旧版Flutter(大概1.x时期)生成的项目,用新版Xcode打包上传App Store,反复在最后一步收到“App contains bitcode ...”这类报错,要么直接被App Store Connect拒绝,要么Xcode里提示Invalid Bundle,整整折腾了一个晚上。如果你也遇到类似情况,不用怀疑是证书、账号或者签名配置的问题,根源基本出在bitcode这个老古董设置上。
这篇文章就从问题本身开始拆,说清楚bitcode到底是个什么东西、为什么旧Flutter项目会踩中、以及我实测有效的完整修复和上架流程。适用对象是手里还留着老Flutter工程、最近需要重新打包上架的开发者,尤其是从Flutter 1.x或2.0早期一路升级过来的项目。
1. 报错长什么样,以及它到底发生在哪一步
1.1 报错原文和触发时机
先说触发时机。这类报错不是在你本地flutter build ios的时候出现的,而是发生在Xcode打包完成、准备上传到App Store Connect或者上传后被苹果后台处理时。最常见的两种表现:
第一种,Xcode Organizer上传过程中直接弹窗,提示类似:
App Store Connect Operation Error. Invalid Bundle. App contains a framework ... that does not contain bitcode.第二种,本地显示上传成功,但过几分钟去App Store Connect后台看,版本状态变成“Invalid Binary”或者“处理中失败”,邮件/后台信息里写明:
ITMS-90472: Invalid Bundle - The app ... does not contain bitcode. ITMS-90474: Invalid Bundle - App contains an app extension or a framework ... that does not contain bitcode.我当时遇到的是ITMS-90474,报错路径里明确指向了Flutter.framework和App.framework。这里有个很容易误导人的点:你以为是自己代码或者签名配置坏了,于是反复换证书、重建描述文件,折腾多个小时后发现问题依旧。实际上,报错信息里的关键词“does not contain bitcode”已经把方向指得很明确:二进制里缺了bitcode段,苹果后台校验不通过。
1.2 为什么偏偏是旧版本Flutter项目
老Flutter项目踩这个坑的概率远高于原生iOS项目,原因是组合出来的:旧版Flutter自动生成的Xcode工程里,默认把ENABLE_BITCODE设置成了YES;但Flutter引擎预编译出来的Flutter.framework和构建出来的App.framework,并不是每个架构切片都包含bitcode信息。本地编译、签名都正常,因为模拟器和真机运行不需要bitcode;可一旦Archive归档,工程设置要求“包含bitcode”,苹果后台再逐库校验时,矛盾就爆发了。
原生iOS项目通常只需要把Build Settings里的Enable Bitcode关掉就能通过,但Flutter老项目关闭后还可能遇到Pod里的第三方库同样开了bitcode,或者因为flutter build缓存导致framework没重新生成,得做一套完整的清理重建。这也是为什么网上很多帖子说“关了bitcode还是不行”,多半是没把Flutter侧重新构建一遍。
2. 先把bitcode这层窗户纸捅破
2.1 bitcode是什么,苹果当初为什么要求它
bitcode是LLVM编译流程里的中间表示(IR),你可以把它理解成“源代码编译成机器码之前的一种半成品”。苹果当初推广bitcode,逻辑是开发者提交App时先不把最终机器码写死,而是把中间表示交给苹果,苹果服务器后续可以根据新处理器架构或者新的编译优化重新生成最终可执行文件,理论上达到“一次提交,长期适配”的效果。对开发者来说,代价就是二进制体积变大、编译时间变长,而且上传的包不再是纯粹意义上的最终成品。
所以苹果早在Xcode 14开始就明确不再为iOS模拟器生成bitcode,并且逐步弱化了这一机制。到现在,苹果已经基本把bitcode从iOS/watchOS/tvOS的构建流程中移除了,新版Xcode里甚至找不到对应开关。问题恰恰出在这里:旧工程文件里还写着ENABLE_BITCODE=YES这个遗留设置,新Xcode构建时不再帮你自动处理,上传后苹果的新后台也不认这种“半成品”二进制,于是报错。
2.2 Flutter旧版和bitcode的纠葛
Flutter在iOS上会把引擎预编译为Flutter.framework,这部分工作通常发生在Flutter SDK安装或升级时。老版本的Flutter工具链在构建这些引擎库时,并不是所有配置都启用了bitcode。具体来说:模拟器架构(x86_64)下基本不含bitcode,真机Release架构虽然理论上可以包含,但在某些Flutter版本里也没做到统一。而你Archive打包时,工程设置要求包含bitcode,可framework本身没有,苹果后台校验时自然对不上账。
更麻烦的是,旧Flutter项目的project.pbxproj里不止主工程,还有Pods工程。CocoaPods集成的第三方库,如果某个Pod是预编译二进制或者其构建参数里也开了bitcode,同样会导致校验失败。关闭bitcode的时候,主工程和Pods工程要一起处理。
2.3 关掉bitcode到底有没有风险
很多老开发者一听到“关闭bitcode”会犹豫,担心影响上架。实际情况是:关闭bitcode完全不影响App Store审核与正常运行。理由很简单——bitcode只是给苹果的未来优化留了个后门,不是上架硬性要求,绝大多数商业App从始至终都关着bitcode;而且新版Xcode本身已经不给iOS构建bitcode了,关掉反而是更符合当前生态的做法。
唯一要留意的是,如果App同时包含watchOS扩展或tvOS扩展,这些平台在特定情况下可能需要单独确认bitcode设置。常规iOS App直接关掉就完事。
3. 亲测有效的修复流程(老项目照做即可)
3.1 方案A:直接在Xcode里关掉Enable Bitcode
如果Build Settings里还能看到Enable Bitcode选项,用这个方式最直观。
第一步,用Xcode打开老项目的.xcworkspace文件。注意是workspace而不是.xcodeproj,因为Flutter项目集成Pods后,直接用project打开容易漏掉Pods的配置。
第二步,选中左侧Project导航里的项目根节点,在中间栏切到Build Settings标签页,顶部搜索框输入bitcode。
第三步,分别在Project级别和Target级别把Enable Bitcode改成NO。我习惯先改Project,再逐个Target确认,防止有的Target单独覆盖了设置。
第四步,如果工程里还有Extension(比如Widget Extension、Notification Service Extension),每个Extension Target都要单独改一遍。只改主App target的话,Extension上传时依然可能报ITMS-90474。
3.2 方案B:工程文件全局修改ENABLE_BITCODE
新版Xcode的Build Settings界面里可能已经没有bitcode选项,但老工程的project.pbxproj里还有ENABLE_BITCODE = YES这行遗存配置。这时候直接用文本编辑器修改工程文件更快。
先退出Xcode,备份.xcodeproj/project.pbxproj和Pods相关文件,然后用编辑器全局搜索ENABLE_BITCODE,把出现的YES都改成NO。注意不要动别的build setting。
同时检查Podfile,如果没有post_install钩子,建议加上这段,确保每次pod install之后Pods工程里的bitcode也保持关闭:
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时,Pods里某些第三方库又把bitcode打开,导致同样的问题阴魂不散。我当时就是因为只改了主工程,执行一次pod install后又回到解放前。
3.3 关掉之后必须做的三步清理
改完配置后不能直接Archive,Flutter的构建缓存会捣乱。以下三步按顺序做:
第一步,在项目根目录执行flutter clean,把Flutter侧的构建产物全部清掉。这个命令会删除build/目录,同时清理.dart_tool/里的部分缓存。
第二步,手动删除iOS目录下的Pods文件夹,然后重新执行pod install。如果没装CocoaPods或者版本较老,先用sudo gem install cocoapods升级到较新版本再操作。重新安装Pods的目的是让第三步Post Install钩子生效,确保Pods工程配置干净。
第三步,在Xcode里执行Product -> Clean Build Folder,或者直接快捷键Shift+Command+K。这个操作会连Xcode编译缓存一起清空,避免Archive时把旧的framework直接复用进去。
全部清理完成后,重新执行Archive,再上传。我实测下来,只要配置改对、清理做全,一次就能过。
4. 顺手把上架流程里的其他坑一起排了
4.1 Archive前后我建议按这个顺序自查
bitcode报错搞定后,老Flutter项目上架还有几个高频问题会在后面等着,建议打包前按顺序自查一遍。
第一,最低系统版本。Flutter老项目的Podfile里默认可能有platform :ios, '8.0'或者9.0之类的设置,而现在很多第三方库和系统框架都要求更高的最低版本。如果Archive时出现building for iOS, but linking ...之类的警告甚至报错,先把Podfile里的platform提到11.0或更高,再重新pod install。
第二,版本号和构建号。App Store Connect后台会校验CFBundleVersion和CFBundleShortVersionString。老项目里版本号可能沿用旧格式,比如1.0,构建号重复也会导致上传失败。直接在Xcode的Signing & Capabilities里改,或者改Info.plist里对应字段,每次上传前确认后台没有重复的构建号。
第三,LaunchScreen。如果App没有提供LaunchScreen.storyboard或者Info.plist里没有配置UILaunchStoryboardName,上传会报错。Flutter老项目里面一般有LaunchScreen.storyboard,但如果曾经手动清理过,务必检查一下。
第四,dSYM文件。如果开了Crashlytics或者要分析崩溃日志,上传前确保Xcode Organizer里有对应版本的dSYM。这个不影响上架成败,但影响后续排查问题,别省略。
第五,证书和描述文件。老项目最容易被拖进“重签大坑”。我建议直接用Xcode的自动签名:在Signing & Capabilities里勾选Automatically manage signing,选择正确的Team,让Xcode自动生成和更新描述文件。如果项目里历史证书已经过期,自动签名能省很多时间。
4.2 搭配出现的其他上传报错速查
实际打包过程中,经常是解决完bitcode又冒出来新报错。这里把我踩过和帮别人排查过的问题整理成速查表:
| 报错现象 | 主要原因 | 快速处理办法 |
|---|---|---|
| ITMS-90125 | 二进制文件损坏或上传工具版本过期 | 用Xcode自带Organizer上传,或者更新Transporter;不要用老旧Application Loader |
| Invalid Bundle - App contains an extension with disallowed key | Extension Info.plist里有不该出现的键值 | 检查Extension的Info.plist,删掉CFBundleDisplayName以外的多余Key |
| App Store Connect Operation Error - multiple commands produce | Xcode新版本对重复资源文件报错 | 检查Build Phase里是否有同名资源被多个Target引用,删除重复引用 |
| 上传后一直显示“正在处理” | dSYM或二进制校验耗时 | 通常等10-30分钟;超过2小时就重新上传一次构建 |
| Xcode找不到Bitcode选项 | 新版Xcode移除UI入口,但工程配置还在 | 用3.2的文本编辑方式处理ENABLE_BITCODE |
这里特别提醒一下:不要因为只想修bitcode,就去下载老版本Xcode。Xcode太老反而和App Store Connect的新接口不兼容,会出现证书校验、签名算法等更头疼的问题。正确姿势是保留新版Xcode,只改工程配置和Flutter侧的兼容性。
5. 常见问题排查与实操心得
5.1 常见问题速查表
为了方便日常排查,我把这个场景下的典型问题再做了一张速查表,按优先级排序:
| 优先级 | 问题点 | 检查项 | 推荐操作 |
|---|---|---|---|
| P0 | bitcode设置 | 主工程、所有Target、Pods工程 | 全部设为NO,并加入Podfile Post Install钩子 |
| P0 | Flutter构建缓存 | build目录、Pods目录 | 执行flutter clean、删除Pods、重新pod install |
| P1 | 最低iOS版本 | Podfile的platform | 至少10.0以上,推荐11.0 |
| P1 | Xcode和CocoaPods版本 | xcodebuild -version、pod --version | 升级到当前稳定版 |
| P2 | 证书与描述文件 | Signing & Capabilities | 开启自动签名,指定正确Team |
| P2 | LaunchScreen配置 | Info.plist的UILaunchStoryboardName | 确认存在LaunchScreen.storyboard并且已配置 |
| P3 | 构建号重复 | App Store Connect的构建版本列表 | 每次构建号递增,不要原地重复上传 |
5.2 我的实操体会和两个小技巧
踩过几次坑之后,我现在遇到老Flutter项目上架,都会多做一个步骤:Archive完成后,先用终端检查一下framework里是不是真的没有bitcode了。命令很简单:
otool -l Flutter.framework/Flutter | grep __LLVM如果输出为空,说明该切片确实没有bitcode。结合工程配置关了bitcode,可以很确定这次不会再因为同样的问题被拒。
再分享一个小技巧:在改完ENABLE_BITCODE之后,别急着Archive,先跑一遍flutter build ios --release看看Flutter侧能不能正常产出framework。这一步在Xcode之外提前暴露问题,比在Archive阶段报错更容易定位。如果Flutter构建顺利,再回Xcode做Archive,整个流程会顺畅很多。
还有一点想特别说一下:如果你手里的老Flutter项目其实是几年前的紧急项目,方案上除了修bitcode,我还建议评估一下Flutter版本的升级路径。旧版Flutter在iOS新系统上的兼容性问题会越来越多,bitcode只是一个先暴露出来的问题。当然,如果项目短期内不能大改,本文这套关闭bitcode的方案就是成本最低、见效最快的办法,完全可以直接上架使用。
下一次如果又遇到类似的神秘“Invalid Bundle”,记住先搜Build Settings里的bitcode,再考虑证书问题,顺序反了只会白折腾。