App Store Connect 隐私清单无效时,不要先在主 App 里补一份通用清单,也不要盲目删除依赖文件。先读取邮件中的错误代码、文件名和 Bundle 路径,再到最终 xcarchive 中定位责任 Target,依次检查 plist 格式、键值、Required Reason API、第三方 SDK 来源和资源位置;修复后必须重新 Archive、重新签名,并完成一次真实上传验收。
这套方法适合收到 ITMS-91056 或类似隐私清单错误、急着恢复 TestFlight 或 App Store 提交的独立开发者。
如果你的项目使用 Swift Package Manager、CocoaPods 或 XCFramework,并且源码看起来正常但上传仍被拒绝,本文可以帮助你把问题从“猜测”变成可验证的证据链。
01 先判断:错误发生在源码、Archive,还是上传处理阶段
一个常见失败场景是:源码目录里的 PrivacyInfo.xcprivacy 看似没有问题,但 App Store Connect 指向了嵌套 Framework 内另一份无效文件。此时继续修改主 App 的清单,只会增加排查噪音。
Apple 的错误邮件通常会给出错误类型、文件名和 Bundle 内路径。ITMS-91056 对应的是无效隐私清单,原因可能是 plist 格式错误,也可能是键名、值类型或允许值不符合规则。你应先保存邮件原文,再把路径拆成以下信息:
- 责任产品:主 App、Extension、App Clip、Framework,还是其他嵌套 Bundle。
- 文件位置:是 App 根目录、
Contents/Resources/,还是 Framework 内部。 - 错误性质:无效、缺失、Required Reason API 未声明,或第三方 SDK 签名异常。
- 产物版本:邮件对应哪一个 Archive,而不是当前工作区里的源码版本。
打开 Xcode 的 Organizer,进入 Archives,找到与失败上传对应的归档,然后选择“在 Finder 中显示”。对 .xcarchive 执行“显示包内容”,进入 Products/Applications/,再沿着邮件里的 Bundle 路径检查实际文件。
如果你通过持续集成或远程 Mac 构建,建议把以下内容保留在同一构建目录:
- 依赖锁定文件。
- Archive 路径和构建日志。
- Xcode 生成的隐私报告。
- App Store Connect 错误邮件。
- 修复后上传的处理结果。
这样可以确认你修复的是同一份提交,而不是本地修改后又被 CI 重新解析出旧依赖。
02 第一步:先做 Archive 路径和产物清单检查
PrivacyInfo.xcprivacy 在 xcarchive 中的位置,取决于它属于哪一个 Bundle。不要把所有平台都当成同一种目录结构。
Apple 给出的关键位置包括:
- iOS、iPadOS、tvOS、visionOS 或 watchOS App:通常位于
YourApp.app/PrivacyInfo.xcprivacy。 - macOS 和 Mac Catalyst App:位于
YourApp.app/Contents/Resources/PrivacyInfo.xcprivacy。 - iOS 等平台的 Framework:位于
YourFramework.framework/PrivacyInfo.xcprivacy。 - macOS 或 Mac Catalyst Framework:位于 Framework 的
Versions/A/Resources/下。
这些位置不是装饰性目录。文件放错位置,即使名称正确,也可能不会被 Xcode 的隐私报告或 App Store Connect 按责任 Bundle 识别。具体目录规则可对照 Apple 的隐私清单放置说明。
在不修改原始 Archive 的前提下,可以先复制一份用于检查:
cp -R "/path/to/YourApp.xcarchive" "/path/to/YourApp-inspection.xcarchive"
find "/path/to/YourApp-inspection.xcarchive/Products" \
-name "PrivacyInfo.xcprivacy" \
-print
随后记录每一份清单的完整路径。重点不是“有几份文件”,而是每一份文件属于哪个责任 Bundle,以及它是否与邮件中的路径一致。
主 App、Extension 和 App Clip 需要分别处理吗?
需要。主 App 的清单不能自动替代 Extension、App Clip 或第三方 Framework 自身的清单。只要相关可执行文件或动态库使用了 Required Reason API,就要在包含该代码的 Bundle 中提供对应声明;第三方 SDK 也不能依赖主 App 的清单代替自身声明。
03 第二步:把 plist 语法检查和规则检查分开
先检查文件能不能被解析,再检查内容是否符合隐私清单规则。两者不是同一件事。
对复制出的 Archive 执行:
plutil -lint \
"/path/to/YourApp-inspection.xcarchive/Products/Applications/YourApp.app/PrivacyInfo.xcprivacy"
如果路径包含空格,保留引号。plutil -lint 通过,只能证明文件是可解析的 plist,不能证明 App Store Connect 会接受其中的键和值。Apple 的 无效隐私清单排障说明也区分了“格式正确”和“隐私清单规则有效”这两个层次。
在 Xcode 属性列表编辑器中,优先核对以下结构:
- 根对象必须是字典。
NSPrivacyAccessedAPITypes必须是字典数组。NSPrivacyAccessedAPIType必须是字符串。NSPrivacyAccessedAPITypeReasons必须是字符串数组。NSPrivacyTracking必须是布尔值,不是字符串。NSPrivacyTrackingDomains必须是字符串数组。NSPrivacyTracking为true时,不能搭配空的追踪域名数组。- 不再使用某类 API 时,应删除对应字典,而不是留下空数组。
- 不要添加 Apple 文档未列出的自定义键。
例如,下面两种写法在 plist 层面可能都能被解析,但后一种类型不符合要求:
<key>NSPrivacyTracking</key>
<true/>
<key>NSPrivacyTracking</key>
<string>true</string>
这就是为什么 plutil 检查通过但 App Store Connect 仍然拒绝:plutil 不会替你判断字符串是否应该是布尔值,也不会验证某个 reason 是否属于对应 API 类别。
可以使用 Xcode 的原始键和值视图检查真实类型,或者用 plutil -p 查看结构:
plutil -p \
"/path/to/PrivacyInfo.xcprivacy"
不要直接在原始 Archive 内保存修改。只要文件发生变化,签名就可能失效。先复制、检查、记录,再决定是在源码中修复,还是在确认边界后处理 Archive。
04 第三步:按真实调用核对 Required Reason API
Required Reason API 的问题不能靠“填一个看起来接近的 reason”解决。你需要把清单中的声明追溯到实际调用来源。
Apple 规定,每个 API 类别都要放入一个字典,字典包含以下 2 个关键键:
NSPrivacyAccessedAPITypeNSPrivacyAccessedAPITypeReasons
API 类别和 reason 必须来自官方允许值,并且 reason 要与实际功能一致。比如,项目只是读取磁盘空间,却不能因为某个 reason 更容易通过,就声明成与用户默认设置或设备启动时间相关的用途。结构细节可参阅 Apple 的 Required Reason API 配置指南。
建议按下面顺序追踪:
- 在主 App 和各个 Target 中搜索相关 API 调用。
- 检查依赖清单,确认调用来自自身代码还是第三方 SDK。
- 对二进制 Framework 使用符号或字符串搜索,记录可能的调用来源。
- 生成 Xcode 隐私报告,观察 API 类别来自哪个 Bundle。
- 将实际调用、Bundle 路径、清单声明和功能说明放在同一份记录中。
- 删除已经不再使用的 API 声明,避免保留过时字典。
如果调用来自第三方 SDK,主 App 清单不能代替 SDK 自己的声明。Apple 的 Required Reason API 官方说明要求使用相关 API 的第三方代码,在其自身的隐私清单中记录类别和 approved reason。
遇到 Invalid privacy manifest 报错时,应该从哪里开始?
先根据邮件路径定位具体文件,再判断是格式、类型、允许值还是来源问题。如果是主 App 的文件,在源码 Target 中修复并重新归档;如果来自 SDK,优先升级到维护者提供有效隐私清单的版本。只有在临时恢复提交且明确知道签名后果时,才考虑处理 Archive 内文件。
05 第四步:第三方 SDK 先判断来源,再决定升级还是临时处理
第三方 SDK 的安装方式会决定隐私清单应该由谁维护。不要看到文件无效,就直接修改依赖目录里的产物。
Swift Package Manager
检查 Package 的资源声明。隐私清单放在包的源码目录或 Resources 目录后,还需要在 Package.swift 中显式声明为资源。否则文件可能存在于仓库,却没有进入最终 Framework 或 App。
CocoaPods
检查 Pod 版本、构建脚本和最终复制阶段。重点确认:
- 依赖锁文件是否发生变化。
- Pod 的资源是否被复制到正确 Target。
- 是否存在同名但来自不同版本的清单。
- 构建脚本是否覆盖或重新打包了 Framework。
动态 Framework 和 XCFramework
对每个嵌套 Framework 分别检查清单和签名。XCFramework 可能包含多个平台变体,不能只检查当前开发机使用的一个目录。每个支持的平台变体都应有与其 Bundle 结构匹配的清单。
Apple 的 第三方 SDK 要求页面还涉及特定 SDK 的隐私清单和二进制签名。官方名单会更新,重新打包名单中的 SDK 依赖也可能受要求影响,因此应直接对照当前页面,不要依赖旧博客或社群整理。
第三方 SDK 的隐私清单无效时,能不能直接自己改?
优先不要。先升级到维护者提供有效文件的版本,或联系维护者确认其清单和签名状态。手改依赖产物只能作为临时措施,而且会破坏 Archive 的代码签名。
如果业务必须临时提交,修改或删除 Archive 内的文件后,必须重新签名。重新签名还要覆盖嵌套 Framework、Extension、App Bundle 及其嵌套代码,不能只对最外层 App 执行一次简单签名。更稳妥的做法是回到源码或依赖版本修复,再创建全新的 Archive。
06 第五步:用隐私报告和 Bundle 位置做交叉验证
源码检查完成后,仍要回到最终产物。Xcode 可以基于 Archive 聚合 App 和第三方 SDK 的隐私清单,生成隐私报告。你可以在 Organizer 中选择 Archive,再生成隐私报告,用它确认各个 SDK 和 Target 的声明是否实际进入产物。具体可参考 Apple 的隐私报告说明。
建议在上传前逐项勾选:
- ✅ 主 App 的
PrivacyInfo.xcprivacy位于正确 Bundle 位置。 - ✅ 每个 Extension 都检查了自己的 Bundle。
- ✅ App Clip 没有漏掉独立清单。
- ✅ Framework 内的清单没有停留在源码目录。
- ✅ Swift Package 的资源已在包配置中声明。
- ✅ macOS 或 Mac Catalyst Bundle 使用了
Contents/Resources/路径。 - ✅ 隐私报告包含预期的 App 和 SDK。
- ✅ 邮件中的错误路径已经在新 Archive 中消失。
- ✅ 依赖锁文件与构建日志已保存。
如果源码里有文件,但 Archive 里找不到,优先检查 Target Membership、Copy Bundle Resources、Package 资源声明和构建脚本。
如果 Archive 里有多份文件,但邮件指向嵌套 Framework,说明主 App 的清单并不是责任文件。
07 最后一步:重新 Archive、验证签名,再做真实上传
修复后不要重复上传旧 Archive。旧产物可能仍然包含错误文件、旧依赖或已经失效的签名。
建议执行完整验收流程:
- 清理或隔离旧的构建目录,避免沿用错误的中间产物。
- 使用锁定的依赖版本重新解析和构建。
- 选择正确的发布 Scheme,创建全新的 Archive。
- 在新 Archive 中重新搜索所有
PrivacyInfo.xcprivacy。 - 重新生成隐私报告并保存。
- 使用 Xcode Organizer 的 Validate App 做初步验证。
- 确认修改过 Archive 内文件时已经完成重签名。
- 进行一次真实上传,等待 App Store Connect 完成处理。
- 保存上传结果、处理日志和对应 Archive 的唯一记录。
“本地校验成功”“Archive 完整”“签名有效”“上传被接收”“后台处理完成”是 5 个不同状态。任何一个状态没有完成,都不能把问题标记为已解决。完整的归档和分发步骤可查看 Xcode App 分发文档。
🔍 经验:如果你在 Archive 内删除或修改
PrivacyInfo.xcprivacy,不要把它当作普通文本修复。这类操作会破坏归档 App 的签名;通过 Organizer 分发时,只有在满足重新签名条件的情况下,Xcode 才能生成可继续验证的产物。
08 按条件选择修复路径
你可以用下面的分支快速决定下一步,而不是所有错误都采用“新增清单”这一种处理方式:
- 若邮件路径指向主 App,且文件由你的代码使用 Required Reason API:在主 App Target 中修复声明,重新 Archive。
- 若邮件路径指向 Extension 或 App Clip:单独检查该 Target 的资源和 Bundle,不要只修改主 App。
- 若路径指向 Swift Package 或 Framework:检查资源是否进入最终 Bundle,并优先升级依赖。
- 若清单来自官方要求名单中的二进制 SDK:同时检查隐私清单和 SDK 签名,不要只改 plist。
- 若
plutil -lint失败:先修复 XML 或 plist 结构,再继续检查业务键值。 - 若
plutil -lint通过但上传仍失败:转向允许键、值类型、approved reason、空数组和 Bundle 路径。 - 若必须临时修改 Archive:只在明确重签名、验证和回滚方案后执行,后续仍应替换为维护者提供的正式依赖版本。
- 若新 Archive 本地通过但后台仍拒绝:保存邮件中的新路径,确认上传的确实是新产物,而不是旧 Archive。
完成这套路径后,你处理的就不再只是一个上传报错,而是一条可复现的发布证据链:错误邮件对应具体 Bundle,Bundle 对应实际文件,文件对应真实调用,最终产物又经过签名和真实上传验证。
如果你只是偶尔发布一次小型 App,本地 Mac 足够完成修复;但如果你需要反复切换依赖、保留多个 Archive、运行 CI 并保存完整上传日志,临时使用一台可持续保留构建证据的远程 Mac 会更合适。相比在 Windows 或 Linux 环境间反复切换,当前方案的缺点通常是无法直接运行 Xcode、Archive 不易复现、签名环境分散,且错误产物很难长期保留。你可以先查看 CALMVPS 的远程 Mac 使用入口,再根据构建频率和是否需要常驻发布环境,评估 Mac 远程租赁方案。