25、Permissions:运行时权限、权限分组、权限最佳实践

权限管理,说白了就是App和用户之间的「信任契约」。你想想看,一个手电筒应用非要读取你的通讯录,这合理吗?Android从6.0开始引入了运行时权限模型,就是为了解决这种乱象。今天我们就来彻底搞懂它。

一、权限模型的前世今生

在Android 6.0之前,权限是「安装时授权」——你安装App时一次性同意所有权限,之后就没法改了。这其实是个很糟糕的设计。我记得早期有个项目,用户反馈说「为什么我装了个壁纸App,它就能偷偷上传我的通讯录?」嗯,这就是旧模型的弊端。

从Android 6.0(API 23)开始,Google推出了运行时权限模型:

  • 正常权限:系统自动授予,无需用户确认。比如访问网络、设置闹钟。
  • 危险权限:涉及用户隐私,必须运行时弹窗请求。比如读取联系人、获取位置。
  • 特殊权限:需要用户到系统设置中手动开启。比如悬浮窗、修改系统设置。

核心要点:危险权限必须动态申请,而且用户可以随时在设置中关闭。你的App必须能优雅地处理权限被拒绝的情况。

二、权限分组:别被「组」这个概念坑了

Google把危险权限分成了几个组,比如:

权限组 包含的权限
CALENDAR READ_CALENDAR, WRITE_CALENDAR
CAMERA CAMERA
CONTACTS READ_CONTACTS, WRITE_CONTACTS, GET_ACCOUNTS
LOCATION ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION
PHONE READ_PHONE_STATE, CALL_PHONE, READ_CALL_LOG, ...
STORAGE READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE

很多人以为「同一个组里的权限,只要申请一个,其他的就自动授权了」。我在项目中就踩过这个坑。实际情况是:同一个组里的权限,如果用户授权了其中一个,再申请同组的其他权限时,系统会直接授权,不会弹窗。但如果你一次性申请多个权限,系统会每个都弹窗。

注意:权限分组是Android系统的内部实现,不同厂商的ROM可能有差异。千万不要依赖分组行为来做业务逻辑判断。老老实实每个权限单独检查、单独申请。

三、运行时权限的完整流程

我个人习惯把权限请求封装成一个工具类,这样代码干净很多。核心流程就三步:

  1. 检查权限:调用ContextCompat.checkSelfPermission()
  2. 解释原因:如果用户之前拒绝过,用shouldShowRequestPermissionRationale()判断,然后展示说明
  3. 发起请求:调用ActivityCompat.requestPermissions()

来看一个完整的例子:

// 检查相机权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
        != PackageManager.PERMISSION_GRANTED) {
    
    // 用户之前拒绝过,需要解释
    if (ActivityCompat.shouldShowRequestPermissionRationale(this,
            Manifest.permission.CAMERA)) {
        // 弹一个Dialog解释为什么需要这个权限
        showExplanationDialog();
    } else {
        // 直接请求
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.CAMERA},
                REQUEST_CAMERA_PERMISSION);
    }
} else {
    // 权限已授予,直接打开相机
    openCamera();
}

然后在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)) {
                // 用户勾选了「不再询问」,引导去设置
                showSettingsDialog();
            } else {
                // 只是拒绝了一次,下次还可以弹窗
                showToast("需要相机权限才能拍照");
            }
        }
    }
}

我的建议:不要在Activity里写这些逻辑。封装一个PermissionHelper或者用第三方库(比如RxPermissions),代码会清爽很多。我曾经在一个项目里看到Activity有800行权限处理代码...嗯,那是个噩梦。

四、权限最佳实践:避坑指南

做了这么多年Android,我总结了几条铁律:

1. 最小权限原则

只申请你真正需要的权限。别因为「可能以后会用」就提前申请。用户对权限很敏感,你申请得越多,被卸载的概率越大。

2. 时机很重要

不要在App启动时一股脑把所有权限都申请了。用户连你的界面都没看到,先看到一堆权限弹窗,体验极差。我建议在用户真正要用到某个功能时,再申请对应的权限。比如用户点击「拍照」按钮时,才申请相机权限。

3. 处理「拒绝并不再询问」

这是最容易被忽视的场景。如果用户勾选了「不再询问」,你再调用requestPermissions()也不会弹窗了,直接返回拒绝。这时候你需要引导用户去系统设置里手动开启。代码示例:

private void openAppSettings() {
    Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
    intent.setData(Uri.parse("package:" + getPackageName()));
    startActivity(intent);
}

4. Android 11+ 的权限变化

从Android 11开始,如果用户多次点击「拒绝」,系统会默认视为「不再询问」。另外,自动重置权限功能会让长时间未使用的App被自动撤销权限。你的App必须能处理权限被系统自动撤销的情况。

我曾经踩过的坑:有个用户反馈说「昨天还能用的功能,今天就不能用了」。查了半天,发现是Android 11的自动重置权限把存储权限给撤了。从那以后,我在每个需要权限的功能入口都加了权限检查,而不是只在启动时检查一次。

五、知识体系总览

下面这张图概括了权限管理的核心脉络:

Android 权限管理知识体系 正常权限 危险权限 特殊权限 系统自动授予 无需用户操作 运行时弹窗请求 用户可随时关闭 系统设置开启 需引导用户操作 最佳实践:最小权限 + 合理时机 + 处理拒绝 + 适配新版本 权限分组(同组自动授权) shouldShowRequestRationale Android 11+ 自动重置

六、总结

权限管理看似简单,但细节很多。我见过太多App因为权限处理不当导致崩溃或者用户体验极差。记住几个关键点:

  • 每个权限单独检查,别依赖分组行为
  • 处理所有拒绝场景,包括「不再询问」
  • 在功能入口处检查权限,而不是在启动时
  • 适配Android 11+的新变化

嗯,权限这块就聊这么多。代码写得好不好,有时候就看这些细节处理得是否到位。


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