第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的授权流程比较“任性”,你想想看,用户拒绝了你,你连解释的机会都没有。

授权流程大概长这样:

  1. 应用/调用方 发起授权请求(比如用requestWhenInUseAuthorization())。
  2. 系统服务 检查Info.plist里有没有对应的描述文案。没有?直接崩溃。
  3. 系统弹窗 显示给用户,询问是否允许。
  4. 用户点击“允许”或“拒绝”。
  5. 系统回调 通过delegate或block返回授权状态。
  6. 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 用户授权流程总览

说了这么多,我画个流程图帮你把整个授权过程串起来。这张图里,我特意把容易忽略的边界情况都标出来了:

iOS 权限授权流程(核心逻辑) App/SDK 发起授权请求 检查 Info.plist 是否有描述文案 没有则直接 Crash 无 有 系统闪退 系统弹窗询问用户 用户选择允许或拒绝 (仅触发一次) 状态回调 Delegate/Block 执行降级策略 使用默认值 / 备用方案 ⚠ 关键说明: 拒绝后再次请求不会弹窗,只能引导用户去设置中手动开启。   iOS 14+ 新增“大致位置”选项(limited),需检测 accuracyAuthorization 属性。

这张图把整个流程串起来了。你仔细看看那个“仅触发一次”的标注——多少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才算真正做好了隐私层面的基础建设。