第4章:iOS权限模型
做SDK数据迁移,绕不开iOS的权限管理。这套模型说复杂也复杂,说简单也简单——你想想看,用户把手机交给你,系统得问清楚你要干嘛。我个人习惯把iOS权限模型拆成三块来看:权限类型、Info.plist配置、用户授权流程。搞明白这三块,迁移路上的坑能少踩一半。
4.1 iOS隐私权限类型
iOS把隐私权限分得挺细的。从我们SDK开发者的视角看,最常打交道的就这么几类:
| 权限类型 | 用途 | 对应的Info.plist Key |
|---|---|---|
| 位置权限 | 获取用户GPS或粗略位置 | NSLocationWhenInUseUsageDescription |
| 相机权限 | 扫码、拍照 | NSCameraUsageDescription |
| 相册权限 | 读取/写入照片库 | NSPhotoLibraryUsageDescription |
| 联系人权限 | 读取通信录 | NSContactsUsageDescription |
| 日历/提醒事项 | 写入事件或日程 | NSCalendarsUsageDescription |
| 蓝牙权限 | 周边设备交互 | NSBluetoothAlwaysUsageDescription |
| 网络权限 | 联网访问 | 无独立描述Key,需配置本地网络权限 |
这里给你提个醒:不要贪多。我见过不少SDK,一启动就把所有权限声明全塞进Info.plist,用户一看弹窗铺天盖地,直接卸载。你的SDK如果只是做数据上报,就别声明相机权限。
4.2 Info.plist配置——绕不过的硬门槛
Info.plist是iOS应用的门禁系统。你的SDK要用某个权限,宿主应用必须在Info.plist里配置对应的描述字符串,否则系统会直接让应用闪退。
致命陷阱:从iOS 10开始,如果你没有在Info.plist里配置权限描述文案,应用调用相关API时会直接crash,而不是默默拒绝。这个crash你是抓不到的——系统层直接杀掉进程。
配置方式很简单,以位置权限为例:
<key>NSLocationWhenInUseUsageDescription</key>
<string>此SDK需要获取您的位置信息用于广告推荐优化</string>
很多人会问:描述文案写多长合适?我个人经验是30-50个字最佳。太短了用户觉得敷衍,太长了用户根本不会读完。关键是要把为什么用这个权限说清楚。
另外要注意,不同权限类型,配置的Key还不一样。比如后台定位:
| 场景 | Key | 备注 |
|---|---|---|
| 使用期间定位 | NSLocationWhenInUseUsageDescription | 必须有 |
| 始终定位 | NSLocationAlwaysAndWhenInUseUsageDescription | iOS 11+ 必须同时配置 |
| 精确位置 | NSLocationDefaultAccuracyReduced | iOS 14+ 新增,默认关闭 |
这里有一个特别容易踩的坑——iOS 14+ 的相册权限变化。以前你只需写一个Key,现在如果要精确控制相册访问级别,还得加一个PHAccessLevel的配置。我当时迁移第一个SDK时,就是因为没注意这个,用户一直报“拒绝访问”的反馈,排查了两天才发现是Info.plist漏配了。
高效配置技巧:建议在主工程里创建一个Info.plist的检查单元测试,遍历你SDK用到的所有权限Key,用Bundle.main.object(forInfoDictionaryKey:)去验证宿主是否配置完整。不通过的,直接抛异常提示配置缺失。
4.3 用户授权流程——理解系统如何拒绝你
搞定了配置文件,接下来就是运行时授权。iOS的授权流程比较“任性”,你想想看,用户拒绝了你,你连解释的机会都没有。
授权流程大概长这样:
- 应用/调用方 发起授权请求(比如用
requestWhenInUseAuthorization())。 - 系统服务 检查Info.plist里有没有对应的描述文案。没有?直接崩溃。
- 系统弹窗 显示给用户,询问是否允许。
- 用户点击“允许”或“拒绝”。
- 系统回调 通过delegate或block返回授权状态。
- SDK层面 根据状态决定能否调用相关API。
等等,这里有一个细节:弹窗只能触发一次。如果用户第一次点了拒绝,你再调用授权方法,系统不会弹窗,而是直接返回“拒绝”状态。这和你之前看到的Android的运行时权限完全不同。
我早期做SDK的时候,就犯过这个错。用户第一次弹窗拒绝了位置权限,我代码里又调了一次授权请求,结果系统静默返回拒绝,我这边还以为网络问题,日志里全是超时重试的报错。后来才发现是权限状态没检查。
为什么会这样?因为iOS认为用户的第一次选择就是最终决定。要想重新弹窗?只有一个办法——引导用户去设置里手动打开。
// 正确做法:先检查权限状态,再决定是否弹引导
let status = CLLocationManager.authorizationStatus()
if status == .denied {
// 引导用户去设置
DispatchQueue.main.async {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
}
} else if status == .notDetermined {
locationManager.requestWhenInUseAuthorization()
}
嗯,这里还要注意一个细节:iOS 14+ 新增了“大致位置”选项。用户在选择“允许一次”或“使用期间允许”时,系统还会问一遍“要精确位置吗?”这直接导致你的SDK拿到的坐标精度可能下降。我迁移友盟SDK时就遇到这个问题,用户反馈定位不准,结果发现是我们的SDK没处理accuracyAuthorization属性,导致默认只拿到了大致位置。
4.4 授权状态枚举——你代码里必须处理的6种情形
每次权限请求,系统会返回一个状态。很多人只处理了授权和拒绝两种,剩下的四种几乎全忽略——这就是线上bug的温床。
| 状态 | 含义 | SDK应该怎么做 |
|---|---|---|
| .notDetermined | 尚未请求权限 | 发起授权请求 |
| .restricted | 受父控/MDM限制 | 降级处理,不阻塞主流程 |
| .denied | 用户拒绝 | 记录日志,使用默认值,不要反复请求 |
| .authorizedWhenInUse | 前台使用授权 | 可调用相关API,后台任务注意生命周期 |
| .authorizedAlways | 始终授权 | 可后台访问,但需注意电量消耗 |
| .limited(限iOS 14+) | 用户只允许部分数据 | 获取有限数据,不能假设全部可用 |
核心原则:权限丢失不应该是SDK失效的理由。你的SDK必须有“无权限模式”的降级逻辑。比如定位权限被拒,至少还能用IP定位。
4.5 用户授权流程总览
说了这么多,我画个流程图帮你把整个授权过程串起来。这张图里,我特意把容易忽略的边界情况都标出来了:
这张图把整个流程串起来了。你仔细看看那个“仅触发一次”的标注——多少SDK在这个点上栽跟头?我见过一些团队,用户拒绝了定位权限,他们的SDK还在后台疯狂循环请求,不仅没用,还把手机电量耗光,用户口碑崩盘。
4.6 避坑清单与最佳实践
做了这么多年隐私SDK迁移,我整理了一个避坑清单,分享给你:
- Info.plist描述文案必须具体,不能写“用于优化体验”。 Apple审核时会看你的申请理由是否合理。模棱两可的直接拒绝。
- 不要在主线程调用授权请求。 虽然系统弹窗本身不卡UI,但回调可能会触发一些UI刷新,做好线程安全。
- 用户拒绝后,不要马上弹引导设置。 我曾经犯过这个错——用户刚拒绝,我立刻弹一个“请在设置中打开权限”的对话框,用户瞬间炸毛,差评一片。等下次用到相关功能时再提示,体验好很多。
- 权限丢失不要影响SDK核心功能。 比如你的SDK是数据上报,定位权限被拒,就别因为这个断了网络请求。权限只是辅助,不是基础。
- 注意iOS 15+ 的“周边设备”权限。 蓝牙和本地网络现在归到不同类别了。如果你的SDK要做跨设备传输,得单独声明。
趁手的工具:Xcode 14+ 的PrivacyInfo.xcprivacy文件。从iOS 17开始,Apple要求SDK自带隐私清单。推荐你用这个文件统一声明所有权限,这样宿主不用手动配置Info.plist,减少漏配的可能性。
总结一下:iOS的权限模型,说白了就是一道配置+弹窗+降级的链条。配置错了crash,弹窗被拒永久沉默,降级没做好功能瘫痪。把这些边界情况都照顾到,你的SDK才算真正做好了隐私层面的基础建设。