63、权限管理:运行时权限请求、权限分组、特殊权限(悬浮窗、通知)

权限管理,说白了就是你的App能不能碰用户手机里的敏感数据。我记得刚入行那会儿,Android 6.0还没普及,只要在清单文件里写个权限声明,App就能为所欲为。后来Google意识到这样不行——你一个手电筒App凭什么读通讯录?于是运行时权限机制就来了。

今天咱们就把权限管理这块彻底聊透。我会从最基础的运行时权限讲起,再到权限分组、特殊权限,最后聊聊我在实际项目中踩过的坑。

运行时权限:从清单声明到动态请求

先看一个最简单的例子。假设你的App需要读取联系人:

// 第一步:在AndroidManifest.xml中声明
<uses-permission android:name="android.permission.READ_CONTACTS" />

// 第二步:在代码中检查并请求
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
        != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this,
            new String[]{Manifest.permission.READ_CONTACTS},
            REQUEST_CODE_READ_CONTACTS);
}

嗯,这里要注意。很多新手会犯一个错误:只写了清单声明,没写运行时请求。结果在Android 6.0以上的设备上,权限永远拿不到。我有个朋友就因为这个,App上线后被用户骂惨了。

请求权限后,系统会弹出一个对话框。用户可以选择「允许」或「拒绝」。如果用户拒绝了,你需要在回调里处理:

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
                                       @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_CODE_READ_CONTACTS) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 权限已授予,可以读取联系人
        } else {
            // 权限被拒绝
            // 这里可以提示用户,或者引导去设置页面
        }
    }
}
我的习惯:我会封装一个权限工具类,把检查、请求、回调处理都统一管理。这样代码不会散落在各个Activity里,维护起来也方便。

权限分组:为什么有些权限不会弹窗?

你可能会发现,有些权限请求时系统会直接授予,根本不弹窗。这就是权限分组在起作用。

Android把权限分成了几个等级:

权限等级 特点 示例
普通权限(Normal) 系统自动授予,不弹窗 INTERNET、ACCESS_NETWORK_STATE
危险权限(Dangerous) 需要运行时请求,会弹窗 READ_CONTACTS、CAMERA、ACCESS_FINE_LOCATION
特殊权限(Special) 需要跳转系统设置页面手动开启 SYSTEM_ALERT_WINDOW、REQUEST_INSTALL_PACKAGES

危险权限还按功能分成了组。比如「联系人」组包含READ_CONTACTS和WRITE_CONTACTS。如果你申请了READ_CONTACTS,系统会自动把同组的WRITE_CONTACTS也授予。反过来,如果用户拒绝了READ_CONTACTS,同组的其他权限也会被拒绝。

我曾经踩过的坑:有一次我同时申请了CAMERA和RECORD_AUDIO,以为它们是不同组,结果用户拒绝了CAMERA,RECORD_AUDIO也跟着被拒了。后来查文档才发现,这两个权限属于同一个「麦克风」组。所以申请权限时,最好按功能分组申请,不要一股脑全扔进去。

特殊权限:悬浮窗和通知

特殊权限是Android里最麻烦的一类。它们不能通过常规的requestPermissions来申请,必须跳转到系统设置页面,让用户手动开启。

悬浮窗权限

悬浮窗权限(SYSTEM_ALERT_WINDOW)常用于悬浮球、视频小窗等功能。申请方式如下:

// 检查悬浮窗权限
if (Settings.canDrawOverlays(this)) {
    // 已有权限,可以显示悬浮窗
} else {
    // 没有权限,跳转到设置页面
    Intent intent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
            Uri.parse("package:" + getPackageName()));
    startActivityForResult(intent, REQUEST_CODE_OVERLAY);
}

跳转后,用户需要在设置页面找到你的App,手动打开「显示在其他应用上层」的开关。这个过程很繁琐,用户很可能直接放弃。所以我一般会在跳转前弹一个说明对话框,告诉用户为什么要这个权限。

通知权限

从Android 13开始,通知权限也变成了运行时权限。这意味着你的App不能默认给用户发通知了,必须主动申请。

// 检查通知权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
            != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.POST_NOTIFICATIONS},
                REQUEST_CODE_NOTIFICATIONS);
    }
}

这里有个细节:如果用户拒绝了通知权限,你可以在合适的时机再次申请。但不要一上来就弹窗,那样很容易被用户永久拒绝。我一般会在用户完成某个关键操作后,再提示开启通知。

核心要点:
  • 普通权限:清单声明即可,系统自动授予
  • 危险权限:清单声明 + 运行时请求,需要处理用户拒绝
  • 特殊权限:清单声明 + 跳转系统设置,需要引导用户手动开启

知识体系总览

下面这张图把权限管理的核心逻辑串起来了。你可以看到从权限声明到最终授予的完整流程:

Android 权限管理核心流程 AndroidManifest.xml 声明 普通权限(自动授予) 危险权限(运行时请求) 特殊权限(手动开启) 无需额外处理 requestPermissions + 回调处理 跳转系统设置页面 权限授予 / 拒绝

避坑指南

做权限管理这么多年,我总结了几条血泪教训:

  • 不要一次性申请所有权限。用户看到一堆弹窗,大概率会全部拒绝。按需申请,用到哪个就申请哪个。
  • 处理「不再询问」的情况。如果用户勾选了「不再询问」,下次requestPermissions会直接返回拒绝。这时候你需要引导用户去设置页面手动开启。
  • 特殊权限要提前说明。跳转系统设置前,最好弹一个对话框解释为什么需要这个权限。用户理解了,才愿意去手动开启。
  • 兼容低版本。Android 6.0以下的设备不需要运行时权限,但你的代码要处理好版本判断,别在低版本上崩溃。
我的一个小技巧:我会在App的「设置」页面里加一个「权限管理」入口,让用户可以随时查看和修改权限。这样既符合用户预期,也能减少因为权限问题导致的差评。

权限管理看似简单,但做不好真的很影响用户体验。你想想看,用户刚下载你的App,还没看到功能呢,先被一堆权限弹窗轰炸,换谁都会反感。所以我的原则是:能晚申请就晚申请,能不申请就不申请。实在需要,也要给用户一个合理的解释。

好了,关于权限管理就聊这么多。记住一句话:尊重用户的选择,才能赢得用户的信任。


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