Flutter iOS上架被拒?ITMS-91053隐私清单报错全解析与修复指南 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 apps 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.xcprivacyCocoaPods 集成时会自动处理。如果某个 pod 版本太老没有清单优先考虑升级升不了再考虑在主工程清单里合并声明。3.2 清单文件的结构与四类 API 的写法PrivacyInfo.xcprivacy本质是个 plist。最小可用结构包含三块NSPrivacyTracking是否用于追踪、NSPrivacyCollectedDataTypes收集的数据类型、NSPrivacyAccessedAPITypes访问的 API 类型及理由。ITMS-91053 直接对应的是第三块。一个覆盖四类 API 的骨架长这样?xml version1.0 encodingUTF-8? !DOCTYPE plist PUBLIC -//Apple//DTD PLIST 1.0//EN http://www.apple.com/DTDs/PropertyList-1.0.dtd plist version1.0 dict keyNSPrivacyTracking/key false/ keyNSPrivacyCollectedDataTypes/key array/ keyNSPrivacyAccessedAPITypes/key array dict keyNSPrivacyAccessedAPIType/key stringNSPrivacyAccessedAPICategoryUserDefaults/string keyNSPrivacyAccessedAPITypeReasons/key array stringCA92.1/string /array /dict dict keyNSPrivacyAccessedAPIType/key stringNSPrivacyAccessedAPICategoryFileTimestamp/string keyNSPrivacyAccessedAPITypeReasons/key array stringC617.1/string /array /dict dict keyNSPrivacyAccessedAPIType/key stringNSPrivacyAccessedAPICategorySystemBootTime/string keyNSPrivacyAccessedAPITypeReasons/key array string35F9.1/string /array /dict dict keyNSPrivacyAccessedAPIType/key stringNSPrivacyAccessedAPICategoryDiskSpace/string keyNSPrivacyAccessedAPITypeReasons/key array stringE174.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 有清单的信任 PodPod 没清单的才在主工程补并且定期清理不再需要的声明。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 上传前的本地自检清单在真正上传之前做一轮本地自检能省下大量等待审核的时间。我的自检清单如下find ios -name PrivacyInfo.xcprivacy确认文件存在且位置正确。打开 XcodeRunner target 的 Copy Bundle Resources 里能看到该文件。用第 2.3 节的strings脚本对 Release 归档产物再扫一遍确认所有命中类别都在清单里有对应声明。清单 XML 格式合法用plutil -lint ios/Runner/PrivacyInfo.xcprivacy校验避免格式错误导致文件被忽略。理由代码与官方当前列表核对没有使用已废弃或自创的代码。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里的规则误伤或者干脆忘了 addCI 上打包出来的产物没有清单提交后被拒。排查了半天以为是内容问题最后发现是文件根本没进版本库。所以清单文件建好后第一件事是确认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 全排查一遍、把清单做扎实后面所有版本都省心。