☰
Flutter iOS上架被拒?ITMS-91053隐私清单报错全解析与修复指南
2026/10/26 13:10:42 网站建设 项目流程

1. 这个报错到底在拦什么:ITMS-91053 的真实含义

如果你正在把 Flutter 应用往应用商店提交,某天上传完成后收到一封邮件,标题里赫然写着ITMS-91053: Missing API declaration,那你来对地方了。这个报错跟 Flutter 代码本身几乎没关系,它卡的是**隐私清单(Privacy Manifest)**这一层。从 2024 年春季开始,应用商店对上架应用的隐私合规要求收紧,凡是使用了某些"可能被用于指纹识别"的 API,就必须在工程里放一份声明文件,说明你为什么用、怎么用。没放,或者放得不对,上传阶段就会被拦下来。

先把结论摆前面:ITMS-91053 的本质是"你调用了被列入清单的 API,但没有提交对应的用途声明"。它和签名、证书、包体大小、Flutter 版本都无关,很多人第一反应去查 Xcode 版本、重签证书、清 DerivedData,折腾一整天,方向完全错了。真正要动的是 iOS 工程目录下的PrivacyInfo.xcprivacy文件,以及它背后对应的依赖链。

这个问题的麻烦之处在于,Flutter 项目的 iOS 侧是一个"套娃"结构:Dart 代码编译成 App.framework,插件以 Pod 形式引入,每个 Pod 又可能带自己的隐私清单。报错邮件里通常会列出具体的 API 类别,比如NSPrivacyAccessedAPICategoryUserDefaults、NSPrivacyAccessedAPICategoryFileTimestamp、NSPrivacyAccessedAPICategorySystemBootTime、NSPrivacyAccessedAPICategoryDiskSpace。你要做的是把这些类别逐一对应到"谁在调用",然后在正确的层级补上声明。

适合谁来读这篇:正在用 Flutter 做跨平台应用、准备或正在提交 iOS 版本的开发者;被这封邮件反复打回、不知道从哪下手的人;以及想提前把合规做在提交之前、避免临门一脚被卡的团队。下面我按"先定位、再补声明、后验证"的真实操作顺序讲,中间穿插我踩过的坑。

2. 先搞清楚是谁在调用:定位 API 来源的完整链路

2.1 报错邮件里的类别信息怎么读

上传被拒后,开发者后台会收到一封邮件,正文里有一段类似这样的描述:Your app's code references one or more APIs that access sensitive user data... NSPrivacyAccessedAPICategoryUserDefaults。这段信息量其实很大,它直接告诉了你缺的是哪一类声明。很多人扫一眼就关掉,然后去网上搜"ITMS-91053 解决",拿到一份通用模板往工程里一塞,结果下次提交换了个类别继续被拒。

正确的读法是:把邮件里出现的所有NSPrivacyAccessedAPICategory*类别全部抄下来,做成一张清单。常见的有四类:UserDefaults、FileTimestamp、SystemBootTime、DiskSpace。每一类对应一组具体的系统 API,比如 UserDefaults 类别对应的是NSUserDefaults相关调用,FileTimestamp 对应的是获取文件创建/修改时间的接口。你抄下来的这张清单,就是后面补声明的"待办列表",一个都不能漏。

注意:邮件可能一次只报一类,也可能一次报多类。别以为修完第一封就万事大吉,补完提交后很可能收到第二封报另一类。我的建议是一次性把四类都排查一遍,别跟审核玩回合制。

2.2 Flutter 项目里 API 调用的三个来源

定位"谁在调用"是整件事最费时的环节,因为 Flutter 项目的调用来源有三层,得逐层排查。

第一层是你自己的 Dart 代码经插件间接调用。绝大多数 Flutter 应用不会直接写 Objective-C/Swift 调NSUserDefaults,但用的插件会。比如本地存储类插件、统计类插件、推送类插件,底层几乎都会碰 UserDefaults。第二层是第三方 Pod 依赖。你在Podfile里引入的每个 pod,编译进产物后都可能带 API 调用。第三层是Flutter 引擎自身。Flutter 的 iOS 引擎在某些版本里也会引用被列入清单的 API,这部分你改不了,只能靠升级 Flutter 版本或依赖官方提供的隐私清单。

排查顺序建议从"最可能"到"最不可能":先看插件,再看 Pod,最后考虑引擎。因为引擎层面的问题通常有官方公告,而插件层面是你自己能动手的。

2.3 用工具把调用链挖出来

纯靠肉眼看Podfile.lock是挖不出 API 调用的,得用点手段。我常用的做法是两步:

第一步,在 iOS 工程目录下对编译产物做符号扫描。Xcode 构建完成后,在build/ios/下能找到各个 framework,用nm或strings配合 grep 快速筛:

# 在 iOS 构建产物目录下扫描 UserDefaults 相关符号 find . -name "*.framework" -type d | while read fw; do binary="$fw/$(basename "$fw" .framework)" if [ -f "$binary" ]; then if strings "$binary" | grep -q "NSUserDefaults"; then echo "命中: $fw" fi fi done

这段脚本会把所有引用了NSUserDefaults的 framework 列出来,命中结果基本就是"嫌疑人名单"。把NSUserDefaults换成NSFileCreationDate、systemUptime、NSFileSystemFreeSize就能分别排查另外三类。

第二步,对命中的 framework 反查它属于哪个 Pod。framework 的名字通常和 pod 名一致或高度相关,对照Podfile.lock里的PODS:段落就能确认。如果某个 pod 是间接依赖(被别的 pod 引入),Podfile.lock的DEPENDENCIES和SPEC REPOS部分能帮你理清层级。

提示:strings扫描是启发式的,可能漏也可能误报。它给的是方向,不是铁证。最终确认还是要结合插件文档和源码。

2.4 一个容易忽略的细节:Debug 与 Release 产物不同

我第一次排查时就栽在这上面。用 Debug 构建扫出来的命中列表,和真正上传到商店的 Release 产物并不完全一致——某些只在 Debug 下启用的调试插件、日志库,在 Release 里被裁掉了;反过来,某些 Release 才启用的优化路径又可能引入新的调用。所以扫描一定要针对 Release 归档产物做,也就是flutter build ipa之后生成的.xcarchive里的内容,而不是模拟器或 Debug 包。这个细节网上很少有人提,但它直接决定了你补的声明是否覆盖真实提交的二进制。

3. 补声明这件事,位置比内容更容易错

3.1 PrivacyInfo.xcprivacy 该放在哪一层

定位到调用方之后,下一步是补声明。这里有个关键认知:隐私清单是分层生效的。你自己的主工程需要一份,每个引入的 Pod 理论上也应该有自己的一份。如果某个第三方 Pod 没带清单,你可以在主工程的清单里"代为声明",但更规范的做法是给缺失的 Pod 补一份,或者升级到带清单的版本。

主工程清单的位置在ios/Runner/PrivacyInfo.xcprivacy,并且必须确保它被加入了 Xcode 工程的 Copy Bundle Resources,否则打包时不会进产物。这一步是新手最容易漏的:文件建了、内容写了,但没加进 target,上传照样被拒,然后一脸懵。检查方法是打开 Xcode,选中 Runner target,看 Build Phases 里的 Copy Bundle Resources 有没有这个文件。

Pod 层面的清单,如果 pod 自带,会在其源码目录里有PrivacyInfo.xcprivacy,CocoaPods 集成时会自动处理。如果某个 pod 版本太老没有清单,优先考虑升级;升不了再考虑在主工程清单里合并声明。

3.2 清单文件的结构与四类 API 的写法

PrivacyInfo.xcprivacy本质是个 plist。最小可用结构包含三块:NSPrivacyTracking(是否用于追踪)、NSPrivacyCollectedDataTypes(收集的数据类型)、NSPrivacyAccessedAPITypes(访问的 API 类型及理由)。ITMS-91053 直接对应的是第三块。

一个覆盖四类 API 的骨架长这样:

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>NSPrivacyTracking</key> <false/> <key>NSPrivacyCollectedDataTypes</key> <array/> <key>NSPrivacyAccessedAPITypes</key> <array> <dict> <key>NSPrivacyAccessedAPIType</key> <string>NSPrivacyAccessedAPICategoryUserDefaults</string> <key>NSPrivacyAccessedAPITypeReasons</key> <array> <string>CA92.1</string> </array> </dict> <dict> <key>NSPrivacyAccessedAPIType</key> <string>NSPrivacyAccessedAPICategoryFileTimestamp</string> <key>NSPrivacyAccessedAPITypeReasons</key> <array> <string>C617.1</string> </array> </dict> <dict> <key>NSPrivacyAccessedAPIType</key> <string>NSPrivacyAccessedAPICategorySystemBootTime</string> <key>NSPrivacyAccessedAPITypeReasons</key> <array> <string>35F9.1</string> </array> </dict> <dict> <key>NSPrivacyAccessedAPIType</key> <string>NSPrivacyAccessedAPICategoryDiskSpace</string> <key>NSPrivacyAccessedAPITypeReasons</key> <array> <string>E174.1</string> </array> </dict> </array> </dict> </plist>

里面的CA92.1、C617.1这些是理由代码,不是随便填的。每个类别都有一组官方认可的理由代码,含义各不相同。比如 UserDefaults 类别下,CA92.1表示"仅访问本应用自身写入的偏好设置",这是绝大多数应用的真实情况;如果你是为了在应用组之间共享数据,理由代码就不一样。填错理由代码,轻则被要求补充说明,重则被判定为声明不实。

3.3 理由代码怎么选才不会被二次打回

理由代码的选择逻辑是"如实描述你的用途"。我整理了一张常用对照表,覆盖 Flutter 项目里最常见的场景:

API 类别常见理由代码适用场景
UserDefaultsCA92.1仅读写本应用自己的偏好设置
UserDefaults1C8F.1应用与扩展之间共享数据
FileTimestampC617.1应用内部访问自己容器内文件的时间戳
FileTimestamp3B52.1用户主动授权访问的文件
SystemBootTime35F9.1用于计算应用内事件的相对时间
DiskSpaceE174.1写入前检查可用空间
DiskSpace85F4.1向用户展示存储占用情况

选代码的原则很简单:你实际怎么用,就选最贴近的那个。如果某个类别你其实并不需要(比如扫描发现是某个可移除的插件引入的),更彻底的做法是把这个插件去掉,而不是硬编一个理由。少一个类别,就少一份合规负担。

注意:理由代码是官方定义的固定值,不能自创。填之前务必对照官方文档确认当前有效的代码列表,因为这份列表会随政策更新调整。

3.4 主工程与 Pod 清单冲突时怎么办

实际项目里经常出现这种情况:主工程清单声明了某类别,某个 Pod 自带的清单也声明了同一类别,两者理由代码还不一样。打包时会不会冲突?答案是不会报错,但会合并。系统会把所有清单里的声明汇总,只要有一处声明了某类别,该类别的调用就被视为已声明。所以理论上你可以在主工程里"兜底"声明所有类别,覆盖 Pod 的缺失。

但我不建议无脑兜底。原因有二:一是声明了却实际没用到,属于过度声明,虽然一般不直接拒,但审核问询时你得解释得清;二是如果某个 Pod 未来升级后不再调用某 API,你的兜底声明就成了"僵尸声明",增加维护成本。更稳妥的策略是:Pod 有清单的信任 Pod,Pod 没清单的才在主工程补,并且定期清理不再需要的声明。

4. 从被拒到通过:一次完整的修复与验证流程

4.1 修复前的基线确认

动手之前先做一件事:确认当前工程里到底有没有PrivacyInfo.xcprivacy。有些 Flutter 模板或插件在初始化时已经悄悄放了一份,只是内容不全。用命令快速查:

# 查找工程内所有隐私清单文件 find ios -name "PrivacyInfo.xcprivacy" -print

如果主工程没有,就新建;如果有,打开看它声明了哪些类别、理由代码是什么,和你的待办清单比对,缺什么补什么。这一步能避免重复劳动,也能防止"以为没有其实有、结果建了两份"的混乱。

同时确认 Flutter 版本。较新的 Flutter 稳定版在引擎层面已经处理了部分隐私清单问题,升级有时能直接消掉引擎相关的报错。查版本用flutter --version,如果版本偏旧,评估一下升级成本——升级可能带来其他兼容问题,不是无脑升就完事。

4.2 逐项补齐与工程配置

按第 2 步得到的"嫌疑人名单",逐个处理:

  • 对自带清单的 Pod:确认其清单已随 pod 集成进产物,通常无需手动干预,但要验证。
  • 对不带清单的 Pod:优先升级到带清单的版本;升不了则在主工程清单里补对应类别。
  • 对自己 Dart 代码间接引入的调用:在主工程清单补声明,理由代码按实际用途选。
  • 对引擎层面:升级 Flutter 或等待官方修复,这部分不在你掌控内。

补完内容后,务必回到 Xcode 确认PrivacyInfo.xcprivacy在 Copy Bundle Resources 里。我见过不止一个案例:文件内容完全正确,就是没加进 target,反复被拒,最后发现是这一步漏了。加进去之后,Clean 一次再 Archive,确保产物是干净的。

4.3 上传前的本地自检清单

在真正上传之前,做一轮本地自检能省下大量等待审核的时间。我的自检清单如下:

  1. find ios -name "PrivacyInfo.xcprivacy"确认文件存在且位置正确。
  2. 打开 Xcode,Runner target 的 Copy Bundle Resources 里能看到该文件。
  3. 用第 2.3 节的strings脚本对 Release 归档产物再扫一遍,确认所有命中类别都在清单里有对应声明。
  4. 清单 XML 格式合法,用plutil -lint ios/Runner/PrivacyInfo.xcprivacy校验,避免格式错误导致文件被忽略。
  5. 理由代码与官方当前列表核对,没有使用已废弃或自创的代码。

plutil -lint这一步特别值得强调。plist 格式对缩进、标签闭合很敏感,手写时一个标签没闭合,整个文件可能被静默忽略,然后你带着一份"看起来有其实无效"的清单去提交,继续被拒。花十秒跑个 lint,比等一天审核划算得多。

4.4 提交后仍被拒的排查思路

如果补完声明、自检通过,提交后还是收到 ITMS-91053,别慌,按这个顺序排查:

先看新邮件报的类别是不是和上次不同。如果是新类别,说明你上次只修了一部分,回到第 2 步补漏。如果类别相同,说明声明没生效,重点查三件事:文件是否真的进了产物(解压 ipa 看Payload/App.app/PrivacyInfo.xcprivacy在不在)、XML 是否合法、理由代码是否被认可。解压 ipa 验证是最硬的证据,能直接排除"以为加了其实没加"的情况。

还有一种情况是某个 Pod 在链接时把调用带进来了,但它的清单没被正确合并。这时可以尝试在主工程清单里显式补上该类别作为兜底,再提交验证。兜底不是长久之计,但能帮你快速判断问题是否出在 Pod 清单合并环节。

5. 几个只有踩过才知道的坑

5.1 插件升级反而引入新问题

有次我为了消掉某个类别的报错,把一个本地存储插件升到了最新版,结果新版插件引入了新的 API 调用,报错类别从一类变成了两类。这事的教训是:升级插件前先看它的更新日志和隐私清单变化,别默认"新版一定更合规"。有时候锁在一个带完整清单的旧版本,比追新更省心。升级后一定要重新跑一遍扫描,确认没有引入新的命中。

5.2 清单文件被 Git 忽略或未提交

团队协作时遇到过:某同学本地建了清单、本地打包通过,但文件没提交到仓库(被.gitignore里的规则误伤,或者干脆忘了 add),CI 上打包出来的产物没有清单,提交后被拒。排查了半天以为是内容问题,最后发现是文件根本没进版本库。所以清单文件建好后第一件事是确认git status里能看到它,并且 CI 的打包流程确实包含了它。

5.3 理由代码填得"太诚实"也会出问题

听起来反直觉,但确实存在:有人为了保险,把某个类别的所有理由代码都列上,想着"总有一个对"。结果审核认为声明与实际用途不符,要求说明。理由代码的原则是精准而非全面,一个类别通常只需要一个最贴近实际用途的代码。多填不等于安全,反而增加解释成本。

5.4 别忘了 Android 侧的类似合规

虽然这篇讲的是 iOS 的 ITMS-91053,但隐私合规是跨平台的事。Flutter 项目在补 iOS 清单的同时,顺手检查一下 Android 侧的隐私政策、权限声明是否同步更新,能避免"iOS 过了、Android 被卡"的尴尬。两端的合规节奏最好对齐,别顾此失彼。

6. 把它变成流程而不是救火

被 ITMS-91053 卡过一次之后,我的做法是把它前置到开发流程里,而不是等提交时救火。具体来说:在项目初始化阶段就把PrivacyInfo.xcprivacy建好并加入 target,作为模板的一部分;每次新增或升级插件时,跑一遍第 2.3 节的扫描脚本,把新引入的 API 类别记录到项目文档里;发版前的检查清单里固定加上隐私清单核对项。这样做的成本很低,但能把"提交被拒、等邮件、改、再提交"这个可能耗时数天的循环,压缩成发版前十分钟的例行检查。

说到底,ITMS-91053 不是一个技术难题,它是一个流程问题。API 调用本身没错,错的是没有及时、准确地声明。把定位、声明、验证这三步固化成习惯,这类报错就再也不会成为上架路上的拦路虎。我个人在实际操作中的体会是:与其每次被拒后临时抱佛脚,不如第一次就把四类 API 全排查一遍、把清单做扎实,后面所有版本都省心。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询