3. Android 权限模型详解:危险权限、运行时权限、权限分组、系统弹窗机制
Android 从 6.0(API 23)开始采用运行时权限模型,将权限分为正常权限和危险权限。危险权限必须在运行时请求用户授权,系统会弹出原生弹窗供用户确认或拒绝。理解权限分组与弹窗机制是实现协程异步权限请求流程的基础,直接影响到用户体验与代码健壮性。
3.1 危险权限与运行时权限
危险权限是指可能触及用户隐私或设备敏感数据的权限,例如相机、位置、麦克风、通讯录等。此类权限必须通过 ActivityCompat.requestPermissions() 或 registerForActivityResult() 在运行时动态申请,并处理 onRequestPermissionsResult() 回调。系统弹窗由 Android 框架自动管理,应用无法定制 UI,但可以决定何时触发。
| 权限分组 | 包含危险权限 | 典型场景 |
|---|---|---|
| CAMERA | android.permission.CAMERA | 拍照、扫码 |
| LOCATION | ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION, ACCESS_BACKGROUND_LOCATION | 地图、导航 |
| STORAGE | READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE (Android 9 及以下) | 文件读写 |
| PHONE | READ_PHONE_STATE, CALL_PHONE, READ_CALL_LOG, WRITE_CALL_LOG, ADD_VOICEMAIL, USE_SIP, PROCESS_OUTGOING_CALLS | 通话、识别设备 |
| MICROPHONE | RECORD_AUDIO | 录音、语音识别 |
| CONTACTS | READ_CONTACTS, WRITE_CONTACTS, GET_ACCOUNTS | 联系人同步 |
| SENSORS | BODY_SENSORS | 健康监测 |
| SMS | SEND_SMS, RECEIVE_SMS, READ_SMS, RECEIVE_WAP_PUSH, RECEIVE_MMS | 短信功能 |
| CALENDAR | READ_CALENDAR, WRITE_CALENDAR | 日历事件 |
关键原则:同一分组内,只要用户授权了其中一个权限,系统会自动授予该分组内的所有权限,无需重复弹窗。例如用户同意
CAMERA,则同一应用再次请求 CAMERA 之外的相机相关权限(如 RECORD_VIDEO 如果被归入该组)会直接通过。
3.2 权限分组机制
Android 将危险权限划分为多个逻辑分组(见上表)。分组边界由 AOSP 定义,不同 Android 版本可能调整。分组的重要意义在于:
- 一次授权,组内通吃:用户授权一个分组中的任意一个权限,整个分组的其他权限自动获得授权。
- 权限请求可以合并:请求同一分组内的多个权限时,系统只弹一次窗,用户决定后生效到整个分组。
- 后台位置权限特殊:
ACCESS_BACKGROUND_LOCATION属于 LOCATION 分组,但 Android 10+ 要求单独弹窗请求。
实际开发中,应当按场景组合权限请求,利用分组减少弹窗次数,提升通过率。
3.3 系统弹窗机制
系统弹窗(Permission Dialog)由 PackageManagerService 控制,属于系统级 UI,不可定制样式,但应用可以判断弹窗是否应显示:
- 首次请求:系统弹窗包含“允许”和“拒绝”两个选项。
- 用户拒绝一次:下次请求仍然弹窗,但增加了“不再询问”复选框(Android 11+ 改为弹窗后不再提供选项,而是直接跳转到设置)。
- 用户选择“不再询问”:此后
requestPermissions()不再弹窗,直接返回拒绝结果。应用必须引导用户前往系统设置手动授权。 - 权限被撤销:系统设置中可以随时关闭权限,应用应在每次使用权限前检查授权状态。
常见陷阱:不要假设权限被拒绝后下次弹窗一定出现。如果用户勾选了“不再询问”,后续请求会静默失败,且
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 权限请求流程图
下图展示了从权限检查到最终用户决策的完整流转,涵盖系统弹窗机制与分组授权影响:
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),解释为何需要该权限,能显著提高用户授权比例。不要等系统弹窗被拒绝后再解释,而是主动引导。