4. Android权限模型(二):运行时权限机制、权限授予与撤销、权限策略变更历史
好,我们接着聊权限模型。上一章我们把权限的“家底”——声明、分组、保护级别——都翻了一遍。这一章,咱们聚焦在运行时权限这个让无数开发者又爱又恨的机制上。
说实话,Android 6.0 之前那套“安装时授权”的模式,我当年做项目时真是受够了。用户安装应用时,看到一长串权限列表,基本就是闭着眼点“接受”。应用拿到的权限,几乎等于“永久有效”。这设计,说白了就是把信任门槛放在了安装那一刻,但用户后续的使用行为,系统根本管不着。
4.1 运行时权限机制的诞生
为什么会推出运行时权限?核心原因就一个:用户对隐私的控制权太弱了。
我记得 2015 年 Google I/O 大会上,Android 团队公布了一个数据:超过 80% 的用户在安装应用时,根本不会仔细看权限列表。你想想看,一个手电筒应用要读取联系人,一个计算器要访问位置——用户可能装完就忘了,但权限却一直在后台悄悄开着。
运行时权限机制,说白了就是把授权时机从“安装时”挪到了“使用时”。应用在需要某个敏感权限时,必须弹出一个系统对话框,让用户当场决定“给”还是“不给”。
核心变化:
- 安装时:不再展示敏感权限列表,用户只需确认应用的基本信息
- 运行时:应用主动请求,系统弹窗,用户实时决策
- 用户可随时在设置中撤销已授予的权限
这个改动,我个人觉得是 Android 安全史上最成功的决策之一。它把“信任”这个抽象概念,变成了每次使用时的一次具体选择。
4.2 权限请求的完整流程
咱们来看一个典型的运行时权限请求流程。假设你的应用需要访问相机:
// 1. 检查权限是否已授予
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 2. 判断是否需要向用户解释为什么需要这个权限
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CAMERA)) {
// 显示一个解释说明,告诉用户为什么需要相机权限
showRationaleDialog();
} else {
// 3. 直接请求权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA_PERMISSION);
}
} else {
// 权限已授予,直接打开相机
openCamera();
}
这段代码看起来简单,但里面有几个坑,我一个个说。
4.2.1 checkSelfPermission:先查后要
应用不能一上来就弹窗请求权限。必须先检查当前状态。这个检查是同步的,不会阻塞主线程太久,因为底层走的是 Binder 调用,直接查 PackageManagerService 里的记录。
我曾经遇到过一个案例:某个应用在 onResume 里每次都调用 requestPermissions,结果用户每次回到前台都看到弹窗。这就是典型的“没检查就直接要”。用户烦了,直接卸载。
4.2.2 shouldShowRequestPermissionRationale:要不要解释?
这个方法返回 true 的情况有两种:
- 用户之前拒绝过这次请求
- 用户之前拒绝过,并且勾选了“不再询问”
嗯,这里有个细节要注意:第一次请求时,这个方法返回 false。所以你不能在第一次请求前就弹解释对话框。我见过一些新手开发者,在第一次请求前就弹自定义对话框,结果用户点了“允许”后,系统又弹一个——双重弹窗,体验极差。
我的建议:第一次请求时直接调 requestPermissions。如果用户拒绝了,下一次再通过 shouldShowRequestPermissionRationale 判断是否需要解释。这样既符合规范,又不会打扰用户。
4.2.3 onRequestPermissionsResult:处理结果
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CAMERA_PERMISSION) {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 用户同意了,打开相机
openCamera();
} else {
// 用户拒绝了
// 这里要判断用户是“拒绝一次”还是“拒绝并不再询问”
if (!ActivityCompat.shouldShowRequestPermissionRationale(
this, Manifest.permission.CAMERA)) {
// 用户勾选了“不再询问”,需要引导用户去设置页手动开启
showSettingsRedirectDialog();
} else {
// 只是拒绝一次,可以再次请求
showRetryDialog();
}
}
}
}
这里有个关键判断:用户拒绝后,shouldShowRequestPermissionRationale 返回 false 意味着什么?意味着用户勾选了“不再询问”。这时候你再弹 requestPermissions 也没用了,系统会直接回调拒绝,不会弹窗。唯一的办法是引导用户去系统设置里手动开启。
避坑指南:我曾经在一个项目中,没有处理“不再询问”的情况。结果用户点了拒绝并勾选不再询问后,应用每次启动都尝试请求权限,但系统静默拒绝,导致相机功能永远无法使用。用户反馈说“应用坏了”,其实是我们代码没处理好。
4.3 权限授予与撤销的底层机制
运行时权限的授予和撤销,最终都落在 PackageManagerService (PMS) 里。咱们来看看底层是怎么玩的。
4.3.1 权限授予
当用户在弹窗中点击“允许”,系统会走以下流程:
- SystemUI 的 GrantPermissionsActivity 接收到用户操作
- 通过 Binder 调用 PMS 的 grantRuntimePermission 方法
- PMS 检查调用方是否有授权权限(只有系统 UI 和系统服务有)
- PMS 更新 /data/system/packages.xml 中的权限记录
- PMS 向所有注册的权限监听器发送广播
这里有个有意思的点:权限授予是持久化的。记录在 packages.xml 里,即使应用被杀死、手机重启,权限依然有效,直到用户手动撤销或应用被卸载。
4.3.2 权限撤销
撤销有两种方式:
- 用户手动撤销:在设置 → 应用 → 权限中关闭
- 系统自动撤销:Android 11 引入的“自动重置权限”功能
自动重置权限这个功能,我特别喜欢。如果一个应用几个月没被使用,系统会自动撤销它所有的运行时权限。下次用户打开这个应用时,应用需要重新请求权限。这有效防止了“僵尸应用”在后台偷偷使用权限。
技术细节:自动重置权限的触发条件是应用进程被杀死后,超过一定时间(默认 90 天)未被启动。PMS 会定期扫描所有已安装应用,检查上次使用时间,对超期的应用执行 revokeRuntimePermission。
4.4 权限策略变更历史
Android 的权限策略,几乎每个大版本都在变。我整理了一张表,方便你对照:
| Android 版本 | 核心变更 | 我的评价 |
|---|---|---|
| 6.0 (API 23) | 引入运行时权限模型 | 里程碑式的改变,用户终于有了控制权 |
| 8.0 (API 26) | 权限分组细化,新增 INSTALL 权限组 | 安装来源控制,防止恶意应用静默安装 |
| 10 (API 29) | 后台位置权限需要单独申请 | 这个改动让很多地图类应用不得不重新设计权限请求流程 |
| 11 (API 30) | 一次性权限、自动重置权限 | 一次性权限是个好设计,但很多应用适配得不好 |
| 12 (API 31) | 大致位置权限、摄像头/麦克风开关 | 隐私指示器(绿点)终于来了,用户能直观看到权限使用 |
| 13 (API 33) | 通知权限独立、照片选择器 | 通知权限不再默认开启,应用必须请求 |
| 14 (API 34) | 照片选择器增强、后台权限进一步收紧 | 系统越来越倾向于“最小权限原则” |
你看这个趋势,权限策略越来越严格,用户控制权越来越大。我个人觉得这是好事。早期 Android 那种“安装即信任”的模式,说白了是把安全责任甩给了用户。现在系统主动承担了更多管理职责。
4.5 知识体系总览
为了让你更直观地理解这一章的内容,我画了一张图:
这张图把运行时权限的三个核心维度串起来了:请求流程、授予与撤销、版本变更。你写代码时,脑子里要有这张图——每次权限请求,都是在跟 PMS 打交道,都是在跟用户博弈。
4.6 一些实战建议
最后,分享几个我在项目中踩过的坑,希望能帮你少走弯路:
- 不要一次性请求多个权限:用户会懵。一次请求一个,或者按功能分组请求。比如“拍照”功能,先请求相机,再请求存储。
- 权限被拒绝后,不要反复弹窗:用户已经表达了意愿。如果用户选择了“不再询问”,就引导去设置页。反复弹窗只会让用户卸载你的应用。
- 测试时记得覆盖“拒绝并不再询问”的场景:很多开发者在模拟器上测试时,只点了“允许”和“拒绝”,忽略了第三种情况。结果上线后,用户反馈权限弹不出来。
- 关注 targetSdkVersion:不同 Android 版本的权限行为,跟 targetSdkVersion 有关。比如 targetSdkVersion 低于 23 的应用,在 Android 6.0+ 设备上会默认获得所有权限(兼容模式),但这在 Android 11 之后被禁止了。
嗯,运行时权限这块,内容其实挺多的。但核心就一句话:尊重用户的选择,做好每一种情况的处理。代码写得好不好,往往就体现在这些边界情况上。