
前阵子有朋友找我处理一个压了两年的 Flutter 老项目要上架新版本。项目本身是 Flutter 1.22 时代的产物本地 Xcode 已经升到了 14.2。Archive 的时候一切正常结果准备上传到 App Store Connect折腾了半天卡在一个非常典型的错误上contains bitcode。一开始我还以为是某个第三方库版本太老后来排查完才发现这个坑几乎每个还在维护旧版本 Flutter 项目的同学都会踩到。这篇就把我当时从报错现象到根因、再到完整修复的链路记录下来给还守着旧工程的人一点参考。先说结论旧版本 Flutter 项目在 Xcode 14 之后打包上架报“contain bitcode”根子基本都出在工程模板默认开启了 Enable Bitcode再加上一批带有 __LLVM 段的三方库没清理干净。解决方案没有多玄学但排查顺序和验证手段必须做对不然你改完可能还是上传失败。1. 先分清你遇到的是哪一阶段的 bitcode 报错很多人在群里甩一条模糊的日志就问“为什么报 contain bitcode”实际上这个报错分两种完全不同的场景处理方式也不一样。拿我这次遇到的情况举例最直观的区分点就是看你在哪一步报的错。1.1 编译链接期的报错这种情况发生在 Xcode 的 Build 或者 Archive 阶段日志里通常长这样ld: warning: SomeLibrary.framework/SomeLibrary contains bitcode. You should rebuild it with bitcode disabled.或者更狠一点直接用红色报错ld: Pods/XXX/XXX does not contain bitcode. You must rebuild it with bitcode enabled.第一种 warning 其实不影响 Archive但你会慌第二种是明确的链接错误属于 Build Phase 阶段就需要处理。旧 Flutter 项目最常见的是第一种它不阻断 Archive但它会直接导致后面上架阶段的一系列连锁反应。1.2 上传 App Store Connect 阶段的报错这阶段报错才是真正的“拦路虎”。你用 Xcode 的 Organizer 上传做好的 ipa 时错误信息一般带 ITMS 编号最常见的是ITMS-90475: Vector. DemoApp.app/DemoApp contains bitcode. The app was submitted with bitcode enabled. Since Xcode 14, bitcode is no longer supported.如果你更新比较勤快、用 Xcode 15可能还会看到ITMS-90476: The DemoApp.app binary contains bitcode. Since Xcode 14, bitcode is no longer supported. Use Xcode 13 or earlier to submit this app.对这就是我这次卡住的点。Archive 成功本地看起来一切完美一上传就被 App Store Connect 打回来。凡是遇到 ITMS-90475 或 ITMS-90476说明你的 App 包内还存在带 bitcode 的切片。注意不是你主工程写了 ENABLE_BITCODENO 就能万事大吉我从这里开始才意识到问题比想象中深。下面这张表可以帮你快速对号入座报错阶段典型错误信息影响处理优先级Xcode Build/Archiveld: warning: ... contains bitcodeArchive 能过但心里没底中Xcode Build/Archiveld: ... does not contain bitcode直接编译失败高上传 App Store ConnectITMS-90475 / 90476上传被驳回高2. bitcode 为什么会在旧版本 Flutter 工程里阴魂不散要说清楚这个问题得先讲明白 bitcode 到底是个什么东西。bitcode 是苹果在 LLVM 编译器架构下推行的一种中间代码格式你可以理解成“源码编译后的半成品”——开发者把这种半成品交给 App Store苹果在下发到你手机之前还会根据当时最新的设备架构再做一次最终编译和优化。它曾经是苹果主推的方向有点“一次提交、多种设备受益”的意思。但苹果后来很快发现这套机制并没有真正达到预期的效果反而给开发者、第三方库维护者带来一堆构建麻烦于是在 Xcode 14 开始直接移除了 bitcode 的支持新提交的 App 不允许包含 bitcode。问题是Xcode 14 移除了支持但不会自动帮你把老工程里的配置和遗留切片清理掉。2.1 Flutter 旧版本模板为什么默认开启 bitcodeFlutter 1.x 到 2.0 初期那个年代新建项目生成的 iOS 工程模板里ENABLE_BITCODE默认值就是 YES或者说至少在你的 App Target 和一部分 Pod Target 里是 YES。当年 Xcode 11、12 时代这个设置是能被正常接受的很多 Flutter 生成的项目就这么一路过来了。后来 Flutter 官方在新版本模板中把 bitcode 默认关闭了但关键问题在于如果你没重新生成工程、也没去改 Build Settings那个老配置会一直躺在你的 project.pbxproj 里。你换了新 Xcode 打开它设置项从界面上一看还在它就会把一个不该存在的默认值带到新的构建流程中。2.2 你以为关了其实还有隐藏的 bitcode这是我最想强调的一点。处理这类报错时我经常看到有人把自己的主 Target 的 Enable Bitcode 改成 NO 之后重新打包上传结果还是报同样的错然后一脸懵。实际上主 Target 只是入口你的 Flutter App 里还挂着一堆插件 Framework、CocoaPods 依赖库和 Extension Target。比如Runner主工程关了但是NotificationService这类 App Extension 的 Target 可能还开着。主工程和 Extension 都关了但某个第三方.framework是动态库打包时直接原样塞进Frameworks/目录它内部带__LLVM段App Store Connect 一扫描照样拒收。所以只看主 Target 的配置是典型的思维盲区。我在第一次修复时把所有 Target 全部翻了一遍才发现有个share_plus对应的 Pod 残留配置还在继承旧的 bitcode 设置。3. 按日志定位根因从 DerivedData 到每个 Framework在动手改配置之前先静下心把日志环境捋清楚。最忌讳的就是一上来闭着眼把 Enable Bitcode 全部改成 NO结果连是哪个二进制文件带 bitcode 都不知道。3.1 第一步拿到完整原始日志我处理问题的第一步永远是先把报错日志完整抓下来。xcodebuild 的输出别看那几行红字要把上下文一起看。比如我用命令构建的时候会把完整日志导出到文件flutter clean flutter pub get cd ios pod install xcodebuild archive \ -workspace Runner.xcworkspace \ -scheme Runner \ -configuration Release \ -archivePath build/Runner.xcarchive \ build.log 21然后把build.log里的bitcode关键字全部揪出来grep -i bitcode build.log这样能看到每一个有 bitcode 警告的具体路径。很多时候一条contains bitcode的 warning 后面会跟着完整的 Framework 路径这就是第一手嫌疑名单。3.2 第二步检查工程里所有 target 的 Enable Bitcode 状态打开 Xcode选中 Project搜索bitcode。你需要看的不只是 Runner 这个 target每个 target 都要点一遍包括 App Extension、Today Extension以及 CocoaPods 的生命周期里出现的 target。一个非常隐蔽的情况是Build Settings里搜索不到Enable Bitcode选项特别是在 Xcode 14 的新项目里默认就没这个设置项。但老项目里它是存在的。你需要确认的是这个列表Project 级别的ENABLE_BITCODE配置Runner Target 的 Debug / Release / Profile 三种配置其他 Extension Target 的配置Pods 工程里所有 Pod Target 的配置如果某个 target 当前值不是 NO且你还希望保留这个 target直接改成 NO。但这里有个细节CocoaPods 的工程通常不是直接改 UI因为每次pod install都会重新生成 Pods.xcodeproj。所以正确做法是改 Podfile 或者整个 pod 环境而不是进 Xcode 里逐个去点 Pod 的 Build Settings——你点了也是白点下次 pod install 就给你冲掉。3.3 第三步用 otool 揪出哪些库还藏着 bitcode这一步是我个人认为最有实用价值的一步。在归档完成之后你可以直接到.xcarchive里面对 App 包做体检。用otool检查一个二进制文件是否包含 bitcode核心是看它有没有__LLVM段cd build/Runner.xcarchive/Products/Applications/ otool -l Runner.app/Runner | grep -A3 __LLVM如果看到类似下面的输出sectname __bitcode segname __LLVM说明主二进制还带 bitcode。同理去检查 Frameworks 目录下所有动态库for f in Runner.app/Frameworks/*.framework; do echo $f otool -l $f/$(basename $f .framework) | grep -c __LLVM donegrep -c返回的数字大于 0就代表这个 framework 里存在 bitcode 段。把步骤 1 日志里的嫌疑库和这里的检查结果对一下你就能准确锁定目标。3.4 第四步确认 CocoaPods 是不是重新 install 过这一步非常容易忽略。旧 Flutter 项目往往还带着老版本 CocoaPods 生成的 pods 目录如果只是改了 Build Settings却没有重新pod install那 Pods 工程可能还停留在旧状态连带着各种老配置一起参与构建。我这次实际上先执行了pod deintegrate清掉整个 pods 集成关系然后重新pod install才保证所有 Pod Target 都基于当前 Build Settings 重新生成。这里的命令行操作在下一章展开先说结论如果项目已经很久没动建议别省这个步骤。4. 修复操作关 bitcode、清缓存、重新 Archive 一套流程确认了根因之后修复路径就清晰了。下面我按我自己执行的顺序完整过一遍每一步都标注了作用。4.1 关闭所有 Target 的 ENABLE_BITCODE最快的方式是在 Xcode 里逐个 target 搜索bitcode全部改成 NO。但如果你不想在 UI 上反复点直接改工程配置文件也行。找到ios/Runner.xcodeproj/project.pbxproj搜索ENABLE_BITCODE你会看到类似这样的片段buildSettings { ... ENABLE_BITCODE YES; ... };把整个文件里所有的ENABLE_BITCODE YES全部替换成ENABLE_BITCODE NO;我用sed快速处理sed -i s/ENABLE_BITCODE YES/ENABLE_BITCODE NO/g ios/Runner.xcodeproj/project.pbxproj注意这里有个大坑。改完 pbxproj 后千万不要直接打开 Xcode 跑构建因为 Xcode 在某些情况下会用xcshareddata/xcschemes里的缓存配置覆盖默认设置。我建议改完后顺手 git diff 看一下变动确认所有 target 都变过来了。还有一种情况你的工程里搜索不到ENABLE_BITCODE字段但依然报 bitcode 相关错误——这代表某个 target 或配置文件用了默认值。此时需要显式添加在buildSettings里补上ENABLE_BITCODE NO;即可。4.2 处理 Podfile 与 pod install 的联动问题只想改本地不是长久之计必须让后续任何pod install都默认生成不带 bitcode 的 Pod 工程。最干净的方式是在 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如果项目里用的是 Flutter 的默认 Podfile通常已经有一小段类似的post_install只需要把循环里的设置补进去。改完 Podfile 后执行cd ios pod deintegrate pod install这条组合拳能保证所有 Pod Target 重新生成并且继承 ENABLE_BITCODENO。不要只跑pod install在旧工程里我还是建议先 deintegrate 再 install原因很简单有些遗留配置会被原样保留在 Pods 工程里。4.3 清理 DerivedData 和 build 缓存这一步我吃了不少亏。前几次改完设置重新 Archive居然还在报错后来排查才发现是 DerivedData 里的缓存没有清干净Xcode 直接复用了旧的编译产物。所以在重新构建之前一定要做一次彻底清理rm -rf ~/Library/Developer/Xcode/DerivedData同时也建议把项目里的 build 目录删掉rm -rf build然后再回到项目根目录flutter clean flutter pub get这套“三连”做完再重新走一遍 Archivecd ios pod install xcodebuild archive \ -workspace Runner.xcworkspace \ -scheme Runner \ -configuration Release \ -archivePath build/Runner.xcarchive4.4 升级还是原地修旧版本 Flutter 项目的路线选择很多人会遇到这样一个抉择要不要顺便升级 Flutter 版本我的建议是如果只是要解决上架报错先别升级原地修复是成本最低的路径。旧 Flutter 项目升级到大版本通常伴随一堆连带问题比如老插件和最新 iOS SDK 不兼容Impeller 渲染引擎变化带来的视觉差异Android 侧 Gradle 配置大换血CocoaPods 版本要求变化这些问题的排查成本远超 bitcode 本身。原地关掉 bitcode、跑通上架流程比一次性做版本升级要稳妥得多。等新版本发完再单独评估升级计划也不迟。5. 上架阶段再遇 ITMS 相关报错的应对即使本地 Archive 已经没有任何 bitcode 警告到了上传阶段依然可能翻车。这一节我再写几种我当时实测过的处理套路。5.1 ITMS-90475 常见变体和处理上传报 ITMS-90475 时错误信息里一般会明确指出是哪个文件包含 bitcode例如ITMS-90475: Vector. Runner.app/Frameworks/SomeFramework.framework/SomeFramework contains bitcode.这就代表主二进制已经不带 bitcode 了某个动态 Framework 里还是存在问题。按第 3.3 节的命令去检查对应的 Framework如果确认它带__LLVM段处理方案优先排序如下找发行方要最新版本通常新版 SDK 会去掉 bitcode这是成本最低的方案。确定框架是静态库还是动态库如果是静态库链接进主二进制后会被ENABLE_BITCODENO的逻辑裁掉一般不影响上传如果是动态库就必须更换为无 bitcode 版本。极少数情况自己编译源码如果这个框架你手里有源码用当前 Xcode 重新编译一次build setting 里关掉 bitcode 即可。5.2 Xcode 14 以后 bitcode 选项消失怎么应对切换到 Xcode 14 之后你会发现有些工程里已经搜不到 Enable Bitcode 了。这不是项目坏了而是新版 Xcode 的 UI 把 bitcode 相关入口隐藏了。但旧工程里的 pbxproj 可能还保留着ENABLE_BITCODEYES.如果你打开 Build Settings 搜索不到但明确知道自己工程是旧模板直接按 4.1 节的sed方案处理 pbxproj 最省事。如果工程里确实没有ENABLE_BITCODE字段且报错来自某个库就需要先确认这个库是不是用旧 Xcode 编译的找对应方升级或重编即可。5.3 用 archive 日志和 altool / Transporter 验证上传修完之后最怕的就是“我以为没问题了”。上传验证这块我习惯的做法是先用 Xcode 的 Organizer 选择 Archive点击 Distribute App在导出界面选择 App Store Connect导出 ipa 前会有一步校验这一步如果能顺利走到导出基本说明本地归档产物符合预期。如果想完全命令行化可以用xcrun altool --upload-app \ -f Runner.ipa \ -t ios \ -u YOUR_APPLE_ID \ -p YOUR_APP_SPECIFIC_PASSWORD \ --verbose我自己实际上更推荐用 Transporter 上传因为它对错误提示更直观。Transporter 能直接把 ITMS 错误展开成可读内容方便确认最终产物是否还有带 bitcode 的残留库。6. 修改完成后的验证清单与旧项目维护建议当整个流程走到这里我最后再分享一份自己总结的检查清单。以后任何旧 Flutter 项目只要报 bitcode 相关错误照着这张表走基本能一遍过。6.1 先验证产物再谈上传每次讲完修复步骤我都喜欢强调一句不要光盯着 Xcode 的报错页面看要验证最终产物里到底还有没有 bitcode。构建完成后用这组命令对.app做一次总检查# 检查主二进制 otool -l Runner.app/Runner | grep -A3 __LLVM # 检查所有动态库 find Runner.app -name *.framework -exec sh -c binary_path$1/$(basename $1 .framework) echo --- $1 --- otool -l $binary_path | grep -q __LLVM echo HAS BITCODE || echo CLEAN _ {} \; # 或者直接看整个 .app 里的所有可执行文件 find Runner.app -type f -perm 111 -exec otool -l {} \; | grep -B2 -A4 __LLVM如果输出里没有__LLVM说明本地归档产物已经完全不含 bitcode。我自己是坚持每次 Archive 后都跑一遍这个验证不然到了上传阶段被拒又要从头来过。6.2 检查清单[ ] 所有 Target包括 Extension的ENABLE_BITCODE设为 NO[ ] Podfile 的post_install中设置了ENABLE_BITCODE NO[ ]pod deintegratepod install已执行Pods 工程已重新生成[ ] DerivedData 和 build 目录已清理[ ] Archive 产物通过otool检查确认无__LLVM段[ ] 使用 Xcode Organizer 或 altool/Transporter 走通上传流程[ ] App Store Connect 的构建版本状态显示“正在处理”而非错误6.3 旧项目维护的最后一点建议处理完这次问题我心里最强烈的感受是老 Flutter 项目就像陈年旧屋住是能住但每次碰到新环境都会冒出一两个“装修遗留问题”。bitcode 只是其中一个比较刺眼的。旧项目只要还能稳定发布就不必过度折腾但有些基本功这次之后必须养成——每个依赖升级前先确认它对 Xcode 14 之后构建链路的兼容性每次pod install后养成查一下 Build Settings 的习惯改动 Build Settings 不要只图界面点了就算务必落到project.pbxproj或 Podfile 这种可持续的配置里大概就是这样。这次修完之后同一个包顺利传上去了审核流程也没再被 bitcode 卡过。如果你现在正被这个报错折磨着先冷静下来拆解日志按上面的顺序一步步来会比直接到处找答案快得多。