4、Android权限模型演变:从6.0到14.0的关键变化归纳
做 targetSdk 升级这么多年,权限模型是最让我头疼的模块之一。从 Android 6.0 到 14.0,权限设计经历了三次大的重构。你想想看,以前一个清单文件搞定的事,现在要写几十行运行时申请代码。
我总结了六个关键转折点。每个点都在实际项目里踩过坑。下面咱们一个一个捋。
4.1 Android 6.0 (API 23):运行时权限的革命
这是权限模型的分水岭。6.0 之前,用户安装时授权,之后就不能再改。6.0 之后,权限改成了运行时动态申请。
说白了,就是把"一次性授权"变成了"按需授权"。用户可以在设置里随时关掉某个权限。应用不能再假设"安装时给了就一直有"。
我记得第一次升级 targetSdk 到 23 时,代码里到处是运行时权限的判断。一个简单的拍照功能,得先检查权限,再申请,再处理回调。代码量直接翻倍。
- 危险权限需要在运行时通过
requestPermissions()申请 - 用户可随时在设置中撤销权限
- 应用必须在每次使用权限前检查是否已被授予
// 6.0 之后的标准权限申请写法
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA},
REQUEST_CODE_CAMERA);
} else {
// 已经有权限,直接打开相机
openCamera();
}
4.2 Android 8.0–9.0:权限分类与组管理
8.0 引入了权限组的概念。同一个组的权限,只要用户授权了一个,同组的其他权限就自动获得授权。
举个例子,CAMERA 和 RECORD_AUDIO 属于不同组。但 READ_CONTACTS、WRITE_CONTACTS、GET_ACCOUNTS 属于同一个"联系人"组。
实际操作中,这其实没太大影响。我个人习惯还是每个权限单独申请,避免用户产生困惑。你想想看,用户明明只点了同意拍照,结果通讯录权限也被偷偷拿了,体验很不好。
9.0 主要调整了权限组的归属关系。比如把 ACTIVITY_RECOGNITION 独立出来。这些细节在升级回测时很容易被忽略。
4.3 Android 10.0–11.0:精细化与自动化
这两个版本是权限模型走向精细化的关键节点。我从 10.0 开始真正感受到了权限管理的复杂度。
- 后台位置权限从普通位置权限中拆分出来,需单独申请
ACCESS_BACKGROUND_LOCATION - 存储权限开始实施分区存储(Scoped Storage),不再直接访问共享目录
- 新增
ACCESS_MEDIA_LOCATION权限用于读取图片地理位置
我在项目中遇到过存储权限拆分的问题。升级 targetSdk 到 29 后,原有的文件访问代码全炸了。图片保存路径变了,文件扫描方式也得改。当时排查了很久才发现是 Scoped Storage 的限制。
// 10.0 申请后台位置权限示例
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
},
REQUEST_CODE_LOCATION);
}
11.0 带来了两个重要机制:
- 一次性权限:用户可以选择"仅本次允许",应用下次使用时需要重新申请。这个设计我很喜欢——类似 iOS 的"临时授权"。
- 自动重置权限:如果应用超过两个月没被使用,系统会自动撤销所有已授予的运行时权限。这意味着应用在后台被唤醒时,可能会因为权限丢失而崩溃。
4.4 Android 13.0–14.0:细粒度与后台限制
这两个版本把权限模型推向了极致细粒度。每次升级都像在拆盲盒。
13.0 的三大变化:
- 细粒度媒体权限:
READ_MEDIA_IMAGES、READ_MEDIA_VIDEO、READ_MEDIA_AUDIO替代了原来的READ_EXTERNAL_STORAGE。你得明确告诉用户你要读取哪种类型的文件。 - 通知权限变成运行时权限:
POST_NOTIFICATIONS需要动态申请。如果用户拒绝,应用无法弹出任何通知。 - 附近的设备权限:
NEARBY_WIFI_DEVICES替代了原来的ACCESS_FINE_LOCATION用于 Wi-Fi 扫描。
// 13.0 申请细粒度媒体权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.requestPermissions(this,
new String[]{
Manifest.permission.READ_MEDIA_IMAGES,
Manifest.permission.READ_MEDIA_VIDEO
},
REQUEST_CODE_MEDIA);
} else {
// 低版本使用旧的存储权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE},
REQUEST_CODE_STORAGE);
}
我因为通知权限踩过一个坑。升级 targetSdk 到 33 后,应用首次打开没有任何通知。调试了半天才发现是忘了申请 POST_NOTIFICATIONS。通知权限之前一直是静默授予的,突然变成运行时权限,很多人都没意识到。
14.0 的核心收紧:
- 后台位置权限进一步限制。即使应用获得了
ACCESS_BACKGROUND_LOCATION,系统也会定期提醒用户检查。 - 新增"选择性照片访问"模式。用户可以选择只给某几张照片的权限,而不是整个图库。
- 后台行为权限更加严格。应用在后台时,某些权限的使用会受到限制。
| Android 版本 | API 级别 | 关键变化 | 对开发者的影响 |
|---|---|---|---|
| 6.0 | 23 | 运行时权限模型 | 需动态申请危险权限 |
| 7.0–8.0 | 24–26 | 权限组概念、权限分类细化 | 同组权限自动授予 |
| 9.0 | 28 | 权限组归属调整 | 部分权限组拆分或合并 |
| 10.0 | 29 | 后台位置权限、分区存储 | 需单独申请后台权限 |
| 11.0 | 30 | 一次性权限、自动重置权限 | 权限生命周期变短 |
| 12.0 | 31 | 附近的设备权限 | Wi-Fi 权限与位置解耦 |
| 13.0 | 33 | 细粒度媒体权限、通知权限 | 媒体访问需分类型申请 |
| 14.0 | 34 | 后台行为限制、选择性照片访问 | 后台权限使用更严格 |
4.5 升级回测中的权限适配策略
说了这么多变化,落地到实际工作中该怎么做?我总结了三条原则:
- 先查 targetSdk 差异:每次升级前,先列出目标版本与当前版本的权限差异列表。重点关注:新增了哪些权限、哪些权限被拆分、哪些权限改了行为。
- 逐权限回测:不要只测试"正常授权"路径。必须测试以下场景:
- 用户拒绝权限
- 用户选择"仅本次允许"
- 用户在设置中撤销权限
- 应用在后台被唤醒时权限已被重置
- 用模拟器覆盖多版本:我习惯在模拟器上跑 Android 10、11、13、14 四个版本的兼容性测试。每个版本覆盖上述所有拒绝场景。
adb shell pm revoke <package> <permission> 快速切换权限状态,无需手动点击设置界面。这在批量回测时能省不少时间。
# 撤销某个权限(模拟用户拒绝)
adb shell pm revoke com.example.myapp android.permission.ACCESS_FINE_LOCATION
# 授予某个权限
adb shell pm grant com.example.myapp android.permission.ACCESS_FINE_LOCATION
# 查看应用当前权限状态
adb shell dumpsys package com.example.myapp | grep permission
权限适配是 targetSdk 升级中最容易出问题的环节。很多崩溃都发生在"用户拒绝权限"这个分支上。我自己每次升级都会专门列一个权限检查清单,逐条确认。
targetSdkVersion 提升时触发新的权限行为变更。哪怕你的 compileSdk 已经很高,只要 targetSdk 没提,新行为就不会生效。这也是为什么回测时必须把 targetSdk 调到目标版本。