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可能有差异。千万不要依赖分组行为来做业务逻辑判断。老老实实每个权限单独检查、单独申请。
三、运行时权限的完整流程
我个人习惯把权限请求封装成一个工具类,这样代码干净很多。核心流程就三步:
- 检查权限:调用ContextCompat.checkSelfPermission()
- 解释原因:如果用户之前拒绝过,用shouldShowRequestPermissionRationale()判断,然后展示说明
- 发起请求:调用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的自动重置权限把存储权限给撤了。从那以后,我在每个需要权限的功能入口都加了权限检查,而不是只在启动时检查一次。
五、知识体系总览
下面这张图概括了权限管理的核心脉络:
六、总结
权限管理看似简单,但细节很多。我见过太多App因为权限处理不当导致崩溃或者用户体验极差。记住几个关键点:
- 每个权限单独检查,别依赖分组行为
- 处理所有拒绝场景,包括「不再询问」
- 在功能入口处检查权限,而不是在启动时
- 适配Android 11+的新变化
嗯,权限这块就聊这么多。代码写得好不好,有时候就看这些细节处理得是否到位。
公众号:蓝海资料掘金营,微信deep3321