App Store Connect 隐私清单无效:2026 怎么修?

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.xcprivacyxcarchive 中的位置,取决于它属于哪一个 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 必须是字符串数组。
  • NSPrivacyTrackingtrue 时,不能搭配空的追踪域名数组。
  • 不再使用某类 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 个关键键

  • NSPrivacyAccessedAPIType
  • NSPrivacyAccessedAPITypeReasons

API 类别和 reason 必须来自官方允许值,并且 reason 要与实际功能一致。比如,项目只是读取磁盘空间,却不能因为某个 reason 更容易通过,就声明成与用户默认设置或设备启动时间相关的用途。结构细节可参阅 Apple 的 Required Reason API 配置指南

建议按下面顺序追踪:

  1. 在主 App 和各个 Target 中搜索相关 API 调用。
  2. 检查依赖清单,确认调用来自自身代码还是第三方 SDK。
  3. 对二进制 Framework 使用符号或字符串搜索,记录可能的调用来源。
  4. 生成 Xcode 隐私报告,观察 API 类别来自哪个 Bundle。
  5. 将实际调用、Bundle 路径、清单声明和功能说明放在同一份记录中。
  6. 删除已经不再使用的 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。旧产物可能仍然包含错误文件、旧依赖或已经失效的签名。

建议执行完整验收流程:

  1. 清理或隔离旧的构建目录,避免沿用错误的中间产物。
  2. 使用锁定的依赖版本重新解析和构建。
  3. 选择正确的发布 Scheme,创建全新的 Archive。
  4. 在新 Archive 中重新搜索所有 PrivacyInfo.xcprivacy
  5. 重新生成隐私报告并保存。
  6. 使用 Xcode Organizer 的 Validate App 做初步验证。
  7. 确认修改过 Archive 内文件时已经完成重签名。
  8. 进行一次真实上传,等待 App Store Connect 完成处理。
  9. 保存上传结果、处理日志和对应 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 远程租赁方案