在 App Store 审核流程中踩到Unsupported Architecture这个报错,几乎是每个跟第三方动态库打过交道的 iOS 开发者都会遇到的坎。尤其是当你从某个 SDK 供应商那里拿到的 .framework 或者动态库同时包含了 x86_64 和 i386 架构时,上传到 App Store Connect 后,邮件里赫然躺着类似“Unsupported Architecture. Your app contains an unsupported architecture: x86_64”这样的警告,运气好只是警告,运气不好直接就是审核被拒。这篇文章就把这个问题的来龙去脉、手动剥离方法、自动化脚本方案以及我在实际项目中踩过的坑一次讲透,希望能帮你少走弯路。
先说清楚这篇文章适合谁看:如果你的项目用到了第三方动态库(包括 CocoaPods 开启 use_frameworks! 后生成的动态 framework),并且遇到过或者担心遇到架构相关的上架问题,那么接下来的内容可以让你在十分钟内拿到一套可落地的解决方案。
1. 问题拆解:为什么 App Store 会对 x86_64 和 i386 架构敏感
1.1 架构与编译环境的对应关系
在 iOS 开发中,我们经常听到 arm64、armv7、x86_64、i386 这几种架构名称。它们分别对应不同的运行环境:
- arm64:iPhone 5s 及之后所有真机设备的 64 位处理器架构,也是当前 App Store 唯一要求支持的架构。
- armv7:iPhone 4s 到 iPhone 5c 时代的 32 位真机架构,如今已经基本退出审核要求,但部分老旧 SDK 仍会在二进制中携带。
- x86_64:64 位模拟器(iPhone Simulator)使用的架构,跑在 Mac 的 Intel 或 Apple Silicon 模拟器环境下。
- i386:32 位模拟器架构,在更早的 Xcode 版本中比较常见,现在基本被 x86_64 取代。
说直白点,x86_64 和 i386 是模拟器专用架构,它们的目的是让你在开发调试阶段不用真机也能跑代码。问题在于,当你把包含这些架构的 framework 直接打进 Release 包并上传审核时,App Store 会认为你的应用携带了不被支持的架构——因为 App Store 分发到用户设备上的包只需要真机架构,模拟器架构在 App Thinning 和系统校验时完全是多余的。
1.2 动态库里为什么会混入模拟器架构
我知道你肯定想问:既然模拟器架构不能上架,为什么第三方 SDK 还会把 x86_64 打进去?原因很现实——第三方框架要同时照顾开发者的模拟器调试体验和真机运行需求。一个完整的胖二进制(Fat Binary)通常同时包含 arm64 和 x86_64 两套架构,开发者在模拟器里跑调试版时需要 x86_64 部分,真机运行和上架时需要 arm64 部分。
很多老牌 SDK 的打包脚本并不区分 Debug 和 Release,或者由于历史原因直接把编译产物做成了多架构合一。还有一部分情况是团队内部自研的动态库,在打包阶段没有做架构裁剪,直接把模拟器构建的版本连同真机版本一起合并进了产物。
1.3 为什么必须剥离而不是忽略
有些同学看到这个报错后,第一反应是“反正只是个警告”,于是抱着侥幸心理直接点了提交。从我实际处理过的案例来看,这个行为相当危险。早期 App Store 对于 x86_64 架构可能只给警告,但随着审核策略收紧,现在这个报错已经频繁导致审核被拒,错误码通常长这样:
Unsupported Architecture Your app contains an unsupported architecture: x86_64如果你选择忽略,审核团队完全有理由拒绝你的二进制,理由是你的包中包含非 iOS 设备所需的架构代码,这既影响包体积,也反映产品不够规范。所以正确姿势非常明确:在上传之前,把动态库中所有非目标架构剥离干净。
2. 架构剥离方案选型:三种思路对比
想要彻底解决这个问题,单靠一种方法可能会比较被动。我在不同项目中用过三类方案,每一种都有它的适用场景和局限。
2.1 方案一:构建阶段自动剥离脚本
这是我在绝大多数项目中采用的主力方案,通过 Xcode 的 Build Phase 添加一段 shell 脚本,在每次编译结束后自动扫描并剥离所有动态库中的多余架构,尤其是 Release 配置下。
它的优点非常明显:一劳永逸。团队里任何人都不会因为忘记手动处理而让产物出问题。脚本的适用面也很广,不区分第三方 SDK 还是自研 framework,只认二进制文件里的架构类型。缺点是脚本逻辑需要仔细写,尤其是在处理符号链接和目录结构时容易出错,另外每次构建都多跑一轮循环,会增加少量编译时间。
2.2 方案二:手动执行 lipo 命令
在你只需要一次性处理某个框架,或者做临时验证时,直接用终端里的 lipo 工具手动操作是最快的。
lipo 是 macOS 自带的命令行工具,专门用来查看和操作胖二进制文件。你可以先用lipo -info查看某个 framework 二进制包含哪些架构,再用lipo -remove移除指定架构,或者用lipo -thin只保留一个架构。
手动方案最大的好处是直观、可控,命令执行完立即生效。但它也有明显的天花板——如果项目里的动态库数量非常多,或者每次拉取最新 SDK 后都要重复处理,手动效率就太低了,而且很容易遗漏一两个库,最终仍然在审核时爆雷。
2.3 方案二:替换 XCFramework —— 从源头根治
这个方法值得深刻理解,虽然它属于“新工程新架构”范畴,但也能用来处理现有依赖。XCFramework 是 Apple 在 Xcode 11 引入的二进制打包格式,它的优势在于用一个 .xcframework 目录统一管理多平台、多架构的二进制,而不是把多个架构塞进同一个二进制文件里。App Store 接受的框架包可以精确到按需加载对应架构,不会出现混入模拟器架构的问题。
对于可以联系到供应商、或者自己掌控源码的库,我会优先建议直接让 SDK 方提供 xcframework 格式。但现实是,很多老 SDK 年久失修,供应商根本不会再更新,这时候还是得靠脚本方案兜底。因此我的推荐顺序是:能用 XCFramework 就优先迁移,迁移不了就上脚本。
3. 实操过程:从诊断到自动化脚本完整落地
3.1 第一步:揪出问题动态库
在改动任何配置之前,先确定到底是哪个动态库包含问题架构。这一步看似简单,但很多人一上来就写脚本,结果脚本扫了个寂寞。正确流程是进入项目的编译产物目录,逐个检查 framework。
假设你的项目编译产物的路径是DerivedData下的某个 Build 目录,你可以先在 Xcode 里查看对应的路径:菜单栏 File → Project Settings → Derived Data,点开小箭头直接进入该目录。然后使用find和lipo -info组合,快速定位所有 framework 的架构情况。
cd /path/to/YourProject/DerivedData/.../Build/Products/Release-iphoneos find . -name "*.framework" -type d -maxdepth 3 | while read fw; do binary="$fw/$(basename "$fw" .framework)" if [ -f "$binary" ]; then echo "==> $binary" lipo -info "$binary" fi done执行结果通常会分成三类:只有 arm64 的框架属于健康状态;同时包含 arm64 和 x86_64 的框架就是问题根源;如果输出里还有 i386,那更是陈年老雷,必须一并处理。
值得说明的是,在 Release-iphoneos 目录下一般不应该出现模拟器架构,如果你在这里看到了 x86_64,说明这个动态库极可能是直接从 Debug 或者模拟器构建产物里拷贝过来的。如果 Release 目录里查不到异常,但上传后仍然报错,请检查你是否手动把某些 framework 拖进了 Copy Files,或者某个 Run Script 阶段又把带模拟器架构的版本拷回了产物。
3.2 第二步:编写架构剥离脚本
拿出一键清扫的思路,我习惯把剥离逻辑做成一个通用脚本,放进项目中作为 Run Script Phase 执行。脚本核心原理并不高深:遍历指定目录下的所有 framework 二进制,用lipo -info判断,如果发现非 arm64 架构,就使用lipo -remove逐一移除。
下面这个脚本我在多个项目中验证过,兼容性强,可以直接复制调整使用:
#!/bin/bash # 架构剥离脚本 # 使用场景: Xcode Build Phase 中嵌入, Release 构建后自动清理动态库中的模拟器架构 # 注意: 建议在 "Embed Frameworks" 或 "Copy Files" 之后执行 APP_PATH="${TARGET_BUILD_DIR}/${WRAPPER_NAME}" # 这里可以根据需要选择要保留的架构, 一般上架只需要 arm64 SUPPORTED_ARCHS="arm64" FRAMEWORKS_DIR="$APP_PATH/Frameworks" if [ ! -d "$FRAMEWORKS_DIR" ]; then echo "警告: Frameworks 目录不存在, 跳过架构剥离" exit 0 fi find "$FRAMEWORKS_DIR" -name "*.framework" -type d | while read framework; do framework_name=$(basename "$framework" .framework) binary_path="$framework/$framework_name" if [ ! -f "$binary_path" ]; then echo "跳过: 未找到二进制 $binary_path" continue fi echo "检查: $framework_name" archs=$(lipo -info "$binary_path" | rev | cut -d ':' -f1 | rev | tr ' ' '\n' | grep -v '^$') for arch in $archs; do case "$SUPPORTED_ARCHS" in *"$arch"*) echo "保留架构: $arch" ;; *) echo "剥离架构: $arch" lipo -remove "$arch" "$binary_path" -o "$binary_path" if [ $? -ne 0 ]; then echo "错误: 剥离 $arch 失败, 请检查二进制文件完整性" exit 1 fi ;; esac done # 验证最终架构 echo "处理完成: $(lipo -info "$binary_path")" done echo "所有动态库架构剥离完毕"这里有几个细节必须强调:
第一,lipo -remove "$arch" "$binary_path" -o "$binary_path"这种原地写回方式用得非常多,官方支持,因为 lipo 打开文件后不会强行占用文件句柄。如果你担心文件正在被使用,可以先输出到临时文件,再mv覆盖回去,例如:
lipo -remove "$arch" "$binary_path" -o "$binary_path".tmp mv "$binary_path".tmp "$binary_path"第二,脚本里的SUPPORTED_ARCHS变量我默认写了arm64。如果你的最低支持版本确实还需要 armv7,需要先确认 App Store 当前是否还接受 armv7。我个人的建议是不要再保留 armv7,一方面原因是最新 Xcode 版本已经很少生成 armv7 代码,另一方面是保留一个被淘汰架构只会增加包体积,对用户体验没有任何帮助。
第三,ARCHS环境变量是 Xcode 自带的,但VALID_ARCHS在一些项目里被外部配置覆盖过。脚本里不依赖这两个变量,直接自己判断,这样更稳定。
3.3 第三步:在 Xcode 中配置 Build Phase
脚本写好之后,怎么让它自动跑起来是关键。如果只是放到终端手动执行,那就失去了自动化的意义。正确配置方式如下:
打开 Xcode,选中你的 target,进入 Build Phases 标签页,点击左上角的加号,选择 New Run Script Phase。把新建的 Run Script 拖动到 Embed Frameworks 或者 Copy Files 阶段之后,因为必须先完成 framework 拷贝,才能对目标目录里的文件做剥离。
然后在脚本区域粘贴上一步的脚本,并且确保Shell填写为/bin/bash(或者/bin/sh,但要注意 bash 和 sh 在语法上略有差异,我的脚本是基于 bash 写的,建议直接指定 bash)。
这里有个容易踩坑的地方:如果你在 Debug 配置下也运行这个脚本,那么模拟器跑代码时会发现动态库的 x86_64 架构被删了,导致模拟器起不来。解决方式两种:一是把脚本用条件判断包起来,只让它在 Release 下执行;二是在 Run Script 的 “Based on dependency analysis” 下方的输入框里根据配置控制,但最直观的还是脚本加一行判断,例如:
if [ "${CONFIGURATION}" != "Release" ]; then echo "非 Release 构建, 跳过架构剥离" exit 0 fi这样处理之后,Debug 配置下模拟器照常工作,Release 配置下动态库被自动净化,彼此互不干扰。
3.4 第四步:验证结果
配置完成后,直接用 Release 模式跑一次 Archive。拿到产物之后,再做一遍完整的检查:
cd ~/Library/Developer/Xcode/DerivedData/YourApp-xxxx/Build/Intermediates.noindex/ArchiveIntermediates/YourApp/IntermediateBuildFilesPath/UninstalledProducts/iphoneos ## 或者直接查看 Archive 中的 .app cd /path/to/YourArchive.xcarchive/Products/Applications/YourApp.app/Frameworks find . -name "*.framework" -maxdepth 2 | while read fw; do lipo -info "$fw/$(basename "$fw" .framework)" done如果所有动态库都只显示 arm64,那么恭喜你,这一步已经将最大的上架隐患排除了。接着再用正常的验证流程,Product → Archive → Distribute App 上传,如果之前有 Unsupported Architecture 警告,这次就会从警告列表里消失。
4. 常见问题与排查技巧实录
4.1 剥离后动态库无法启动
这个情况我遇到过两三次,现象是脚本跑成功了,架构也确实变成了 arm64,但 App 一启动就崩溃,或者加载 framework 时报错 “Library not loaded for architecture” 之类。这通常不是剥离脚本的问题,而是因为你剥离的是一个本来就不完整的二进制,或者该动态库存在依赖链。比如 framework 在编译时链接了另一个只含 x86_64 的静态库,当你剥掉 x86_64 后发现还有别的符号找不到。
排查思路很简单,崩溃时抓取设备日志,用 symbolicatecrash 解析后看崩溃在哪个动态库的哪个方法。如果问题锁定在特定第三方库上,而且该库更新频繁,建议先检查是否拿到了新版,新版本往往已经修复了架构混打问题。
4.2 脚本只在默认 configuration 生效
当你为项目配置了多个自定义的 Build Configuration(例如 Staging、PreProd),并且 Archive 时使用了这些自定义配置,那么脚本里的CONFIGURATION判断可能永远不满足,导致剥离一直没有执行。解决方式不是把条件改成!= "Debug"那么简单,而是明确计划保留的配置白名单。我在实际项目里用的是:
# 只在需要上架的配置下剥离 if [[ "${CONFIGURATION}" != "Release" && "${CONFIGURATION}" != "Staging" ]]; then exit 0 fi最重要的还是每次 Archive 后手动抽查一下最终的产物,别盲目相信脚本一定跑了。
4.3 CocoaPods 与 use_frameworks! 的场景
现在很多 Swift 项目依然在用 CocoaPods,并且开启了use_frameworks!,这样 pods 里的库会被编译成动态 framework。如果你碰到 Unsupported Architecture 警告,又是从某个 pod 产生的,别急着写脚本。先确认 pod 的版本和仓库配置,有些 podspec 默认把EXCLUDED_ARCHS[sdk=iphonesimulator*]设置好了,但真机 Release 下可能出现属性覆盖。
针对 CocoaPods 的架构问题,还有一个更优雅的解法:在 Podfile 顶部添加 post_install hook:
post_install do |installer| installer.pods_project.targets.each do |target| target.build_configurations.each do |config| config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64' end end end如果你用 Xcode 12 及以上版本,且只跑 Apple Silicon 模拟器,这段代码能解决绝大多数 pod 架构提示问题。但需要留意,它的真实作用是控制编译期不会生成多余的模拟器架构,而不是对已经打进 App 的二进制做事后清理,所以跟前面脚本的定位不一样。
4.4 XCFramework 迁移的操作路径
刚才提过从源头根治的方法是 XCFramework。如果你决定把一个老旧的 framework 进行迁移,步骤并不复杂:拿到该 framework 的模拟器版本和真机版本,用 Xcode 自带工具打包成 xcframework。一个简单命令如下:
xcodebuild -create-xcframework \ -framework ./build/Release-iphoneos/YourSDK.framework \ -framework ./build/Release-iphonesimulator/YourSDK.framework \ -output ./build/YourSDK.xcframework但这要求你手里同时有真机和模拟器两个版本的 framework。如果只有胖二进制也可以,先 lipo -thin 拆分出两个架构的单架构版本,再合成 xcframework。这个过程比想象中繁琐,而且涉及 Info.plist 的兼容性验证,没有把握时先做一个小 Demo 验证一下再合入主工程。
下面把几个高频问题整理成一张速查表,方便你以后遇到类似情况快速对照:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Archive 成功后 lipo -info 仍有 x86_64 | Run Script 位置太靠前,运行时 framework 还没拷贝进目标目录 | 把 Run Script 挪到 Embed Frameworks 之后 |
| 模拟器 Debug 跑不起来,framework 加载失败 | 脚本在 Debug 配置下也执行了剥离 | 增加 CONFIGURATION != "Debug" 的判断 |
| 多个 framework 中只有个别剥离失败 | 二进制文件只读,或者处于锁定状态 | 先 chmod +w,再执行剥离;或输出到临时文件后 mv |
| 上传后仍有 Unsupported Architecture 警告 | 某些 framework 被拷贝进了 .app 根目录而非 Frameworks 目录 | 扩展脚本,同时检查 APP_PATH 下所有 framework |
| 不同架构间符号冲突 | 使用了同一个 framework 的多个副本 | 清理干净的 Build 目录后重新 Archive |
5. 一些在实战中沉淀下来的额外经验
很多人在处理流程走完之后基本不会再管这个脚本,但我更建议你在项目的 README 或者构建说明里留一段注释,写清楚脚本是干什么用的、为什么存在、维护时需要注意什么。团队协作时,别人看到 Run Script 里一大段 shell 脚本很容易犯迷糊,不知道能不能删,也搞不清什么时候会曝光问题。
另外一个比较冷门的细节:Xcode 14 之后,EXCLUDED_ARCHS设置在某些情况下会被 Xcode 忽略,特别是当你把 architecture 设置为$(ARCHS_STANDARD)时。所以不要过度依赖 Xcode 的构建设置,build phase 脚本仍然是最可靠的兜底手段。
App Store 审核的生命周期里,架构问题属于那种爆发一次就让人印象深刻的红牌警告。说实话,在工作流里加一段十几行脚本就能免掉重复劳动,还能让团队其他成员不踩同样的坑,这笔投入是非常划算的。
这个问题的后续扩展方向也很实际:如果你们公司的 SDK 开始转向 XCFramework,甚至用上了 Swift Package Manager 分发二进制,架构管理的方式还会进一步简化。只要明白 lipo、xcframework、Build Phase 脚本这些底层工具的工作思路,无论格式怎么变,你都能在第一时间给出靠谱结论。