3. Android 权限模型详解:危险权限、运行时权限、权限分组、系统弹窗机制

Android 从 6.0(API 23)开始采用运行时权限模型,将权限分为正常权限和危险权限。危险权限必须在运行时请求用户授权,系统会弹出原生弹窗供用户确认或拒绝。理解权限分组与弹窗机制是实现协程异步权限请求流程的基础,直接影响到用户体验与代码健壮性。

3.1 危险权限与运行时权限

危险权限是指可能触及用户隐私或设备敏感数据的权限,例如相机、位置、麦克风、通讯录等。此类权限必须通过 ActivityCompat.requestPermissions() 或 registerForActivityResult() 在运行时动态申请,并处理 onRequestPermissionsResult() 回调。系统弹窗由 Android 框架自动管理,应用无法定制 UI,但可以决定何时触发。

常见危险权限分组(部分)
权限分组包含危险权限典型场景
CAMERAandroid.permission.CAMERA拍照、扫码
LOCATIONACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ACCESS_BACKGROUND_LOCATION地图、导航
STORAGEREAD_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE (Android 9 及以下)文件读写
PHONEREAD_PHONE_STATE, CALL_PHONE, READ_CALL_LOG, WRITE_CALL_LOG, ADD_VOICEMAIL, USE_SIP, PROCESS_OUTGOING_CALLS通话、识别设备
MICROPHONERECORD_AUDIO录音、语音识别
CONTACTSREAD_CONTACTS, WRITE_CONTACTS, GET_ACCOUNTS联系人同步
SENSORSBODY_SENSORS健康监测
SMSSEND_SMS, RECEIVE_SMS, READ_SMS, RECEIVE_WAP_PUSH, RECEIVE_MMS短信功能
CALENDARREAD_CALENDAR, WRITE_CALENDAR日历事件
关键原则:同一分组内,只要用户授权了其中一个权限,系统会自动授予该分组内的所有权限,无需重复弹窗。例如用户同意 CAMERA,则同一应用再次请求 CAMERA 之外的相机相关权限(如 RECORD_VIDEO 如果被归入该组)会直接通过。

3.2 权限分组机制

Android 将危险权限划分为多个逻辑分组(见上表)。分组边界由 AOSP 定义,不同 Android 版本可能调整。分组的重要意义在于:

  • 一次授权,组内通吃:用户授权一个分组中的任意一个权限,整个分组的其他权限自动获得授权。
  • 权限请求可以合并:请求同一分组内的多个权限时,系统只弹一次窗,用户决定后生效到整个分组。
  • 后台位置权限特殊:ACCESS_BACKGROUND_LOCATION 属于 LOCATION 分组,但 Android 10+ 要求单独弹窗请求。

实际开发中,应当按场景组合权限请求,利用分组减少弹窗次数,提升通过率。

3.3 系统弹窗机制

系统弹窗(Permission Dialog)由 PackageManagerService 控制,属于系统级 UI,不可定制样式,但应用可以判断弹窗是否应显示:

  1. 首次请求:系统弹窗包含“允许”和“拒绝”两个选项。
  2. 用户拒绝一次:下次请求仍然弹窗,但增加了“不再询问”复选框(Android 11+ 改为弹窗后不再提供选项,而是直接跳转到设置)。
  3. 用户选择“不再询问”:此后 requestPermissions() 不再弹窗,直接返回拒绝结果。应用必须引导用户前往系统设置手动授权。
  4. 权限被撤销:系统设置中可以随时关闭权限,应用应在每次使用权限前检查授权状态。
常见陷阱:不要假设权限被拒绝后下次弹窗一定出现。如果用户勾选了“不再询问”,后续请求会静默失败,且 onRequestPermissionsResult() 返回拒绝但不会说明原因。需要通过 shouldShowRequestPermissionRationale() 判断是否应显示解释说明。

3.4 代码实现与 API

标准运行时权限请求流程(Java/Kotlin):

// 检查权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
        == PackageManager.PERMISSION_GRANTED) {
    // 已有权限,直接执行
    openCamera()
} else {
    // 需要请求权限
    ActivityCompat.requestPermissions(this,
            arrayOf(Manifest.permission.CAMERA),
            REQUEST_CAMERA)
}

// 处理回调
override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<String>,
        grantResults: IntArray
) {
    when (requestCode) {
        REQUEST_CAMERA -> {
            if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                openCamera()
            } else {
                // 权限被拒绝
                if (!ActivityCompat.shouldShowRequestPermissionRationale(this,
                                Manifest.permission.CAMERA)) {
                    // 用户勾选了“不再询问”,应引导前往设置
                    navigateToAppSettings()
                } else {
                    // 用户只是拒绝,可再次弹窗解释
                    showRationaleAndRequestAgain()
                }
            }
            return
        }
    }
}
协程封装思路:传统回调方式破坏协程上下文。使用 suspendCancellableCoroutine 或 ActivityResultContracts.RequestPermission 可以将其转换为挂起函数,从而在协程中直接 await 权限结果。这是后续章节的核心。

3.5 权限请求流程图

下图展示了从权限检查到最终用户决策的完整流转,涵盖系统弹窗机制与分组授权影响:

Android 运行时权限请求流程 Android 运行时权限请求流程 开始请求权限 检查 checkSelfPermission (分组内已有授权?) 权限已授权 直接执行功能 权限未授权 准备请求弹窗 是否应显示解释? 显示 Rationale 否 系统弹窗 (允许/拒绝) onRequestPermissionsResult 检查 grantResults 允许 执行功能 拒绝 处理拒绝分支 说明:若用户曾勾选“不再询问”,系统将跳过弹窗直接回调拒绝,需引导至系统设置。 分组授权:同一分组内已有授权时,checkSelfPermission 直接返回 GRANTED。

3.6 常见错误与调试

  • 错误 1:权限清单声明缺失 – 在 AndroidManifest.xml 中未声明权限,调用 requestPermissions() 会导致闪退。应始终先添加 <uses-permission> 标签。
  • 错误 2:在未检查授权状态下直接使用敏感 API – 导致 SecurityException。必须每次使用前调用 checkSelfPermission()。
  • 错误 3:忽略“不再询问”状态 – 多次弹窗拒绝后用户选择不再询问,应用仍不断弹窗会造成恶劣体验。正确做法:检测 shouldShowRequestPermissionRationale() 返回 false 且权限未授权时,跳转到系统设置页面。
  • 错误 4:在非 Activity 上下文中调用 requestPermissions – 例如在 Service 或 Application 中,弹窗需要 Activity 作为宿主。必须通过 Activity 实例发起请求。

调试技巧:

  • 使用 adb shell dumpsys package [packageName] 查看当前应用的权限授予状态。
  • 可以在开发者选项中通过“权限管理”强制切换权限状态进行测试。
  • 单元测试可使用 Robolectric 模拟权限授权结果。

3.7 性能与稳定性优化

  • 避免重复请求:在 Activity 或 Fragment 中缓存权限授予结果,减少 checkSelfPermission 调用次数(虽然开销很小,但最佳实践是只检查一次并记录状态)。
  • 利用分组减少弹窗:如果同时需要 CAMERA 和 RECORD_AUDIO,尽量在一次请求中一起申请(同一分组或不同分组可合并),减少用户拒绝次数。
  • 提前预处理:在启动页或关键功能入口预先请求最敏感的权限,避免用户操作时临时打断。
  • 使用协程挂起函数封装:将权限请求封装为 suspend fun,保证在协程内顺序执行,避免回调嵌套导致时序混乱。后续章节将详细实现。
最佳实践:建议在权限请求之前先通过 showRationale() 做一个轻量说明(使用 Snackbar 或 Dialog),解释为何需要该权限,能显著提高用户授权比例。不要等系统弹窗被拒绝后再解释,而是主动引导。