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 权限授予

当用户在弹窗中点击“允许”,系统会走以下流程:

  1. SystemUI 的 GrantPermissionsActivity 接收到用户操作
  2. 通过 Binder 调用 PMS 的 grantRuntimePermission 方法
  3. PMS 检查调用方是否有授权权限(只有系统 UI 和系统服务有)
  4. PMS 更新 /data/system/packages.xml 中的权限记录
  5. 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 知识体系总览

为了让你更直观地理解这一章的内容,我画了一张图:

运行时权限机制知识体系 运行时权限 权限请求流程 checkSelfPermission shouldShowRationale requestPermissions onRequestPermissionsResult 授予与撤销 PMS 更新 packages.xml 用户手动撤销 系统自动重置(90天) 版本变更历史 6.0: 运行时权限诞生 10: 后台位置权限 11: 一次性/自动重置 12-14: 持续收紧 核心原则:最小权限 + 用户控制

这张图把运行时权限的三个核心维度串起来了:请求流程、授予与撤销、版本变更。你写代码时,脑子里要有这张图——每次权限请求,都是在跟 PMS 打交道,都是在跟用户博弈。

4.6 一些实战建议

最后,分享几个我在项目中踩过的坑,希望能帮你少走弯路:

  • 不要一次性请求多个权限:用户会懵。一次请求一个,或者按功能分组请求。比如“拍照”功能,先请求相机,再请求存储。
  • 权限被拒绝后,不要反复弹窗:用户已经表达了意愿。如果用户选择了“不再询问”,就引导去设置页。反复弹窗只会让用户卸载你的应用。
  • 测试时记得覆盖“拒绝并不再询问”的场景:很多开发者在模拟器上测试时,只点了“允许”和“拒绝”,忽略了第三种情况。结果上线后,用户反馈权限弹不出来。
  • 关注 targetSdkVersion:不同 Android 版本的权限行为,跟 targetSdkVersion 有关。比如 targetSdkVersion 低于 23 的应用,在 Android 6.0+ 设备上会默认获得所有权限(兼容模式),但这在 Android 11 之后被禁止了。

嗯,运行时权限这块,内容其实挺多的。但核心就一句话:尊重用户的选择,做好每一种情况的处理。代码写得好不好,往往就体现在这些边界情况上。


公众号:蓝海资料掘金营,微信deep3321