iOS17 隐私协议适配详解距离 iOS 17 正式推送已经过去几个月了,我陆续处理了几个项目的隐私协议适配工作,也看了不少群里开发者的吐槽。说实话,很多团队对“隐私协议适配”这件事的理解还停留在“在 Info.plist 里加几段权限描述文案”的阶段,结果等到提审被拒、或者上线后收到用户投诉“为什么总是弹窗”才反应过来。这篇文章我想从实际踩坑的角度,把 iOS 17 里涉及到隐私协议适配的几块核心改动讲清楚,包括系统级的权限交互变化、图片选择器、位置授权、后台联网通知,以及 2024 年之后所有上架应用都需要提供的隐私清单文件。内容会尽量贴合真实开发场景,代码和配置文件都是可以直接拿回去用的。先说明一下这篇文章适合谁看:如果你正在维护一个已经上线多年的老 App,还没做过 iOS 17 适配,那你会遇到的大部分问题都在这里;如果你是刚接触 iOS 开发的新人,想搞明白权限弹窗背后到底发生了什么,这篇文章也能帮你建立一个完整的认知框架。我会尽量把“为什么系统要这么做”“为什么适配要这么写”讲明白,而不是只丢一堆 API 文档。1. iOS 17 隐私体系改了什么:先看清全局再动手1.1 这次不是单纯的“加权限文案”,而是完整闭环很多开发者第一次感知到 iOS 17 隐私变化,是在 Xcode 15 升级后第一次跑项目时发现系统弹窗变多了,或者提交 App Store 审核时收到一条关于“隐私清单”的警告。但如果你仔细看苹果在 WWDC 2023 上的内容,就会发现这次改动是一个完整的闭环:从权限申请、权限使用、权限透明度到权限管理,系统都介入了。具体来说,iOS 17 里跟开发者直接相关的隐私改动包括这么几个方向:第一,照片权限的交互逻辑改变了。iOS 17 之前,用户第一次访问系统相册时,弹窗会直接给出“允许访问所有照片”的选项。iOS 17 里系统更倾向于引导用户使用“选择照片”或者“只允许部分访问”,在一些场景下甚至不会弹窗,而是弹出系统级的“图片选择器”。很多老项目还在用 UIImagePickerController 或者直接请求照片权限,这部分必须改。第二,位置服务新增了“减量授权”模式。用户在授权弹窗里除了可以选“使用 App 期间”“使用 App 期间(精确)”“不允许”之外,还可以选择关闭“精确位置”。注意,位置权限弹窗在默认展示时,精确位置开关是一个可选项,很多用户会下意识地关掉它。如果你的 App 对定位精度有强依赖,比如地图导航类,这部分适配不好会直接影响核心功能。第三,后台联网权限通知。iOS 17 在部分场景下会对 App 在后台发起网络请求的行为进行弹窗提示,系统会告诉用户“某某 App 可能正在后台连接互联网”,并让用户选择是否继续允许。这个弹窗的出现逻辑虽然不完全由开发者控制,但与你的网络发起方式、BGTask 使用方式都有关系。第四,隐私清单(Privacy Manifest)。这是最需要重视的一项,苹果要求从 2024 年春季开始,新提交的应用和更新版本如果包含用户隐私数据的收集行为,或者使用了“必需原因 API”(Required Reason API),就必须在项目里包含一份名为 PrivacyInfo.xcprivacy 的清单文件。这个不是可选项,而是上架要求。1.2 适配策略:先静态后动态,先自查后整改我的经验是,iOS 17 隐私适配这项工作不能零散地做,最好按照“静态自查 - 动态适配 - 联调验证”的顺序推进。静态自查指的是梳理项目里所有涉及权限的代码。建议直接全文搜索几个关键词:UIImagePickerController、PHPhotoLibrary、CLLocationManager、UIApplication backgroundTasks、NSURLSession,还有 Info.plist 里的所有 UsageDescription。先搞清楚项目里到底用了哪些权限,才能知道后面要改什么。动态适配指的是针对上面梳理出来的每一项,逐一选择合适的 iOS 17 方案。比如图片选择器改 PHPicker,定位流程加精确位置检测,后台联网逻辑加用户提示等。这里要说的一点是,适配不是简单的把 API 换掉,而是要结合产品形态来做决策。同样是获取照片,如果你的 App 只是让用户上传一张头像,那完全不需要请求相册权限,用 PHPicker 就够了;但如果你要做的是一个相册管理工具,那 PHPicker 就不够用,还是要走相册权限,但要适配 limited 授权状态。联调验证这一步最容易忽略,但恰恰是最重要的。苹果的权限弹窗、授权状态变化,在模拟器和真机上的表现是有差异的,尤其是位置服务里的减量授权,只有真机才能完整测出来。我建议适配完成后准备一台 iOS 17 的真机,把授权状态的各种组合都走一遍:未授权、拒绝、仅精确、仅模糊、后台定位等等。把这些状态下的界面表现、功能表现都截图留档,既能作为开发自测依据,将来提审被问到时也有证据可以回复。2. 相册权限与图片选择器:最普遍的适配场景2.1 为什么 iOS 17 强烈推荐 PHPicker在 iOS 17 之前,很多 App 获取照片的路径是这样的:先请求 NSPhotoLibraryUsageDescription 权限,用户点击允许后,再调起系统相册界面或者自己写一个相册列表页面,用 PHAsset 去读取图片数据。这套流程有一个天然的体验问题:用户一旦拒绝授权,App 就什么都干不了。而且从数据隐私的角度讲,App 其实拿到了整个相册的访问权,即使你只想要一张头像。PHPicker(PHPickerViewController,iOS 14 引入)从根本上改变了这个逻辑。它运行在独立的进程里,App 无法直接访问用户的完整相册,只能拿到用户明确选中的图片资源。最重要的是:PHPicker 不需要申请相册权限,也就不会触发权限弹窗。用户在 PHPicker 里选择完图片后,App 得到一个临时访问凭证,只能读取选中的内容。这种“按需授权”的模式正是苹果想推的,iOS 17 在系统设置里也对相册权限做了更细的分类,目的就是让开发者少拿权限、用户少弹窗。我当时接手的一个项目就是活生生的例子。那个 App 是一款二手交易应用,用户需要上传商品图片。老版本里,开发者在 App 启动后的新手引导页就要求用户授权相册,因为后面要上传图片。iOS 17 用户对这个弹窗的反感度明显提升,很多用户直接拒绝,导致后面连“选择图片”这个功能都进不去。后来我们换成了 PHPicker,App 启动时的权限弹窗直接移除了,用户在需要上传图片时才调起选择器,整个流程的转化率反而提高了。2.2 PHPicker 的完整接入流程与关键参数PHPicker 的接入不算复杂,但有几个细节容易踩坑,我把关键代码贴出来,然后逐个讲解。第一步,在需要调起选择器的地方创建配置并弹出:import PhotosUI var config = PHPickerConfiguration() config.selectionLimit = 9 config.filter = .images config.preferredAssetRepresentationMode = .current let picker = PHPickerViewController(configuration: config) picker.delegate = self present(picker, animated: true)这里有几个参数需要注意。selectionLimit 是选择数量的上限,如果业务上只需要一张图,就设为 1。很多人忽略的是,这个参数在 PHPicker 里是支持用户多选的,如果你不想让用户选多张,必须显式设置。filter 用来限定内容类型,.images 表示只看图片,.videos 表示只看视频,也可以传 nil 表示全部内容。如果你同时还要支持 Live Photo,这里需要用 .images 配合 PHPickerResult 里的资源类型判断,因为 Live Photo 本质上是图片和视频的复合体。preferredAssetRepresentationMode 是 iOS 15 才新增的参数,它决定了你希望拿到的资源格式是原始的、兼容的还是当前的。如果你的业务需要高质量原图,建议设置为 .current 或者 .original,避免拿到被转码后的图。上传头像之类的场景用 .current 比较稳定。第二步,实现代理方法:extension ViewController: PHPickerViewControllerDelegate { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { dismiss(animated: true) let identifiers = results.compactMap { $0.assetIdentifier } let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil) // 处理 fetchResult 中的 PHAsset } }注意 PHPickerResult 里有一个 assetIdentifier,这个字段在你需要做后续处理(比如保存到相册、上传原图)时非常有用。拿到 identifier 之后通过 PHAsset.fetchAssets 可以获取对应的 PHAsset 对象,进而读取更多元数据。需要注意的一个坑是:PHPicker 返回的结果只在当前这次选择有效,你不能把 assetIdentifier 存起来下次再用,因为用户可能已经撤销了对 App 的访问授权。很多开发者在实现“选择图片后先保存草稿,下次再上传”这类功能时,直接存了 identifier,结果下次运行发现读不到图片,就是因为这一层原因。正确的做法是把选中的图片数据先临时拷贝到 App 自己的沙盒目录,再存草稿。2.3 必须保留系统相册权限的场景怎么处理虽然苹果推荐使用 PHPicker,但总有一些场景绕不开相册权限。比如你做的是一个图片编辑工具,需要读取用户在相册里创建的所有相簿;或者你需要在 App 内实现“保存到相册”的功能,那就要用到 PHPhotoLibrary 的 add 方法,这同样需要用户授权。iOS 17 对相册权限做了更进一步的细化,系统设置里现在会把访问级别分成“无”“仅添加”“选择照片”“完整访问”几种。对应到开发侧,PHPhotoLibrary.requestAuthorization(for: .readWrite) 和 .addOnly 的区别要搞清楚。如果你的产品只是想让用户“保存图片到相册”,建议使用 addOnly 权限,不要请求 readWrite。这样用户在设置里看到的是“仅添加照片”,而不是“访问所有照片”,用户的信任感会强很多。之前有一个反馈数据很能说明问题:我们一个工具类 App 从 readWrite 改为 addOnly 之后,权限通过率提升了将近 20 个百分点。如果你确实需要读取相册内容,那就必须做好 limited 授权状态下的适配。iOS 14 之后,用户可以选择“允许访问部分照片”,这时你的相册列表界面要提供一个入口,引导用户去系统设置里把权限改为“全部”。系统的 PHPhotoLibrary.shared().libraryUpgradeRequired 方法可以帮你判断是否需要展示升级提示。我见过不少 App 在这个状态下直接把相册列表渲染为空,用户完全不知道发生了什么,体验很差。这里再补充一个细节:在 iOS 17 的新交互里,即使用户选择了“选择照片”,在第一次触发 PHPicker 时,系统也可能出现一个类似“访问受限”的说明提示。这个是系统控制的,开发者不需要做额外处理,但如果你同时监听 PHPhotoLibraryChangeObserver,可能会收到一次资源变化的回调,代码逻辑上要做好幂等处理。3. 位置权限适配:最容易被忽视的“减量授权”模式3.1 减量授权到底改了什么iOS 17 在位置权限弹窗里加入了一个“精确位置”开关。用户可以选择保持“约略位置”而不是精确位置。这里需要理解的是,这个开关不是简单的“精度下降”,而是系统层面给 App 返回的定位结果完全变成了“模糊”数据。在开发层面,你需要同时关注两个维度:一个是授权状态(authorizationStatus),决定 App 能否获得地理位置;另一个是精确度授权(accuracyAuthorization),决定 App 拿到的是精确位置还是模糊位置。如果用户关闭了精确位置,CLLocationManager 的 locationManagerDidChangeAuthorization 代理方法会首先被触发,你可以通过 locationManager.accuracyAuthorization 属性拿到一个 CLAccuracyAuthorization 枚举值,它只有两种情况:.fullAccuracy 和 .reducedAccuracy。同时,当你开始定位时,系统返回的 CLLocation 对象的 horizontalAccuracy 值会变得很大,通常是 5000 米左右。也就是说,即使 you 所在的城市没有变,返回的经纬度也会偏差很多。这对地图类、打车类、外卖类应用的影响是致命的,因为这些场景下你根本没法做精确导航和派单。3.2 适配代码与降级策略适配的第一步是在请求权限之前做好判断,别一上来就闷头请求。import CoreLocation class LocationManager: NSObject, CLLocationManagerDelegate { let manager = CLLocationManager() override init() { super.init() manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyBest } func requestLocationAuth() { // 先检查当前精确度状态 if manager.accuracyAuthorization == .reducedAccuracy { // 提示用户去设置开启精确位置 } else { manager.requestWhenInUseAuthorization() } } func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { switch manager.accuracyAuthorization { case .fullAccuracy: break case .reducedAccuracy: // 业务降级处理,并弹出引导 break @unknown default: break } } }这个判断逻辑看着简单,但实际项目中很容易漏掉。不少开发者只判断了 authorizationStatus,忽略了 accuracyAuthorization,结果用户明明点了允许定位,但定位信息返回的是 5000 米的模糊范围,功能却还在走精确流程,最后的体验自然是一塌糊涂。系统在用户关闭精确位置后,CLLocationManager 会返回 horizontalAccuracy 为负数的结果吗?不会,它返回的是一个大数值的 positive accuracy。这里有一个比较可靠的判断方式:当 horizontalAccuracy 大于 1000 米时,就可以认为当前是减量授权状态,UI 上要给出明显的提示。如果你的 App 在“减量授权”状态下依然可以运行,那就要考虑业务上的降级策略。比如一个天气 App,精确位置能定位到你所在的小区,模糊位置只能定位到所在商圈,那也可以接受,只需要在页面上提示一下“已开启省电定位,天气信息可能不太精确”。但如果是叫车类 App,模糊定位会导致司机找不到你,那最好直接弹窗引导用户去系统设置开启精确位置。苹果是允许这种引导的,只要不诱导用户做违反隐私政策的事就没问题。3.3 关于“临时精确位置”的新 APIiOS 17 还新增了一个临时精确位置授权的 API:CLLocationManager 的 requestTemporaryFullAccuracyAuthorization(withPurposeKey:completion:)。这个 API 可以让你在业务关键时刻临时申请一次性的精确位置访问权限。举个例子:用户之前关闭了精确位置,但这次他要用 App 的“导航到停车场”功能。这时你可以通过 requestTemporaryFullAccuracyAuthorization 让系统弹出一个说明弹窗,用户点了允许之后,App 可以在短时间内获得精确位置。用完即止,不会改变用户在系统设置里的默认选择。这个 API 在 iOS 17 的适配中非常实用。它既尊重了用户的默认选择,又满足了特定场景下的高精度需求。使用时要记得在 Info.plist 里配置 NSLocationTemporaryUsageDescriptionDictionary 这个键,里面的每个 purpose key 都要有对应的描述文案,否则调用 API 后系统不会弹窗,回调会直接返回失败。踩坑提醒:这个临时授权弹窗是有使用频率限制的,连续多次调起会失败。我遇到过在测试机上调试时连续点了七八次,后面直接不弹了,重启手机才恢复。所以在产品交互上不要设计成“用户每进一次页面就申请一次”,而是要把这个能力留给真正需要精确位置的场景。4. 后台联网权限通知与隐私清单:上架审核的必考项4.1 后台联网权限是怎么触发的iOS 17 里,部分用户在安装 App 后第一次收到系统弹窗“某某 App 正在后台连接互联网”的时候,第一反应是卸载。开发商第一反应是“我明明没写后台联网代码,为什么系统会弹?”这个弹窗背后的机制,其实是系统在监控 App 在“非前台活跃状态”下的网络连接行为。具体来说,当 App 处于后台、锁屏、或者处于非活跃状态时,如果通过 NSURLSession 发起了网络请求,系统就会记录并在后续弹出提示。这不一定是开发者显式写的后台任务才触发,很多 SDK 在后台做统计上报、推送相关请求、或者及时通讯长连接,都会触发这个机制。要搞清楚自己的 App 是否会被弹窗,最直接的办法是在 iOS 17 真机上跑一次完整流程:打开 App、切后台、等待若干秒观察日志和弹窗。但更根本的做法是在设计和开发阶段就明确:你的 App 里有哪些网络请求可能在后台发起?如果确实存在这些请求,那么适配的重点有两条。第一,确认这些请求是必要且合规的,比如推送服务的 ack 请求、后台下载任务;第二,在 App 内的隐私设置页面里,给用户一个明确的说明和可能的关闭入口。苹果并不是禁止后台联网,而是要求开发者对后台联网行为负责,给用户知情权。4.2 怎么合理规避无意义的弹窗弹窗的核心触发条件是“用户在非预期情况下感知到了 App 在后台联网”。所以规避思路不是去欺骗系统,而是尽可能减少不必要的后台网络行为。一种常见的做法是把所有非关键性上报都合并到 App 进入前台后的第一时间统一发送,而不是像以前那样每隔几秒就发一个埋点。对很多 App 来说,统计上报和日志上报完全可以在进入前台时批量做,根本没必要在后台实时发送。如果你们的业务有强实时性需求,比如即时通信或实时定位上报,那这些行为应该走系统提供的后台任务或推送机制,而不是自己无限循环地在后台跑 NSURLSession。还要注意一个细节:如果你的 App 使用了 BGTaskScheduler 进行后台任务刷新(BGAppRefreshTask 或 BGProcessingTask),iOS 17 对这些任务的审批和运行限制更严格了。系统可能不会按你要求的时间点执行,更不会因为任务没跑完就让你继续在后台联网。适配时要做好任务失败后的状态同步,避免出现“后台任务没执行但 UI 状态已经改了”的问题。4.3 隐私清单:从“建议”变成“上架要求”重点说一下隐私清单。这个文件本质上是一份声明,你需要把 App 内收集了哪些用户数据、使用了哪些 API 的原因写清楚,然后放进你的 App 包内。苹果官方提供了一份需要声明的“必需原因 API”清单,涉及到文件时间戳、系统启动信息、用户默认设置、磁盘空间等 API。如果项目里使用了一些容易触发隐私声明的第三方 SDK,比如崩溃统计、用户行为分析、广告聚合,这些 SDK 都可能在你的二进制文件里引入需要声明的 API。你需要在项目里搜索并生成一份综合的隐私清单。实际操作中,很多第三方 SDK 的最新版本已经自带 PrivacyInfo.xcprivacy,你只需要确认集成的版本是不是最新的。老版本 SDK 大概率没有这个文件,上架时会被警告。创建一个隐私清单的步骤很简单:在 Xcode 15 的文件菜单里选择 New File,搜索 PrivacyInfo.xcprivacy,然后打开编辑,填入 App 收集的数据类型、使用的 API 类型和对应的原因。关键点在于你要知道你到底用了哪些 API。这一点比较麻烦,因为第三方 SDK 内部的调用不会直接暴露给你。我的建议是分三步走:第一步,先把你自己写的代码里涉及必需原因 API 的地方找出来。搜 UserDefaults、FileManager、mach_absolute_time、systemUptime 这些关键词,逐一记录使用场景。第二步,把项目里所有第三方 SDK 列个清单,去官网查看是否有新版本支持隐私清单,并确认升级后是否还需要在你的主工程里额外声明。第三步,用一个私有静态扫描工具或者找一台安装了 Xcode 15.3 的机器,用官方提供的命令对编译产物进行扫描。你可以通过 Xcode 的 Product > Archive 导出 .xcarchive,然后在终端里使用 grep 搜索相关 key,也能在 build 日志里看到警告。实际扫出来的问题往往比你预期的多,不要慌,逐个处理即可。下面是一个隐私清单文件的基本结构示例:<?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> <dict> <key>NSPrivacyCollectedDataType</key> <string>NSPrivacyCollectedDataTypeUserID</string> <key>NSPrivacyCollectedDataTypeLinked</key> <true/> <key>NSPrivacyCollectedDataTypeTracking</key> <false/> <key>NSPrivacyCollectedDataTypePurposes</key> <array> <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string> </array> </dict> </array> <key>NSPrivacyAccessedAPITypes</key> <array> <dict> <key>NSPrivacyAccessedAPIType</key> <string>NSPrivacyAccessedAPICategoryUserDefaults</string> <key>NSPrivacyAccessedAPITypeReasons</key> <array> <string>CA92.1</string> </array> </dict> </array> </dict> </plist>注意 NSPrivacyAccessedAPITypeReasons 里的代码是不能乱填的,它必须和你的实际调用场景匹配。比如访问 UserDefaults,如果你的 App 需要用 UserDefaults 存登录密钥或用户偏好,可以用 CA92.1;如果只是用来检测 App 是否第一次启动,可能就对应不上这个 reason。填错了苹果在审核时是有可能发起质询的。关于审核被拒的应对策略,我的经验是:如果你的隐私清单描述和你实际上传的二进制行为不一致,苹果的审核系统会通过静态扫描发现并发出 ITMS-91053 警告。这个警告在 2024 年春季之后会直接变成硬性拒绝。处理办法是不要试图在文案上蒙混过关,把代码里实际调用的 API 和用途一条条捋清楚,然后用客观的语言描述。5. 常见问题与排查技巧实录5.1 高频问题速查表这段时间我前前后后处理了几十个适配咨询,把大家遇到最多的问题整理成一个表,字段包括现象、原因、解决思路,方便你在适配的时候对照着查。现象根本原因解决方案用户明明选了图片,但回调里拿不到图片数据PHPicker 的临时授权在后台被系统回收在回调中立即复制图片到沙盒,而不是只保存 identifier相册列表是空的,用户说“我相册里明明有照片”limited 授权状态下未做增量检测使用 PHPhotoLibraryChangeObserver 监听变化并及时刷新定位信息返回经纬度偏差 5 公里用户关闭了精确位置,accuracyAuthorization 为 reduced判断精确度状态,提示引导开启或使用临时精确授权 API用户拒绝定位权限后,点击“重新授权”无效使用了旧版 requestAlwaysAuthorization,系统机制限制引导用户到系统设置页修改,代码无法直接弹第二次系统弹窗进入后台后再回前台,发现网络请求全部失败被系统后台联网限制,请求未完成统一网络层,进入前台后重新发起,并给用户显示重试状态崩溃日志里有 UserDefaults 相关的隐私警告使用了必需原因 API 但未在隐私清单里声明补充 NSPrivacyAccessedAPITypes 配置隐私清单加了 UserDefaults 声明但审核被拒Reason Code 与实际调用场景不匹配检查实际用途,选择正确的 code,必要时移除相关调用第三方 SDK 更新后 App 启动变慢新版本 SDK 增加了隐私数据的采集和同步评估 SDK 功能,关闭不必要的采集项,或者更换 SDK5.2 审核被拒后的排查顺序隐私问题导致的审核被拒,最容易让人慌,因为苹果邮件里通常只给了一个笼统的描述,比如“App 访问了用户敏感数据但未提供准确的使用说明”。我的排查顺序是这样的。先看邮件里的具体附件。苹果在处理隐私类问题时会附带一份报告或者截图,看清楚它是指向了你的主工程代码还是第三方 SDK。如果是指向主工程代码,直接看代码里对应的 API 位置,检查是否在隐私清单声明里遗漏了。如果是指向第三方 SDK,则去官网找新版 SDK,看 Release Notes 里关于隐私清单的说明。再看 Unity 或者其他跨平台引擎生成的工程。如果你用的是跨平台方案,编译产物里可能包含了不止一份隐私清单,而且是在你自己看不见的地方生成的。这种情况下要同时检查原生工程和引擎导出的工程里的 Info.plist,把两份配置对齐。最后看审核报告里的时间戳。有时候审核被拒不是因为你现在的版本有问题,而是你之前测试时留下的旧包被扫描到了。如果你确认当前包没问题,可以在回复审核中附上最新的测试报告和导出日志,把自查结果发过去。5.3 调试权限状态的小技巧权限状态在模拟器里测试是不完整的,尤其是位置授权的减量模式和后台联网弹窗,模拟器基本不会触发。所以我强烈建议适配期准备一台 iOS 17 真机,专门用来做权限状态切换。有几个调试技巧值得分享。第一,在真机上修改权限状态时,不需要每次都卸载重装。可以在系统设置里找到对应 App,进入权限管理页,把各个权限关闭后再打开,实测下来系统状态更新的及时性没问题,App 侧可以通过设置里的“重置隐私权限”或者在 App 内用 UIApplication.openSettingsURLString 引导用户跳转。第二,测试精确位置开关时,要观察的核心指标是 CLLocation 的 horizontalAccuracy,不要只看模拟器地图上的蓝色圆圈,那个不准确。第三,后台联网弹窗的复现,要先把 App 切到后台,然后立刻在日志里看是否有网络请求日志,如果没有日志但弹窗出现了,那么大概率是某个 SDK 发的请求,可以通过关掉各 SDK 的收集开关来逐一定位。6. 写在最后:按这份清单去做,能少走 90% 的弯路这篇文章的最后,我不打算做什么总结性的升华,就想分享一点实际经验。如果你现在才开始做 iOS 17 隐私适配,我建议你给自己留出至少三个工作日,不要想着一天搞定。第一天做静态自查,把项目里所有权限相关代码、第三方 SDK 版本、隐私清单缺失情况全部梳理出来;第二天做动态适配,按照我这篇文章里提到的照片、位置、网络、清单四条线逐项改;第三天用真机做组合场景测试,把各种拒绝、模糊授权、取消授权的状态都过一遍,同时把界面截图留档。另外一个小小的建议是:适配过程中遇到某一行代码不确定时,优先查看苹果官方文档的“Required Reason API”页面,以那个名单为准。网上很多转载的清单已经过时了,比如有的文章还在用 iOS 16 时代的 description 文案列表,那对 iOS 17 是不适用的。最后说一点我的真实感受:隐私适配这项工作看起来繁琐,但其实是 iOS 生态回归用户体验本质的一个信号。做得好,用户的信任感会提升,对产品的长期留存是有很大帮助的。希望这篇内容能帮你扫清适配路上的障碍,也欢迎你在评论区分享你遇到的具体问题,我会尽可能给出有针对性的建议。
iOS17 隐私协议适配详解距离 iOS 17 正式推送已经过去几个月了,我陆续处理了几个项目的隐私协议适配工作,也看了不少群里开发者的吐槽。说实话,很多团队对“隐私协议适配”这件事的理解还停留在“在 Info.plist 里加几段权限描述文案”的阶段,结果等到提审被拒、或者上线后收到用户投诉“为什么总是弹窗”才反应过来。这篇文章我想从实际踩坑的角度,把 iOS 17 里涉及到隐私协议适配的几块核心改动讲清楚,包括系统级的权限交互变化、图片选择器、位置授权、后台联网通知,以及 2024 年之后所有上架应用都需要提供的隐私清单文件。内容会尽量贴合真实开发场景,代码和配置文件都是可以直接拿回去用的。先说明一下这篇文章适合谁看:如果你正在维护一个已经上线多年的老 App,还没做过 iOS 17 适配,那你会遇到的大部分问题都在这里;如果你是刚接触 iOS 开发的新人,想搞明白权限弹窗背后到底发生了什么,这篇文章也能帮你建立一个完整的认知框架。我会尽量把“为什么系统要这么做”“为什么适配要这么写”讲明白,而不是只丢一堆 API 文档。1. iOS 17 隐私体系改了什么:先看清全局再动手1.1 这次不是单纯的“加权限文案”,而是完整闭环很多开发者第一次感知到 iOS 17 隐私变化,是在 Xcode 15 升级后第一次跑项目时发现系统弹窗变多了,或者提交 App Store 审核时收到一条关于“隐私清单”的警告。但如果你仔细看苹果在 WWDC 2023 上的内容,就会发现这次改动是一个完整的闭环:从权限申请、权限使用、权限透明度到权限管理,系统都介入了。具体来说,iOS 17 里跟开发者直接相关的隐私改动包括这么几个方向:第一,照片权限的交互逻辑改变了。iOS 17 之前,用户第一次访问系统相册时,弹窗会直接给出“允许访问所有照片”的选项。iOS 17 里系统更倾向于引导用户使用“选择照片”或者“只允许部分访问”,在一些场景下甚至不会弹窗,而是弹出系统级的“图片选择器”。很多老项目还在用 UIImagePickerController 或者直接请求照片权限,这部分必须改。第二,位置服务新增了“减量授权”模式。用户在授权弹窗里除了可以选“使用 App 期间”“使用 App 期间(精确)”“不允许”之外,还可以选择关闭“精确位置”。注意,位置权限弹窗在默认展示时,精确位置开关是一个可选项,很多用户会下意识地关掉它。如果你的 App 对定位精度有强依赖,比如地图导航类,这部分适配不好会直接影响核心功能。第三,后台联网权限通知。iOS 17 在部分场景下会对 App 在后台发起网络请求的行为进行弹窗提示,系统会告诉用户“某某 App 可能正在后台连接互联网”,并让用户选择是否继续允许。这个弹窗的出现逻辑虽然不完全由开发者控制,但与你的网络发起方式、BGTask 使用方式都有关系。第四,隐私清单(Privacy Manifest)。这是最需要重视的一项,苹果要求从 2024 年春季开始,新提交的应用和更新版本如果包含用户隐私数据的收集行为,或者使用了“必需原因 API”(Required Reason API),就必须在项目里包含一份名为 PrivacyInfo.xcprivacy 的清单文件。这个不是可选项,而是上架要求。1.2 适配策略:先静态后动态,先自查后整改我的经验是,iOS 17 隐私适配这项工作不能零散地做,最好按照“静态自查 - 动态适配 - 联调验证”的顺序推进。静态自查指的是梳理项目里所有涉及权限的代码。建议直接全文搜索几个关键词:UIImagePickerController、PHPhotoLibrary、CLLocationManager、UIApplication backgroundTasks、NSURLSession,还有 Info.plist 里的所有 UsageDescription。先搞清楚项目里到底用了哪些权限,才能知道后面要改什么。动态适配指的是针对上面梳理出来的每一项,逐一选择合适的 iOS 17 方案。比如图片选择器改 PHPicker,定位流程加精确位置检测,后台联网逻辑加用户提示等。这里要说的一点是,适配不是简单的把 API 换掉,而是要结合产品形态来做决策。同样是获取照片,如果你的 App 只是让用户上传一张头像,那完全不需要请求相册权限,用 PHPicker 就够了;但如果你要做的是一个相册管理工具,那 PHPicker 就不够用,还是要走相册权限,但要适配 limited 授权状态。联调验证这一步最容易忽略,但恰恰是最重要的。苹果的权限弹窗、授权状态变化,在模拟器和真机上的表现是有差异的,尤其是位置服务里的减量授权,只有真机才能完整测出来。我建议适配完成后准备一台 iOS 17 的真机,把授权状态的各种组合都走一遍:未授权、拒绝、仅精确、仅模糊、后台定位等等。把这些状态下的界面表现、功能表现都截图留档,既能作为开发自测依据,将来提审被问到时也有证据可以回复。2. 相册权限与图片选择器:最普遍的适配场景2.1 为什么 iOS 17 强烈推荐 PHPicker在 iOS 17 之前,很多 App 获取照片的路径是这样的:先请求 NSPhotoLibraryUsageDescription 权限,用户点击允许后,再调起系统相册界面或者自己写一个相册列表页面,用 PHAsset 去读取图片数据。这套流程有一个天然的体验问题:用户一旦拒绝授权,App 就什么都干不了。而且从数据隐私的角度讲,App 其实拿到了整个相册的访问权,即使你只想要一张头像。PHPicker(PHPickerViewController,iOS 14 引入)从根本上改变了这个逻辑。它运行在独立的进程里,App 无法直接访问用户的完整相册,只能拿到用户明确选中的图片资源。最重要的是:PHPicker 不需要申请相册权限,也就不会触发权限弹窗。用户在 PHPicker 里选择完图片后,App 得到一个临时访问凭证,只能读取选中的内容。这种“按需授权”的模式正是苹果想推的,iOS 17 在系统设置里也对相册权限做了更细的分类,目的就是让开发者少拿权限、用户少弹窗。我当时接手的一个项目就是活生生的例子。那个 App 是一款二手交易应用,用户需要上传商品图片。老版本里,开发者在 App 启动后的新手引导页就要求用户授权相册,因为后面要上传图片。iOS 17 用户对这个弹窗的反感度明显提升,很多用户直接拒绝,导致后面连“选择图片”这个功能都进不去。后来我们换成了 PHPicker,App 启动时的权限弹窗直接移除了,用户在需要上传图片时才调起选择器,整个流程的转化率反而提高了。2.2 PHPicker 的完整接入流程与关键参数PHPicker 的接入不算复杂,但有几个细节容易踩坑,我把关键代码贴出来,然后逐个讲解。第一步,在需要调起选择器的地方创建配置并弹出:import PhotosUI var config = PHPickerConfiguration() config.selectionLimit = 9 config.filter = .images config.preferredAssetRepresentationMode = .current let picker = PHPickerViewController(configuration: config) picker.delegate = self present(picker, animated: true)这里有几个参数需要注意。selectionLimit 是选择数量的上限,如果业务上只需要一张图,就设为 1。很多人忽略的是,这个参数在 PHPicker 里是支持用户多选的,如果你不想让用户选多张,必须显式设置。filter 用来限定内容类型,.images 表示只看图片,.videos 表示只看视频,也可以传 nil 表示全部内容。如果你同时还要支持 Live Photo,这里需要用 .images 配合 PHPickerResult 里的资源类型判断,因为 Live Photo 本质上是图片和视频的复合体。preferredAssetRepresentationMode 是 iOS 15 才新增的参数,它决定了你希望拿到的资源格式是原始的、兼容的还是当前的。如果你的业务需要高质量原图,建议设置为 .current 或者 .original,避免拿到被转码后的图。上传头像之类的场景用 .current 比较稳定。第二步,实现代理方法:extension ViewController: PHPickerViewControllerDelegate { func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { dismiss(animated: true) let identifiers = results.compactMap { $0.assetIdentifier } let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: identifiers, options: nil) // 处理 fetchResult 中的 PHAsset } }注意 PHPickerResult 里有一个 assetIdentifier,这个字段在你需要做后续处理(比如保存到相册、上传原图)时非常有用。拿到 identifier 之后通过 PHAsset.fetchAssets 可以获取对应的 PHAsset 对象,进而读取更多元数据。需要注意的一个坑是:PHPicker 返回的结果只在当前这次选择有效,你不能把 assetIdentifier 存起来下次再用,因为用户可能已经撤销了对 App 的访问授权。很多开发者在实现“选择图片后先保存草稿,下次再上传”这类功能时,直接存了 identifier,结果下次运行发现读不到图片,就是因为这一层原因。正确的做法是把选中的图片数据先临时拷贝到 App 自己的沙盒目录,再存草稿。2.3 必须保留系统相册权限的场景怎么处理虽然苹果推荐使用 PHPicker,但总有一些场景绕不开相册权限。比如你做的是一个图片编辑工具,需要读取用户在相册里创建的所有相簿;或者你需要在 App 内实现“保存到相册”的功能,那就要用到 PHPhotoLibrary 的 add 方法,这同样需要用户授权。iOS 17 对相册权限做了更进一步的细化,系统设置里现在会把访问级别分成“无”“仅添加”“选择照片”“完整访问”几种。对应到开发侧,PHPhotoLibrary.requestAuthorization(for: .readWrite) 和 .addOnly 的区别要搞清楚。如果你的产品只是想让用户“保存图片到相册”,建议使用 addOnly 权限,不要请求 readWrite。这样用户在设置里看到的是“仅添加照片”,而不是“访问所有照片”,用户的信任感会强很多。之前有一个反馈数据很能说明问题:我们一个工具类 App 从 readWrite 改为 addOnly 之后,权限通过率提升了将近 20 个百分点。如果你确实需要读取相册内容,那就必须做好 limited 授权状态下的适配。iOS 14 之后,用户可以选择“允许访问部分照片”,这时你的相册列表界面要提供一个入口,引导用户去系统设置里把权限改为“全部”。系统的 PHPhotoLibrary.shared().libraryUpgradeRequired 方法可以帮你判断是否需要展示升级提示。我见过不少 App 在这个状态下直接把相册列表渲染为空,用户完全不知道发生了什么,体验很差。这里再补充一个细节:在 iOS 17 的新交互里,即使用户选择了“选择照片”,在第一次触发 PHPicker 时,系统也可能出现一个类似“访问受限”的说明提示。这个是系统控制的,开发者不需要做额外处理,但如果你同时监听 PHPhotoLibraryChangeObserver,可能会收到一次资源变化的回调,代码逻辑上要做好幂等处理。3. 位置权限适配:最容易被忽视的“减量授权”模式3.1 减量授权到底改了什么iOS 17 在位置权限弹窗里加入了一个“精确位置”开关。用户可以选择保持“约略位置”而不是精确位置。这里需要理解的是,这个开关不是简单的“精度下降”,而是系统层面给 App 返回的定位结果完全变成了“模糊”数据。在开发层面,你需要同时关注两个维度:一个是授权状态(authorizationStatus),决定 App 能否获得地理位置;另一个是精确度授权(accuracyAuthorization),决定 App 拿到的是精确位置还是模糊位置。如果用户关闭了精确位置,CLLocationManager 的 locationManagerDidChangeAuthorization 代理方法会首先被触发,你可以通过 locationManager.accuracyAuthorization 属性拿到一个 CLAccuracyAuthorization 枚举值,它只有两种情况:.fullAccuracy 和 .reducedAccuracy。同时,当你开始定位时,系统返回的 CLLocation 对象的 horizontalAccuracy 值会变得很大,通常是 5000 米左右。也就是说,即使 you 所在的城市没有变,返回的经纬度也会偏差很多。这对地图类、打车类、外卖类应用的影响是致命的,因为这些场景下你根本没法做精确导航和派单。3.2 适配代码与降级策略适配的第一步是在请求权限之前做好判断,别一上来就闷头请求。import CoreLocation class LocationManager: NSObject, CLLocationManagerDelegate { let manager = CLLocationManager() override init() { super.init() manager.delegate = self manager.desiredAccuracy = kCLLocationAccuracyBest } func requestLocationAuth() { // 先检查当前精确度状态 if manager.accuracyAuthorization == .reducedAccuracy { // 提示用户去设置开启精确位置 } else { manager.requestWhenInUseAuthorization() } } func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { switch manager.accuracyAuthorization { case .fullAccuracy: break case .reducedAccuracy: // 业务降级处理,并弹出引导 break @unknown default: break } } }这个判断逻辑看着简单,但实际项目中很容易漏掉。不少开发者只判断了 authorizationStatus,忽略了 accuracyAuthorization,结果用户明明点了允许定位,但定位信息返回的是 5000 米的模糊范围,功能却还在走精确流程,最后的体验自然是一塌糊涂。系统在用户关闭精确位置后,CLLocationManager 会返回 horizontalAccuracy 为负数的结果吗?不会,它返回的是一个大数值的 positive accuracy。这里有一个比较可靠的判断方式:当 horizontalAccuracy 大于 1000 米时,就可以认为当前是减量授权状态,UI 上要给出明显的提示。如果你的 App 在“减量授权”状态下依然可以运行,那就要考虑业务上的降级策略。比如一个天气 App,精确位置能定位到你所在的小区,模糊位置只能定位到所在商圈,那也可以接受,只需要在页面上提示一下“已开启省电定位,天气信息可能不太精确”。但如果是叫车类 App,模糊定位会导致司机找不到你,那最好直接弹窗引导用户去系统设置开启精确位置。苹果是允许这种引导的,只要不诱导用户做违反隐私政策的事就没问题。3.3 关于“临时精确位置”的新 APIiOS 17 还新增了一个临时精确位置授权的 API:CLLocationManager 的 requestTemporaryFullAccuracyAuthorization(withPurposeKey:completion:)。这个 API 可以让你在业务关键时刻临时申请一次性的精确位置访问权限。举个例子:用户之前关闭了精确位置,但这次他要用 App 的“导航到停车场”功能。这时你可以通过 requestTemporaryFullAccuracyAuthorization 让系统弹出一个说明弹窗,用户点了允许之后,App 可以在短时间内获得精确位置。用完即止,不会改变用户在系统设置里的默认选择。这个 API 在 iOS 17 的适配中非常实用。它既尊重了用户的默认选择,又满足了特定场景下的高精度需求。使用时要记得在 Info.plist 里配置 NSLocationTemporaryUsageDescriptionDictionary 这个键,里面的每个 purpose key 都要有对应的描述文案,否则调用 API 后系统不会弹窗,回调会直接返回失败。踩坑提醒:这个临时授权弹窗是有使用频率限制的,连续多次调起会失败。我遇到过在测试机上调试时连续点了七八次,后面直接不弹了,重启手机才恢复。所以在产品交互上不要设计成“用户每进一次页面就申请一次”,而是要把这个能力留给真正需要精确位置的场景。4. 后台联网权限通知与隐私清单:上架审核的必考项4.1 后台联网权限是怎么触发的iOS 17 里,部分用户在安装 App 后第一次收到系统弹窗“某某 App 正在后台连接互联网”的时候,第一反应是卸载。开发商第一反应是“我明明没写后台联网代码,为什么系统会弹?”这个弹窗背后的机制,其实是系统在监控 App 在“非前台活跃状态”下的网络连接行为。具体来说,当 App 处于后台、锁屏、或者处于非活跃状态时,如果通过 NSURLSession 发起了网络请求,系统就会记录并在后续弹出提示。这不一定是开发者显式写的后台任务才触发,很多 SDK 在后台做统计上报、推送相关请求、或者及时通讯长连接,都会触发这个机制。要搞清楚自己的 App 是否会被弹窗,最直接的办法是在 iOS 17 真机上跑一次完整流程:打开 App、切后台、等待若干秒观察日志和弹窗。但更根本的做法是在设计和开发阶段就明确:你的 App 里有哪些网络请求可能在后台发起?如果确实存在这些请求,那么适配的重点有两条。第一,确认这些请求是必要且合规的,比如推送服务的 ack 请求、后台下载任务;第二,在 App 内的隐私设置页面里,给用户一个明确的说明和可能的关闭入口。苹果并不是禁止后台联网,而是要求开发者对后台联网行为负责,给用户知情权。4.2 怎么合理规避无意义的弹窗弹窗的核心触发条件是“用户在非预期情况下感知到了 App 在后台联网”。所以规避思路不是去欺骗系统,而是尽可能减少不必要的后台网络行为。一种常见的做法是把所有非关键性上报都合并到 App 进入前台后的第一时间统一发送,而不是像以前那样每隔几秒就发一个埋点。对很多 App 来说,统计上报和日志上报完全可以在进入前台时批量做,根本没必要在后台实时发送。如果你们的业务有强实时性需求,比如即时通信或实时定位上报,那这些行为应该走系统提供的后台任务或推送机制,而不是自己无限循环地在后台跑 NSURLSession。还要注意一个细节:如果你的 App 使用了 BGTaskScheduler 进行后台任务刷新(BGAppRefreshTask 或 BGProcessingTask),iOS 17 对这些任务的审批和运行限制更严格了。系统可能不会按你要求的时间点执行,更不会因为任务没跑完就让你继续在后台联网。适配时要做好任务失败后的状态同步,避免出现“后台任务没执行但 UI 状态已经改了”的问题。4.3 隐私清单:从“建议”变成“上架要求”重点说一下隐私清单。这个文件本质上是一份声明,你需要把 App 内收集了哪些用户数据、使用了哪些 API 的原因写清楚,然后放进你的 App 包内。苹果官方提供了一份需要声明的“必需原因 API”清单,涉及到文件时间戳、系统启动信息、用户默认设置、磁盘空间等 API。如果项目里使用了一些容易触发隐私声明的第三方 SDK,比如崩溃统计、用户行为分析、广告聚合,这些 SDK 都可能在你的二进制文件里引入需要声明的 API。你需要在项目里搜索并生成一份综合的隐私清单。实际操作中,很多第三方 SDK 的最新版本已经自带 PrivacyInfo.xcprivacy,你只需要确认集成的版本是不是最新的。老版本 SDK 大概率没有这个文件,上架时会被警告。创建一个隐私清单的步骤很简单:在 Xcode 15 的文件菜单里选择 New File,搜索 PrivacyInfo.xcprivacy,然后打开编辑,填入 App 收集的数据类型、使用的 API 类型和对应的原因。关键点在于你要知道你到底用了哪些 API。这一点比较麻烦,因为第三方 SDK 内部的调用不会直接暴露给你。我的建议是分三步走:第一步,先把你自己写的代码里涉及必需原因 API 的地方找出来。搜 UserDefaults、FileManager、mach_absolute_time、systemUptime 这些关键词,逐一记录使用场景。第二步,把项目里所有第三方 SDK 列个清单,去官网查看是否有新版本支持隐私清单,并确认升级后是否还需要在你的主工程里额外声明。第三步,用一个私有静态扫描工具或者找一台安装了 Xcode 15.3 的机器,用官方提供的命令对编译产物进行扫描。你可以通过 Xcode 的 Product > Archive 导出 .xcarchive,然后在终端里使用 grep 搜索相关 key,也能在 build 日志里看到警告。实际扫出来的问题往往比你预期的多,不要慌,逐个处理即可。下面是一个隐私清单文件的基本结构示例:<?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> <dict> <key>NSPrivacyCollectedDataType</key> <string>NSPrivacyCollectedDataTypeUserID</string> <key>NSPrivacyCollectedDataTypeLinked</key> <true/> <key>NSPrivacyCollectedDataTypeTracking</key> <false/> <key>NSPrivacyCollectedDataTypePurposes</key> <array> <string>NSPrivacyCollectedDataTypePurposeAppFunctionality</string> </array> </dict> </array> <key>NSPrivacyAccessedAPITypes</key> <array> <dict> <key>NSPrivacyAccessedAPIType</key> <string>NSPrivacyAccessedAPICategoryUserDefaults</string> <key>NSPrivacyAccessedAPITypeReasons</key> <array> <string>CA92.1</string> </array> </dict> </array> </dict> </plist>注意 NSPrivacyAccessedAPITypeReasons 里的代码是不能乱填的,它必须和你的实际调用场景匹配。比如访问 UserDefaults,如果你的 App 需要用 UserDefaults 存登录密钥或用户偏好,可以用 CA92.1;如果只是用来检测 App 是否第一次启动,可能就对应不上这个 reason。填错了苹果在审核时是有可能发起质询的。关于审核被拒的应对策略,我的经验是:如果你的隐私清单描述和你实际上传的二进制行为不一致,苹果的审核系统会通过静态扫描发现并发出 ITMS-91053 警告。这个警告在 2024 年春季之后会直接变成硬性拒绝。处理办法是不要试图在文案上蒙混过关,把代码里实际调用的 API 和用途一条条捋清楚,然后用客观的语言描述。5. 常见问题与排查技巧实录5.1 高频问题速查表这段时间我前前后后处理了几十个适配咨询,把大家遇到最多的问题整理成一个表,字段包括现象、原因、解决思路,方便你在适配的时候对照着查。现象根本原因解决方案用户明明选了图片,但回调里拿不到图片数据PHPicker 的临时授权在后台被系统回收在回调中立即复制图片到沙盒,而不是只保存 identifier相册列表是空的,用户说“我相册里明明有照片”limited 授权状态下未做增量检测使用 PHPhotoLibraryChangeObserver 监听变化并及时刷新定位信息返回经纬度偏差 5 公里用户关闭了精确位置,accuracyAuthorization 为 reduced判断精确度状态,提示引导开启或使用临时精确授权 API用户拒绝定位权限后,点击“重新授权”无效使用了旧版 requestAlwaysAuthorization,系统机制限制引导用户到系统设置页修改,代码无法直接弹第二次系统弹窗进入后台后再回前台,发现网络请求全部失败被系统后台联网限制,请求未完成统一网络层,进入前台后重新发起,并给用户显示重试状态崩溃日志里有 UserDefaults 相关的隐私警告使用了必需原因 API 但未在隐私清单里声明补充 NSPrivacyAccessedAPITypes 配置隐私清单加了 UserDefaults 声明但审核被拒Reason Code 与实际调用场景不匹配检查实际用途,选择正确的 code,必要时移除相关调用第三方 SDK 更新后 App 启动变慢新版本 SDK 增加了隐私数据的采集和同步评估 SDK 功能,关闭不必要的采集项,或者更换 SDK5.2 审核被拒后的排查顺序隐私问题导致的审核被拒,最容易让人慌,因为苹果邮件里通常只给了一个笼统的描述,比如“App 访问了用户敏感数据但未提供准确的使用说明”。我的排查顺序是这样的。先看邮件里的具体附件。苹果在处理隐私类问题时会附带一份报告或者截图,看清楚它是指向了你的主工程代码还是第三方 SDK。如果是指向主工程代码,直接看代码里对应的 API 位置,检查是否在隐私清单声明里遗漏了。如果是指向第三方 SDK,则去官网找新版 SDK,看 Release Notes 里关于隐私清单的说明。再看 Unity 或者其他跨平台引擎生成的工程。如果你用的是跨平台方案,编译产物里可能包含了不止一份隐私清单,而且是在你自己看不见的地方生成的。这种情况下要同时检查原生工程和引擎导出的工程里的 Info.plist,把两份配置对齐。最后看审核报告里的时间戳。有时候审核被拒不是因为你现在的版本有问题,而是你之前测试时留下的旧包被扫描到了。如果你确认当前包没问题,可以在回复审核中附上最新的测试报告和导出日志,把自查结果发过去。5.3 调试权限状态的小技巧权限状态在模拟器里测试是不完整的,尤其是位置授权的减量模式和后台联网弹窗,模拟器基本不会触发。所以我强烈建议适配期准备一台 iOS 17 真机,专门用来做权限状态切换。有几个调试技巧值得分享。第一,在真机上修改权限状态时,不需要每次都卸载重装。可以在系统设置里找到对应 App,进入权限管理页,把各个权限关闭后再打开,实测下来系统状态更新的及时性没问题,App 侧可以通过设置里的“重置隐私权限”或者在 App 内用 UIApplication.openSettingsURLString 引导用户跳转。第二,测试精确位置开关时,要观察的核心指标是 CLLocation 的 horizontalAccuracy,不要只看模拟器地图上的蓝色圆圈,那个不准确。第三,后台联网弹窗的复现,要先把 App 切到后台,然后立刻在日志里看是否有网络请求日志,如果没有日志但弹窗出现了,那么大概率是某个 SDK 发的请求,可以通过关掉各 SDK 的收集开关来逐一定位。6. 写在最后:按这份清单去做,能少走 90% 的弯路这篇文章的最后,我不打算做什么总结性的升华,就想分享一点实际经验。如果你现在才开始做 iOS 17 隐私适配,我建议你给自己留出至少三个工作日,不要想着一天搞定。第一天做静态自查,把项目里所有权限相关代码、第三方 SDK 版本、隐私清单缺失情况全部梳理出来;第二天做动态适配,按照我这篇文章里提到的照片、位置、网络、清单四条线逐项改;第三天用真机做组合场景测试,把各种拒绝、模糊授权、取消授权的状态都过一遍,同时把界面截图留档。另外一个小小的建议是:适配过程中遇到某一行代码不确定时,优先查看苹果官方文档的“Required Reason API”页面,以那个名单为准。网上很多转载的清单已经过时了,比如有的文章还在用 iOS 16 时代的 description 文案列表,那对 iOS 17 是不适用的。最后说一点我的真实感受:隐私适配这项工作看起来繁琐,但其实是 iOS 生态回归用户体验本质的一个信号。做得好,用户的信任感会提升,对产品的长期留存是有很大帮助的。希望这篇内容能帮你扫清适配路上的障碍,也欢迎你在评论区分享你遇到的具体问题,我会尽可能给出有针对性的建议。