每当我们安装一款新的 iOS 应用时,总会习惯性地面对一连串系统弹窗:“是否允许访问相册?”、“是否允许使用定位?”、“是否允许跨应用追踪?”。许多用户因此认为,只要拒绝了所有弹窗,App 就对自己一无所知;而不少初入行的 iOS 开发者也常常困惑于系统权限的边界——究竟哪些信息可以直接拿?为什么有些无需用户授权的接口,上传 App Store 时却会收到苹果的警告邮件?
本文将从用户隐私边界与开发者工程实践双重视角出发,彻底厘清 iOS 系统中那些“无需授权即可直接获取”的设备信息,并深入拆解苹果强制推行的 隐私清单(Privacy Manifest / Required Reason API) 机制与提审避坑指南。
一、历史分水岭:App 还能直接获取“设备名称”吗?
在探讨静默信息之前,最典型的莫过于用户的自定义设备名称(例如“张三的 iPhone”)。
在 iOS 15 及更早版本中,任何 App 只要在代码里调用一行简单的 UIDevice.current.name,就能在不触发任何弹窗的情况下,直接拿走用户设置的手机名称。由于很多用户习惯用真实姓名或昵称命名手机,这一接口不仅直接暴露了个人身份隐私,还长期被广告行业用作识别与追踪用户的特征值。
从 iOS 16 开始,苹果彻底封堵了这一路径:
- 默认行为变更: 当普通应用再次调用
UIDevice.current.name时,系统不再返回用户自定义的名称,而是统一返回抹除个性的通用硬件类型字符串——即单纯的"iPhone"或"iPad"。 - 极少数白名单特例: 只有具备强业务合理性的应用(例如跨设备局域网文件互传、多设备管理软件),由开发者向苹果官方提交专门的受限权限申请(
com.apple.developer.device-information.user-assigned-device-name)并通过严格的人工审核后,才能读取真实设备名。即便获批,苹果也在协议中严令禁止将该名称用于广告追踪或画像构建。
二、冰山之下:无需用户授权,App 即可静默读取的 7 类信息
虽然通讯录、相册、精准 GPS 定位、相机、麦克风以及广告标识符(IDFA)被严格关在“弹窗授权”的笼子里,但为了保障应用的基础运行、界面渲染和网络通信,iOS 依然向开发者默认开放了大量基础环境接口。
只要你打开一款 App,它在后台即可瞬间、静默地读取以下 7 大类数据,且全程不会弹出任何确认框:
| 信息类别 | App 可直接获取的具体内容 | 正常业务用途 | 潜在的隐私与追踪风险 |
|---|---|---|---|
| 1. 硬件与系统参数 | 具体硬件型号代码(如 iPhone16,2 对应 iPhone 15 Pro Max)、iOS 系统版本号、CPU 核心数与架构、物理内存(RAM)总大小、屏幕分辨率与缩放比例、当前屏幕亮度 | 针对不同机型做 UI 屏幕适配、评估设备性能以调整渲染画质 | 硬件型号与系统版本的细分组合,是勾勒用户消费能力与设备特征的基础维度 |
| 2. 电池与存储状态 | 当前精确电量百分比、充电状态(未充电/充电中/已充满)、是否开启低电量模式、手机总存储容量及当前精确到字节的剩余可用空间 | 在低电量或空间不足时降低后台功耗、提醒清理本地缓存 | 精确到字节的剩余存储空间变化极慢,常被当作高辨识度的设备特征;电量状态也曾被部分平台用于分析用户当下心理状态 |
| 3. 网络与运营商环境 | 公网 IP 地址(只要发起网络请求,服务端必然获知)、当前网络类型(Wi-Fi 或蜂窝网络)、本地局域网 IP、是否开启了 VPN 或网络代理(Proxy) | 优化网络请求策略、切换清晰度、进行网络安全风控 | 通过公网 IP 地址,无需任何定位权限即可通过 IP 库反推用户所在的城市甚至区县级粗略位置及宽带运营商 |
| 4. 地区与语言偏好 | 系统当前语言、首选语言列表、当前时区(如 Asia/Shanghai)、地区格式设置、日历制式、当前启用的键盘输入法列表 | 实现多语言国际化(i18n)、本地化时间与货币格式展示 | 语言列表、时区与第三方输入法的组合,能高度还原用户的地域背景与使用习惯 |
| 5. 特定唯一标识符 | IDFV(Identifier for Vendor) 以及存储在系统 Keychain(钥匙串) 中的自定义标记字符串 | 供同一家公司旗下的多款应用共享登录态、维持设备内账号安全 | 即使拒绝了 ATT 追踪弹窗,同一开发者账号下的多款 App 仍能通过 IDFV 识别同一台设备;而写入 Keychain 的标记甚至在卸载重装 App 后依然保留 |
| 6. 基础物理传感器 | 陀螺仪、加速计、磁力计(指南针) 的实时三维运动数据 | 体感游戏控制、摇一摇功能、图像防抖与横竖屏切换 | “运动与健身”权限管控的是计步与健康数据,而底层物理传感器默认无需权限即可高频读取,可被用于分析用户持握姿势、步态甚至微动作 |
| 7. 无障碍与系统设置 | 是否开启旁白(VoiceOver)、粗体文本、字体缩放比例、深色模式(Dark Mode)、减弱动态效果等辅助功能状态 | 为视障或特定习惯用户优化无障碍阅读与交互体验 | 极其个性化的辅助功能开关组合,在人群中具有极高的唯一性辨识度 |
两个常被误解的“半静默”敏感区域
除了上述完全静默的信息外,还有两项数据处于大众认知的模糊地带:
- 系统剪贴板(Pasteboard):
读取剪贴板并不像相册那样需要事先弹出一个“是否允许访问剪贴板”的系统权限开关。在早期系统中,App 切到前台即可无感读取你复制的淘口令、地址或密码。自 iOS 16 起,苹果引入了拦截与提示机制:如果是用户主动点击界面上的“粘贴”控件,或同开发者应用间合理调用,系统仅在顶部闪过一条横幅提示(“XX 粘贴自 XX”);若 App 试图在后台或初始化时未经用户动作强行跨应用读取剪贴板,系统则会弹出阻断式确认框。 - Wi-Fi 名称(SSID 与 BSSID):
很多人以为 App 能直接看到自己连着哪个 Wi-Fi。实际上,由于路由器 MAC 地址(BSSID)能被轻易用于高精度室内定位,苹果早就收紧了策略:App 必须先获得用户的“定位权限”,才能读取当前连接的 Wi-Fi 名称(SSID)和路由器硬件地址。在没有定位权限时,App 只知道设备处于 Wi-Fi 环境,但无法得知具体连接的是哪一个热点。
三、猫鼠游戏:“设备指纹”与苹果的隐私反击战
既然单个免授权信息(比如“深色模式”或“iOS 17.5”)无法锁定具体的人,为什么广告与数据追踪公司依然对它们趋之若鹜?
答案在于交叉熵与组合唯一性。当你把这些无需授权即可静默获取的参数拼接在一起:
硬件型号 + 系统精确开机时间 + 磁盘总容量与剩余字节数 + 特定系统文件创建时间戳 + 语言与字体缩放比例 + 公网 IP
经过哈希算法计算后,就能得出一个几乎在全球范围内独一无二的识别码——这就是行业内普遍采用的“设备指纹(Device Fingerprinting)”(国内部分广告联盟早期推行的 CAID 等方案便高度依赖此类原理)。通过设备指纹,哪怕用户关闭了广告追踪(IDFA 为全零),甚至卸载重装了应用,服务端依然能精准认出这台设备。
为了彻底斩断这种绕过 ATT(App Tracking Transparency)框架的隐蔽追踪行为,苹果自 iOS 17 起引入、并在 2024 年 5 月 1 日正式实施了强制性的 隐私清单(Privacy Manifest)与必填理由 API(Required Reason API) 审查机制。
其核心逻辑非常直接:苹果无法在系统层面直接封禁磁盘空间、系统时间或文件属性接口(否则所有正常 App 都会崩溃),但苹果可以通过静态代码扫描,强制要求开发者为每一次底层调用“签字画押”,声明合法的业务用途,严禁将其用于生成设备指纹。
四、开发者实战:5 大类 Required Reason API 完整拆解
从开发者的视角来看,只要你的主工程代码或引入的第三方 SDK 在编译后的二进制文件中包含了以下 5 大类别中的任何一个底层符号,App Store Connect 在接收提审包并执行静态二进制分析(Static Binary Analysis)时就会触发拦截,要求必须在 PrivacyInfo.xcprivacy 文件中声明对应的合法理由代码(Reason Code):
| API 类别(Category Key) | 涵盖的具体 Swift / ObjC / C / POSIX 接口符号 | 官方允许的常用合法理由代码(Reason Code) |
|---|---|---|
1. 用户默认设置 (NSPrivacyAccessedAPICategoryUserDefaults) | • UserDefaults / NSUserDefaults 下的所有读写方法 | • CA92.1:仅在 App 自身内部读写配置与状态(99% 的常规主工程选此项) • 1C8F.1:通过 App Group 在主应用、Widget 小组件或 Extension 之间共享数据 • C56D.1:第三方 SDK 自身用于读写内部状态(仅限 SDK 内部清单使用) |
2. 文件时间戳 (NSPrivacyAccessedAPICategoryFileTimestamp) | • Foundation: NSFileCreationDate, NSFileModificationDate, fileModificationDate • URL Resource: NSURLContentModificationDateKey, NSURLCreationDateKey • POSIX C 函数: stat, lstat, fstat, fstatat, getattrlist, fgetattrlist, getattrlistbulk | • C617.1:读取 App 沙盒容器、App Group 容器或 CloudKit 容器内的文件时间戳(如缓存过期校验、本地文件管理) • DDA9.1:将文件修改或创建时间直接展示在 UI 上供用户查看 • 3B52.1:读取用户通过 Document Picker 等方式主动授权访问的外部文件信息 • 0A2A.1:第三方 SDK 封装了文件时间戳接口供宿主 App 调用 |
3. 系统启动时间 (NSPrivacyAccessedAPICategorySystemBootTime) | • ProcessInfo.processInfo.systemUptime / NSProcessInfo.systemUptime • 底层时钟: mach_absolute_time() • C 接口: clock_gettime()(传入 CLOCK_UPTIME_RAW 或 CLOCK_MONOTONIC 等参数时)、sysctl(读取 KERN_BOOTTIME) | • 35F9.1:用于计算 App 内部事件之间的时间差(如性能监控、统计页面停留时长、音视频同步、动画计时器) • 8FFB.1:用于计算应用内特定事件发生的绝对时间戳 • 3D61.1:在用户主动选择提交的可选崩溃或错误报告(Bug Report)中附带系统运行时间 |
4. 磁盘空间 (NSPrivacyAccessedAPICategoryDiskSpace) | • URL Resource: NSURLVolumeAvailableCapacityKey, NSURLVolumeAvailableCapacityForImportantUsageKey, NSURLVolumeAvailableCapacityForOpportunisticUsageKey, NSURLVolumeTotalCapacityKey • FileManager: NSFileSystemFreeSize, NSFileSystemSize • POSIX C 函数: statfs, statvfs, fstatfs, fstatvfs, getattrlist | • E174.1:在写入文件前检查磁盘空间是否充足,或根据剩余空间清理本地缓存、决定是否下载资源(最常用) • 854F.1:在界面上向用户直观展示剩余存储空间(如网盘类、相册清理类或专业摄像类 App) • 7D9E.1:在用户主动提交的可选 Bug 报告中附带磁盘空间诊断信息 |
5. 当前启用键盘 (NSPrivacyAccessedAPICategoryActiveKeyboards) | • UITextInputMode.activeInputModes | • 54BD.1:根据用户设备安装的键盘语言定制 UI 界面或优化输入体验 • 3EC4.1:应用本身就是一款自定义第三方输入法(Custom Keyboard App),需要判断自身激活状态 |
五、工程配置模板与 6 大提审避坑指南
在 Xcode 中,你可以通过菜单栏依次点击 File → New → File → App Privacy 创建名为 PrivacyInfo.xcprivacy 的配置文件。它的底层本质是一个标准的 XML 属性列表(plist)。
1. 标准 PrivacyInfo.xcprivacy 源码模板
如果习惯直接通过源码(Open As → Source Code)进行维护,可以参考以下覆盖了常见业务场景的标准结构:
<?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>
<!-- 1. 是否开启了 ATT 追踪(若代码调用了 requestTrackingAuthorization 设为 true,否则为 false) -->
<key>NSPrivacyTracking</key>
<false/>
<!-- 2. 用于追踪的域名列表(若未开启追踪或无特定追踪域名,保持空数组) -->
<key>NSPrivacyTrackingDomains</key>
<array/>
<!-- 3. 收集的数据类型声明(需与 App Store Connect 后台填写的隐私营养标签对齐) -->
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<!-- 4. 必填理由 API(Required Reason API)声明列表 -->
<key>NSPrivacyAccessedAPITypes</key>
<array>
<!-- 声明 UserDefaults:应用内读写 + App Group 共享 -->
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
<string>1C8F.1</string>
</array>
</dict>
<!-- 声明文件时间戳:沙盒内文件管理与缓存校验 -->
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<!-- 声明磁盘空间:写文件前容量检查与缓存清理 -->
<dict>
<key>NSPrivacyAccessedAPAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<!-- 声明系统启动时间:计算内部事件耗时 -->
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
</array>
</dict>
</plist>
(注:检查上方配置时,务必确保所有的 Key 拼写严丝合缝,例如磁盘空间的 Key 为 NSPrivacyAccessedAPIType,任何微小的字母重拼都会触发 ITMS-91056 无效键值报错。)
2. 开发者必须警惕的 6 个实战暗坑
坑位一:切忌“宁滥勿缺”全选所有类别
不少开发者为了图省事,不管工程里有没有用到,直接把 5 大类和几十个 Reason Code 全部塞进 PrivacyInfo.xcprivacy。
- 风险: 苹果的静态扫描器会交叉比对二进制文件的符号表(Symbol Table)与你的隐私清单。如果你声明了
ActiveKeyboards(键盘),但二进制符号表里根本没有activeInputModes;或者你声明了854F.1(向用户展示磁盘空间),但审核员在 App 界面里根本找不到展示存储空间的地方,就会直接判定为虚假声明并拒审。 - 准则: 严格按照工程实际命中的符号与真实业务场景按需填写。
坑位二:混淆“普通静态库”与“苹果点名必查 SDK(Listed SDKs)”
项目中引入的第三方库(CocoaPods / SPM / 静态 .a / .framework)在隐私清单的处理上分为两类:
- 苹果官方点名的常用第三方 SDK(Listed SDKs): 包括但不限于 AFNetworking、Alamofire、SDWebImage、Kingfisher、Firebase、AppsFlyer、Lottie、FMDB 等上百个主流库。这些库必须以独立 Bundle 或动态库的形式自带官方的
PrivacyInfo.xcprivacy且具备有效数字签名。宿主 App 无法在自己的主工程清单里替它们“代填”,唯一的解决方案是将这些依赖升级到支持 Privacy Manifest 的最新官方版本。 - 未上榜的中小开源库或公司内部私有静态库: 当它们通过静态链接(Static Link)编译进主包时,其目标代码会直接合并进主应用的可执行二进制文件中。如果这些库调用了受控 API 且自身没有独立资源包,你应当直接在主工程的
PrivacyInfo.xcprivacy中将其用到的 API 理由一并合并声明。
坑位三:遗漏独立 Target(Widget 与各类 Extension)
如果你的工程包含 Widget(桌面小组件)、Notification Service Extension(推送通知扩展)、Share Extension 或 App Clip,它们在编译产物中都是拥有独立 Mach-O 可执行文件的独立 Target。
- 如果某个 Extension 代码(或其链接的静态库)调用了
UserDefaults或文件时间戳接口,必须确保 Xcode 右侧面板的 Target Membership 勾选了对应的 Extension Target(或者为该 Extension 单独创建一份匹配的PrivacyInfo.xcprivacy)。 - 务必在
Build Phases → Copy Bundle Resources中核实该文件确实被打包进了产物根目录,否则提包后会立刻收到ITMS-91053: Missing API declaration邮件警告。
坑位四:被 C/C++ 底层 POSIX 函数“背刺”
很多时候,你在 Swift 或 Objective-C 代码里全局搜索 NSFileCreationDate 或 systemUptime 结果为空,但上传 App Store Connect 后依然被提示缺失 FileTimestamp 或 SystemBootTime 声明。
- 真相: 苹果扫描的是编译后的底层 Mach-O 导入符号表。只要工程里引入了 SQLite、解压缩库(如
libzip、SSZipArchive)、日志库(如CocoaLumberjack、Mars Xlog)或音视频库(如FFmpeg),它们的 C/C++ 底层代码必然会调用标准 POSIX 函数stat()、fstat()(命中文件时间戳)或mach_absolute_time()、clock_gettime()(命中系统启动时间)。
坑位五:善用命令行工具在本地精准排雷
在打包上传前,无需盲目猜测是哪个库触发了敏感 API。打开终端进入编译产物目录,运行以下两条命令即可快速定位:
# 1. 语法校验:检查 PrivacyInfo.xcprivacy 是否存在 XML 标签闭合或格式错误
plutil -lint PrivacyInfo.xcprivacy
# 2. 符号扫描:检索主二进制文件或第三方 .a/.framework 中是否包含受控底层符号
nm -u YourAppBinary | grep -E "stat|fstat|statfs|mach_absolute_time|clock_gettime|systemUptime|activeInputModes|NSUserDefaults"
如果想要排查具体是 Pods 目录下的哪一个静态库引用了 statfs(磁盘空间),可以直接对构建产物中的静态库批量检索:
grep -rn "statfs" ./Pods/
坑位六:提审前使用 Xcode Organizer 生成聚合隐私报告
将配置合入主干并准备发布前,可以用 Xcode 内置的报告生成器做最后一次全量体检:
- 在 Xcode 菜单栏选择
Product → Archive生成正式归档包。 - 在弹出的 Organizer 窗口中,右键点击刚刚归档成功的版本,选择
Generate Privacy Report。 - 检查导出的 PDF 报告:如果报告能够顺利生成,且清晰列出了主工程及内嵌各个 SDK 的 API 声明与数据类型,说明格式与嵌套关系完全合规,此时再上传 App Store Connect 即可稳妥通关。
从早年粗放读取设备名与 MAC 地址,到 iOS 14.5 强推 ATT 追踪弹窗,再到如今深究到底层 C 语言符号的 PrivacyInfo.xcprivacy 隐私清单,iOS 生态的隐私合规早已不再是一纸表面协议,而是深入到了编译链接与二进制静态分析的工程细节之中。对于普通用户而言,了解这些机制能帮助我们看清商业应用在后台的数据触角;而对于 iOS 开发者来说,透彻理解底层符号与隐私清单的映射关系,则是保障应用平稳迭代、避免提审卡壳的必修基本功。
版权属于:soarli
本文链接:https://blog.soarli.top/archives/1219.html
转载时须注明出处及本声明。