第3章:运行时权限请求API实现——registerForActivityResult 结合 RequestMultiplePermissions、单权限请求流程、回调处理
3.1 原理概述
自 Android 11(API 30)起,Google 逐步废弃了传统的 onRequestPermissionsResult() 回调方式,并推荐使用 Activity Result API 来管理权限请求。在 API 33 及以上版本中,registerForActivityResult() 配合 RequestMultiplePermissions 或 RequestPermission 契约成为处理运行时权限的标准方案。
- RequestMultiplePermissions:同时请求多个权限,返回一个
Map<String, Boolean>表示每个权限的授予状态。 - RequestPermission:仅请求单个权限,返回一个
Boolean表示是否被授予。
这两种契约都基于 ActivityResultLauncher,在 Activity 或 Fragment 初始化时注册,并通过 launch() 方法触发系统权限对话框。回调在用户做出选择后(同意/拒绝/不再询问)自动执行,无需再手动编写 onRequestPermissionsResult()。
3.2 实现流程概览
| 步骤 | 操作 | 说明 |
|---|---|---|
| 1 | 创建 Contract 与 Launcher | 在 Activity 或 Fragment 中调用 registerForActivityResult(),传入 RequestMultiplePermissions() 或 RequestPermission() 实例,并实现回调 Lambda。 |
| 2 | 检查权限状态 | 使用 ContextCompat.checkSelfPermission() 判断权限是否已授予,避免重复弹窗。 |
| 3 | 触发权限请求 | 调用 launcher.launch(permissionsArray) 或 launcher.launch(Manifest.permission.XXX)。 |
| 4 | 处理回调结果 | 根据返回的 Map 或 Boolean 判断每个权限的授权情况,并执行相应逻辑(如继续、提示、引导设置)。 |
3.3 核心代码示例
以下演示使用 RequestMultiplePermissions 同时请求蓝牙扫描、连接和位置权限(API 33 以上所需):
// 在 Activity 的 onCreate 或初始化阶段注册
private final ActivityResultLauncher<String[]> multiplePermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), result -> {
// result: Map<String, Boolean>
boolean bluetoothScan = result.getOrDefault(Manifest.permission.BLUETOOTH_SCAN, false);
boolean bluetoothConnect = result.getOrDefault(Manifest.permission.BLUETOOTH_CONNECT, false);
boolean accessFineLocation = result.getOrDefault(Manifest.permission.ACCESS_FINE_LOCATION, false);
if (bluetoothScan && bluetoothConnect && accessFineLocation) {
// 所有权限已获得,开始蓝牙操作
startBluetoothDiscovery();
} else {
// 权限被拒绝,提示用户
showPermissionDeniedDialog();
}
});
// 在需要权限的地方触发请求
private void requestBluetoothPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
multiplePermissionLauncher.launch(new String[]{
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.ACCESS_FINE_LOCATION
});
} else {
// 旧版本逻辑,略
}
}
单权限请求示例(仅请求 BLUETOOTH_CONNECT):
private final ActivityResultLauncher<String> singlePermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
if (isGranted) {
// 连接蓝牙设备
} else {
// 提示用户
}
});
private void requestSinglePermission() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
singlePermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT);
}
}
3.4 回调处理与状态判断
回调中的 result 参数仅反映用户对本次弹出对话框的操作结果,并非持久状态。每次请求后都应重新检查权限状态,以防止用户在设置中手动更改权限。
最佳实践:回调中不要直接使用 result 作为最终授权依据,应再调用一次
checkSelfPermission() 确认当前授予状态。这是因为在部分厂商 ROM 上,用户可能勾选“不再询问”后,返回的 result 仍为 false,但再次请求时系统不再弹窗。3.5 内联流程图:运行时权限请求执行流程
3.6 调试与验证
推荐使用 ADB 命令模拟权限授予/撤销来测试回调逻辑:
# 授予单个权限(以 BLUETOOTH_CONNECT 为例)
adb shell pm grant com.example.app android.permission.BLUETOOTH_CONNECT
# 撤销权限
adb shell pm revoke com.example.app android.permission.BLUETOOTH_CONNECT
# 查看当前权限状态
adb shell dumpsys package com.example.app | grep "grantedPermissions"
在 Logcat 中过滤 ActivityResultContracts 或自定义日志标签,观察回调执行顺序。特别注意:当用户勾选“不再询问”时,launch() 不会再显示系统弹窗,而是直接返回拒绝结果。此时应使用 shouldShowRequestPermissionRationale() 检测并引导用户到设置页。
3.7 常见错误
| 错误场景 | 原因 | 解决方案 |
|---|---|---|
| IllegalStateException: LifecycleOwner 未初始化 | 在 Activity 或 Fragment 的 onCreate() 之前调用 registerForActivityResult() | 确保在 onCreate() 或 onViewCreated() 中注册,且传入的 LifecycleOwner 处于 CREATED 状态以上。 |
| 同一 Launcher 被重复调用 | 未检查权限状态多次 launch | 每次请求前先检查权限是否已授予,避免重复弹窗。 |
| 回调中 result 为 false 但权限实际已授予 | 用户可能在设置中手动开启后未重新触发回调 | 始终以 checkSelfPermission() 为准,回调仅作 UI 提示 |
| Android 12+ 弹窗被系统限制 | 短时间内多次请求权限,系统将自动拒绝 | 合并权限请求,使用 RequestMultiplePermissions 一次性请求所有相关权限。 |
3.8 性能与稳定性优化
- 合并请求:将同一功能所需的所有权限放在一次请求中,减少弹窗次数,提高用户体验。
- 延迟注册:Launcher 应尽早注册,但不要在
onCreate()之前调用launch(),避免因生命周期未就绪导致崩溃。 - Rationale 提示:当用户首次拒绝后,
shouldShowRequestPermissionRationale()返回 true,此时应在 UI 中解释为什么需要该权限,再重新请求。若返回 false,则说明用户已选择“不再询问”,应直接跳转应用设置页。 - 避免重复请求:使用标志位记录是否已经请求过,防止在多个入口触发重复弹窗。
- 内存泄漏防范:Launcher 会持有 Activity 引用,若在 Fragment 中使用,务必在
onDestroy()中清理或使用getViewLifecycleOwner()注册。
重要提醒:在 API 33 以下的设备上使用
registerForActivityResult 时,蓝牙权限可能仍需要位置权限作为前提条件,务必做好版本兼容处理。同时,部分国产厂商对权限弹窗有额外限制,测试时应在多台设备上验证回调行为。