第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 内联流程图:运行时权限请求执行流程

运行时权限请求流程图(API 33+) 触发权限请求(用户操作) 检查当前权限状态 已授权? 直接执行操作 调用 launcher.launch() 系统弹窗 → 用户选择 回调 onResult 执行 授权成功? 提示/引导设置 继续业务逻辑 ⚠ 注意:用户可能选择 “不再询问”导致永久拒绝

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 时,蓝牙权限可能仍需要位置权限作为前提条件,务必做好版本兼容处理。同时,部分国产厂商对权限弹窗有额外限制,测试时应在多台设备上验证回调行为。