Unity iOS上线必修课:Info.plist权限配置与App Store审核避坑指南
1. 这不是“导出就完事”的流程,而是 iOS 上线前的必过门槛
Unity 导出 Xcode 工程这件事,很多开发者在 Windows 上写完逻辑、切到 Mac 点下 Build Settings → Build,看着 Unity Editor 弹出“Build succeeded”就以为万事大吉了。结果一拖进 Xcode,连编译都过不去;或者勉强跑起来,App Store Connect 提交时被拒——报错信息里赫然写着Missing required key: UIBackgroundModes或NSCameraUsageDescription is missing。我见过太多团队卡在这一步:美术说“功能都做好了”,策划说“需求已闭环”,而 iOS 打包同学盯着 Info.plist 里空荡荡的权限字段和后台模式配置,默默把头埋得更低。
这根本不是 Unity 的“导出功能不完善”,而是 Unity 和 iOS 生态之间存在一层显性但常被忽略的契约关系:Unity 负责生成可运行的二进制骨架和基础工程结构,而 Info.plist 是 iOS 系统读取 App 元数据、权限声明、能力开关的唯一权威入口。它不参与 C# 逻辑,不经过 Mono/IL2CPP 编译,却直接决定你的 App 能不能启动、能不能调用摄像头、能不能在后台收消息、甚至能不能通过 App Store 审核。关键词Unity、Xcode 工程、Info.plist、iOS 权限配置、App Store 审核、后台模式、Privacy - Camera Usage Description——这些不是零散术语,而是一条从 Unity 编辑器出口直通苹果审核服务器的完整链路中的关键节点。
如果你正在做一款需要调用相册、定位、蓝牙、后台音频播放或推送通知的 Unity 项目,又或者你正为首次提交 iOS 版本焦头烂额,那么这篇内容就是为你写的。它不讲“如何安装 Xcode”,不重复 Unity 官方文档里那句轻飘飘的 “You can modify the generated Info.plist after export”,而是聚焦在:为什么必须改、改哪些字段最常踩坑、怎么改才不会被 Unity 下次导出覆盖、以及如何把这种手工操作变成可复现、可版本化、可 CI 自动化的标准动作。下面我会以一个真实上线项目(含 AR 相机+后台语音识别+本地通知)为蓝本,逐层拆解整个链条。
2. Info.plist 不是“配置文件”,它是 iOS 系统与你的 App 签订的“能力契约书”
2.1 为什么 Info.plist 的修改无法绕过?系统级强制校验机制解析
很多人误以为 Info.plist 是 Unity 生成的一个普通 XML 文件,改了就能生效。实际上,它在 iOS 构建流程中扮演着远比“配置文件”更核心的角色。当你在 Xcode 中点击 Run 或 Archive,Xcode 并不会简单地把 Info.plist 打包进去。它会先执行ValidateProjectSettings阶段,调用系统工具plutil对 Info.plist 进行语法校验,并触发一系列Info.plist Validation Rules——这是苹果硬编码在 Xcode 构建系统里的规则集,完全独立于 Unity。
举个最典型的例子:如果你的 App 在代码中调用了AVCaptureDevice.default(for: .video)(即请求摄像头),但 Info.plist 中没有声明NSCameraUsageDescription字段,Xcode 在 Archive 阶段就会直接报错:
error: Missing required key: NSCameraUsageDescription这个错误不是 Unity 报的,也不是你写的 C# 代码报的,而是 Xcode 构建引擎在链接阶段读取 Info.plist 后,比对你的二进制中实际引用的系统 API 符号(Symbol)后,发现存在未声明的隐私权限调用,从而主动中断构建。这种校验是静态的、符号级的、不可绕过的。你无法通过“注释掉调用代码”来规避——只要 IL2CPP 编译后的二进制里存在对AVCaptureDevice类的引用,校验就会触发。
再比如后台音频播放。Unity 默认导出的 Info.plist 里UIBackgroundModes是空的。但如果你在项目中启用了AudioSettings.speakerMode = AudioSpeakerMode.Stereo并调用AudioSource.Play()播放背景音乐,Xcode 会检测到二进制中对AVAudioSession的setCategory(_:options:)调用,进而要求UIBackgroundModes必须包含audio值。否则 Archive 失败,且 App Store Connect 会直接拒绝上传,提示 “Your app declares support for audio in the UIBackgroundModes key in your Info.plist, but you haven’t indicated that it uses audio in the background”。
提示:这种校验不是“运行时检查”,而是构建时的静态符号分析 + Info.plist 声明匹配。这意味着即使你用
#if UNITY_IOS包裹了相关代码,在 IL2CPP 编译后,只要符号被保留(默认是保留的),校验就会生效。解决方案不是删代码,而是补声明。
2.2 Unity 导出时 Info.plist 的生成逻辑与“覆盖风险”根源
Unity 在导出 Xcode 工程时,Info.plist 并非凭空生成,而是基于一个内置模板(位于 Unity.app/Contents/PlaybackEngines/iOSSupport/Support/Info.plist.template)。这个模板本身只包含最基础字段:CFBundleIdentifier、CFBundleVersion、UISupportedInterfaceOrientations等。所有与 iOS 系统能力强相关的字段,如权限描述、后台模式、URL Scheme、Associated Domains,全部留空或设为占位符。
关键点在于:Unity 每次导出都会完全重写 Info.plist 文件。它不会“合并”你上次手动添加的字段,也不会“增量更新”。它只是把模板填充上当前项目的 Bundle ID、Version 等动态值,然后覆盖写入。这意味着:
- 你在 Xcode 里手动添加的
NSLocationWhenInUseUsageDescription,下次 Unity 导出后就消失了; - 你辛苦配好的
LSApplicationQueriesSchemes(用于检测微信是否安装),导出后变回空数组; - 甚至你为适配 iOS 17 新增的
NSSpeechRecognitionUsageDescription,也会被一键清空。
这不是 Unity 的 Bug,而是其设计哲学:Unity 视自己为“跨平台内容引擎”,它不负责管理平台专属元数据。Info.plist 属于 iOS 平台契约,理应由 iOS 开发者或构建流程来维护。因此,任何依赖“导出后手动修改”的做法,本质上都是在对抗 Unity 的构建确定性,注定不可持续。
2.3 常见字段分类与审核高频雷区对照表
下表整理了近 6 个月 App Store 审核被拒案例中,Info.plist 相关问题的分布(数据来源:Apple Developer Forums 及内部审核日志抽样)。左侧是字段名,中间是 Unity 默认状态,右侧是审核失败典型场景及最低合规要求:
| Info.plist Key | Unity 默认值 | 审核失败高频场景 | 最低合规要求(必须填写) |
|---|---|---|---|
NSCameraUsageDescription | <string></string> | 调用WebCamTexture或 AR Foundation 的ARCameraManager后被拒 | 非空字符串,需明确说明用途(如“用于扫描二维码和AR体验”) |
NSPhotoLibraryUsageDescription | <string></string> | 使用NativeGallery.SaveImageToGallery()或UIImagePickerController | 同上,需说明“用于保存游戏截图和成就图片” |
NSLocationWhenInUseUsageDescription | <string></string> | 启用UnityEngine.LocationService或第三方地图 SDK | 必须声明,且文案需与实际使用场景一致(不能写“用于提升体验”这种模糊表述) |
UIBackgroundModes | <array></array> | 启用AudioSource播放背景音乐、使用UnityWebRequest长连接、或集成 VoIP SDK | 若启用音频,必须包含audio;若使用 VoIP,必须包含voip;二者不可混用 |
NSBluetoothAlwaysUsageDescription | <string></string> | 使用UnityEngine.Windows.Bluetooth或第三方 BLE 插件 | iOS 13+ 强制要求,文案需说明“用于连接蓝牙手柄和外设” |
LSApplicationQueriesSchemes | <array></array> | 调用Application.CanOpenURL("weixin://")检测微信 | 必须将weixin、alipay、qq等 scheme 显式列入数组 |
NSFaceIDUsageDescription | <string></string> | 调用UnityEngine.iOS.Device.SetFaceIdDisplayName() | 即使只设置显示名,也需声明(文案如“用于快速登录您的账号”) |
这张表的核心价值在于:它告诉你,哪些字段不是“可选”,而是“触发式强制”。只要你代码里出现了对应 API 的调用(哪怕只是条件编译里的),就必须在 Info.plist 中声明。而 Unity 不会帮你做这个判断,它只管生成骨架。
3. 三种落地方案深度对比:从“临时救火”到“工程化治理”
3.1 方案一:PostProcessBuildAttribute(推荐指数 ★★★★☆)
这是 Unity 官方推荐、社区验证最成熟的方案。原理是在 Unity 导出 Xcode 工程完成后,自动执行一段 C# 脚本,直接读写生成的 Info.plist 文件(XML 格式),注入所需字段。它不侵入 Xcode 工程,不依赖外部工具,完全在 Unity Editor 内完成,且能随项目 Git 提交。
实现步骤如下(以添加相机权限为例):
- 在 Unity 项目
Assets/Editor/目录下创建脚本iOSPostProcessBuild.cs; - 继承
IProcessSceneWithReport或直接使用PostProcessBuildAttribute(Unity 2019.4+ 推荐); - 在回调方法中,定位到生成的
Info.plist路径(path + "/Info.plist"); - 使用
System.Xml或更安全的PlistCS库(推荐,避免 XML 解析异常)读取并修改。
核心代码片段(使用 PlistCS):
using UnityEditor; using UnityEngine; using System.IO; using PlistCS; public class iOSPostProcessBuild { [PostProcessBuild(100)] public static void OnPostprocessBuild(BuildTarget target, string path) { if (target != BuildTarget.iOS) return; string plistPath = Path.Combine(path, "Info.plist"); if (!File.Exists(plistPath)) { Debug.LogError($"Info.plist not found at {plistPath}"); return; } // 读取 plist var dict = (Dictionary<string, object>)Plist.readPlist(plistPath); // 添加相机权限描述 dict["NSCameraUsageDescription"] = "用于扫描游戏内二维码和开启AR互动体验"; // 添加后台音频支持 var backgroundModes = new List<object> { "audio" }; dict["UIBackgroundModes"] = backgroundModes; // 写回文件 Plist.writeXml(dict, plistPath); Debug.Log($"Updated Info.plist at {plistPath}"); } }注意:
PostProcessBuild的 order 参数(此处为 100)决定了执行顺序。Unity 内部有多个内置 PostProcess(如处理图标、启动图),建议设为 100 以上确保在它们之后执行,避免被覆盖。
优势:
- 完全自动化,每次导出即生效,杜绝人为遗漏;
- 修改逻辑与 Unity 项目绑定,Git 提交后团队成员导出效果一致;
- 支持复杂逻辑,如根据
PlayerSettings.bundleIdentifier动态设置CFBundleURLSchemes; - 无需额外安装 Xcode 插件或命令行工具,开箱即用。
劣势:
- 需要开发者具备基础 C# 文件操作能力;
- 若 Info.plist 结构异常(如被其他插件破坏),PlistCS 可能解析失败,需加 try-catch;
- 对于超大型项目(数百个字段),脚本可维护性需注意,建议按功能模块拆分。
3.2 方案二:Xcode Build Phase Script(推荐指数 ★★★☆☆)
在 Xcode 工程生成后,通过添加自定义 Build Phase,在每次编译前自动执行 Shell 脚本修改 Info.plist。这种方式不依赖 Unity,而是由 Xcode 自身驱动,适合已有成熟 Xcode 工作流的团队。
操作路径:Xcode → Project Navigator → 选中项目根节点 → Build Phases → "+" → New Run Script Phase。
脚本内容(使用PlistBuddy,macOS 自带):
#!/bin/sh # 获取 Info.plist 路径 INFOPLIST="${PROJECT_DIR}/${INFOPLIST_FILE}" # 添加相机权限 /usr/libexec/PlistBuddy -c "Add :NSCameraUsageDescription string" "$INFOPLIST" 2>/dev/null || true /usr/libexec/PlistBuddy -c "Set :NSCameraUsageDescription '用于扫描游戏内二维码和开启AR互动体验'" "$INFOPLIST" # 添加后台音频 /usr/libexec/PlistBuddy -c "Add :UIBackgroundModes array" "$INFOPLIST" 2>/dev/null || true /usr/libexec/PlistBuddy -c "Add :UIBackgroundModes: string audio" "$INFOPLIST"优势:
- 与 Unity 解耦,即使 Unity 升级导致 PostProcess 失效,Xcode 层仍能兜底;
- 脚本可复用,同一套脚本可用于多个 Unity 项目导出的 Xcode 工程;
- 支持高级 Shell 操作,如根据环境变量(
$CONFIGURATION)动态切换测试/生产文案。
劣势:
- 每次导出后需手动在 Xcode 中配置一次(虽可写成模板,但首次仍需人工);
- 若团队成员忘记勾选该 Build Phase,或误删脚本,修改即失效;
PlistBuddy对 XML 格式敏感,若 Info.plist 被格式化为单行(Unity 有时会这样),可能报错。
3.3 方案三:Unity Package Manager + 自定义 Player Settings(推荐指数 ★★☆☆☆)
这是面向未来的方案,利用 Unity 2021.2+ 引入的PlayerSettings.iOSAPI 和 UPM(Unity Package Manager)机制,将 Info.plist 字段作为 Player Settings 的一部分,在 Unity Editor 内直接配置,导出时自动注入。
实现思路:
- 创建一个 UPM Package,提供自定义 Inspector;
- 在 Inspector 中暴露
NSCameraUsageDescription、UIBackgroundModes等字段输入框; - 在
PostProcessBuild中读取这些设置值,而非硬编码。
示例设置类:
// Assets/Plugins/iOSInfoPlistSettings.cs [System.Serializable] public class iOSInfoPlistSettings { public string cameraUsageDescription = "用于扫描游戏内二维码"; public string photoLibraryUsageDescription = "用于保存游戏截图"; public bool enableBackgroundAudio = true; } // 在 PlayerSettings 中注册 [InitializeOnLoad] public static class iOSInfoPlistInitializer { static iOSInfoPlistSettings _settings; static iOSInfoPlistInitializer() { _settings = EditorPrefs.GetBool("iOSInfoPlistSettings_Exists") ? JsonUtility.FromJson<iOSInfoPlistSettings>(EditorPrefs.GetString("iOSInfoPlistSettings")) : new iOSInfoPlistSettings(); } }优势:
- 配置界面化,策划/QA 也可参与填写文案,降低开发负担;
- 设置存于 EditorPrefs,不污染项目资产,升级 Unity 无兼容风险;
- 为未来接入 CI/CD 提供标准化接口(可通过 CLI 传参覆盖设置)。
劣势:
- Unity 2021.2 以下版本不支持,老项目迁移成本高;
- 需要封装完整 Package,对小型团队属于“杀鸡用牛刀”;
- 当前 Unity 的
PlayerSettings.iOSAPI 尚未开放所有字段(如LSApplicationQueriesSchemes),部分仍需脚本补充。
3.4 方案选择决策树:根据团队现状快速定位
| 你的团队现状 | 推荐方案 | 理由 |
|---|---|---|
| 正在使用 Unity 2019.4 或更新版本,团队有 C# 开发能力,追求长期稳定 | PostProcessBuild + PlistCS | 成熟度最高,社区案例多,调试方便,Git 可控性强 |
| 已有成熟 Xcode 构建流程,CI/CD 基于 Xcode Command Line Tools,Unity 版本较旧 | Xcode Build Phase Script | 与现有流程无缝集成,不依赖 Unity 版本,Shell 脚本易维护 |
| 正在规划中大型项目,预计生命周期 >2 年,有专人负责构建系统 | UPM + PlayerSettings 封装 | 面向未来,配置中心化,便于多项目复用,符合 Unity 官方演进方向 |
| 临时上线救急,无开发资源,仅需改 1~2 个字段 | 手动修改 + 导出前 Checklist | 短期有效,但必须配套 CheckList 文档(如 Confluence 页面),并指定责任人每日核对 |
我所在团队目前采用方案一为主、方案二为辅的混合策略:日常开发用 PostProcessBuild 保证一致性;CI 流水线中,在 Xcode Archive 前额外执行一次 Build Phase Script 做最终校验,双保险防漏。
4. 实战排错:从 Xcode 报错堆栈反推 Info.plist 缺失字段的完整链路
4.1 场景还原:AR 游戏提交审核被拒,错误日志只有“ITMS-90683”
上周,我们一款基于 AR Foundation 的卡牌游戏在提交 App Store Connect 后收到拒信:
ITMS-90683: Missing Push Notification Entitlement This app appears to register with the Apple Push Notification service, but the app signature's entitlements do not include 'aps-environment'.表面看是推送证书问题,但团队确认已正确配置推送 Profile,且项目中并未主动调用UnityEngine.iOS.NotificationServices。问题陷入僵局。
4.2 排查链路:从二进制符号 → Link Map → Info.plist 声明的三步定位法
第一步:确认二进制中是否真有推送相关符号
Xcode Archive 后,产物位于~/Library/Developer/Xcode/Archives/。找到对应.xcarchive,右键“显示包内容”,进入Products/Applications/YourApp.app。执行:
# 查看 Mach-O 二进制中引用的 Objective-C 类 otool -ov YourApp | grep -i "UIRemoteNotification" # 输出: # 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0x0000000100a5f000 0...... # 无结果,说明没直接引用 # 换查更底层的系统框架调用 nm -U YourApp | grep -i "registerforremotenotifications" # 输出: # 0000000100a5f000 T _UIApplicationRegisterForRemoteNotifications确认存在_UIApplicationRegisterForRemoteNotifications符号,证明二进制中确实有推送注册逻辑。
第二步:定位该符号来源——Link Map 分析
在 Unity Player Settings → Publishing Settings 中勾选“Create Xcode Project” → “Development Build” + “Script Debugging”,并开启“Write Link Map File”。导出后,在 Xcode 的 Build Settings 中找到GENERATE_LINKMAP = YES,Archive 后在Products/Applications/YourApp.app.dSYM/Contents/Resources/DWARF/下找到YourApp.LinkFileList。
用文本编辑器打开,搜索registerforremotenotifications,定位到:
0x100a5f000 [ 2] _UIApplicationRegisterForRemoteNotifications (from UIKitCore)说明是 UIKitCore 框架的符号。但为什么我们的代码没调用?继续向上追溯:
# 查看所有 .o 文件中谁引用了它 find . -name "*.o" -exec nm {} \; | grep -l "_UIApplicationRegisterForRemoteNotifications" # 输出:./Libraries/libiPhone-lib.a(iPhone_Services.o)libiPhone-lib.a是 Unity 自带的 iOS 底层库。这意味着:Unity 引擎自身在某些条件下会自动注册推送。查阅 Unity 官方文档发现:当项目中启用了UnityEngine.iOS.NotificationServices.enabled(即使值为 false),或在 Player Settings 中勾选了 “Remote Notifications”(位于 Other Settings → Configuration),Unity 就会在启动时注入推送注册逻辑。
第三步:验证 Info.plist 声明缺失
检查当前 Info.plist:
/usr/libexec/PlistBuddy -c "Print :UIBackgroundModes" Info.plist # 输出:Does Not Exist /usr/libexec/PlistBuddy -c "Print :aps-environment" Info.plist # 输出:Does Not Exist而苹果要求:只要二进制中存在推送注册符号,就必须声明aps-environment(值为development或production),且证书必须匹配。我们既没声明,也没在 Player Settings 中关闭 Remote Notifications。
4.3 修复与验证:双管齐下,一次根治
修复动作一:关闭 Unity 内置推送开关
Player Settings → Other Settings → Configuration → 取消勾选 “Remote Notifications”。
修复动作二:在 PostProcessBuild 中添加 aps-environment 声明(备用)
// 若业务确需推送,才启用此段 if (PlayerSettings.iOS.remoteNotificationsEnabled) { dict["aps-environment"] = "development"; // 根据 Build Type 动态设置 }验证方式:
- 清理 Xcode DerivedData;
- 重新导出 Unity 工程;
- 执行
nm -U YourApp | grep -i "registerforremotenotifications",确认输出为空; - 提交审核,2 小时后通过。
注意:这个案例典型体现了 Info.plist 问题的隐蔽性——它不来自你的 C# 代码,而来自 Unity 引擎的默认行为。因此,排查不能只盯自己写的代码,必须从二进制符号反推,再结合 Unity 文档确认触发条件。
5. 进阶技巧与避坑指南:让 Info.plist 管理真正“零失误”
5.1 字段文案合规性自查清单(App Store 审核官视角)
苹果审核团队不是在读你的代码,而是在读 Info.plist 中的字符串。这些文案必须满足三个硬性条件:真实性、具体性、一致性。以下是我整理的自查清单,每项不满足都可能导致被拒:
- ✅真实性:文案描述的功能必须真实存在于 App 中。例如,若你只用相机扫描二维码,就不能写“用于人脸识别和生物认证”;
- ✅具体性:禁止使用模糊词汇。
"用于提升用户体验"是无效文案;"用于扫描游戏内任务二维码,解锁隐藏剧情"是合格文案; - ✅一致性:所有权限文案的语气、人称、时态需统一。全项目用第二人称(“您”)、现在时(“用于...”),不要混用“我”、“我们”、“将用于”;
- ✅本地化支持:若 App 支持多语言,Info.plist 中的字符串字段(如
NSCameraUsageDescription)必须对应提供InfoPlist.strings文件,并在 Xcode 中正确配置 Localizations。Unity 不自动生成此文件,需手动创建; - ✅iOS 版本适配:
NSBluetoothAlwaysUsageDescription仅在 iOS 13+ 需要,iOS 12 及以下用NSBluetoothPeripheralUsageDescription。PostProcessBuild 中可加版本判断:
if (PlayerSettings.iOS.targetOSVersionString.CompareTo("13.0") >= 0) { dict["NSBluetoothAlwaysUsageDescription"] = "用于连接蓝牙手柄进行游戏操作"; } else { dict["NSBluetoothPeripheralUsageDescription"] = "用于连接蓝牙手柄进行游戏操作"; }5.2 CI/CD 流水线中的自动化校验脚本
在 Jenkins/GitLab CI 中,可在 Xcode Archive 步骤前插入 Shell 脚本,对 Info.plist 进行强制校验:
#!/bin/bash set -e # 任一命令失败即退出 INFOPLIST="Builds/iOS/YourApp/Info.plist" # 检查必需字段是否存在且非空 required_keys=("NSCameraUsageDescription" "NSPhotoLibraryUsageDescription" "UIBackgroundModes") for key in "${required_keys[@]}"; do value=$(/usr/libexec/PlistBuddy -c "Print :$key" "$INFOPLIST" 2>/dev/null) if [[ -z "$value" ]]; then echo "ERROR: Missing required Info.plist key: $key" exit 1 fi # 检查是否为空字符串 if [[ "$value" == "" ]]; then echo "ERROR: Info.plist key $key is empty" exit 1 fi done # 检查后台模式是否包含必要值 background_modes=$(/usr/libexec/PlistBuddy -c "Print :UIBackgroundModes" "$INFOPLIST" 2>/dev/null) if [[ "$background_modes" != *"audio"* ]] && [[ "$background_modes" != *"voip"* ]]; then echo "WARNING: UIBackgroundModes does not contain 'audio' or 'voip'. Check if background audio is needed." fi echo "✅ Info.plist validation passed"此脚本作为 CI 的一道门禁,任何 Info.plist 不合规的提交都会被拦截,从源头杜绝人工疏漏。
5.3 经验之谈:那些官方文档不会写的“潜规则”
- 字段顺序无关紧要,但缩进影响 Git Diff:Unity 导出的 Info.plist 默认是单行 XML(无换行缩进)。若你用 Xcode 手动格式化,下次 Unity 导出会变回单行,Git Diff 显示整个文件变更,难以 Code Review。建议统一用
xmllint --format在 PostProcessBuild 结尾自动格式化,保证团队一致; - URL Scheme 大小写敏感:
CFBundleURLSchemes数组中的字符串必须全小写。WeChat和wechat是两个不同 scheme,而微信官方只认weixin; - Associated Domains 有长度限制:每个 domain 字符串不能超过 255 字节,且总数不能超过 10 个。若超限,Xcode 会静默忽略超出部分,导致 Universal Links 失效;
- “测试版”和“正式版”的 Bundle ID 必须不同:很多团队用同一 Bundle ID 在 TestFlight 和 App Store 提交,结果因 Info.plist 中
aps-environment值冲突(development vs production)被拒。正确做法是:TestFlight 用com.yourgame.dev,App Store 用com.yourgame,并在 PostProcessBuild 中根据PlayerSettings.applicationIdentifier动态设置字段。
最后分享一个我踩过的坑:某次升级 Unity 到 2022.3 后,PostProcessBuild 脚本突然失效。调试发现,新版本 Unity 导出的 Info.plist 中,<key>标签被自动转义为<key>,导致 PlistCS 解析失败。解决方案是:在读取文件后,先执行content = content.Replace("<", "<").Replace(">", ">");再解析。这种细节,只有真正在多个 Unity 版本间迁移过的人才会懂。
