## 第2章 Android运行时权限模型详解
### 2.1 运行时权限模型原理
Android 6.0(API 23)引入了**运行时权限模型**,将权限分为两类:
- **正常权限(Normal Permissions)**:在AndroidManifest.xml中声明后系统自动授予,无需用户确认。
- **危险权限(Dangerous Permissions)**:涉及用户隐私或设备敏感功能,必须在运行时动态请求,用户可拒绝或撤销。
蓝牙相关权限在Android 12之后进一步细分,多个权限属于危险级别,需要开发者通过标准流程在运行时请求。
### 2.2 权限分组(Permission Groups)
危险权限按功能逻辑划分为**权限分组(Permission Group)**。系统弹窗展示时,若同一分组内多个权限同时请求,只会显示一次对话框,用户授权即授予组内所有声明的权限。
| 权限名称 | 所属分组 | 保护级别 | Android版本变化 |
|---------|---------|---------|---------------|
| `BLUETOOTH` | 无分组(正常权限) | normal | 始终自动授予 |
| `BLUETOOTH_ADMIN` | 无分组(正常权限) | normal | 始终自动授予 |
| `BLUETOOTH_CONNECT` | `BLUETOOTH` | dangerous | Android 12+ |
| `BLUETOOTH_SCAN` | `BLUETOOTH` | dangerous | Android 12+ |
| `BLUETOOTH_ADVERTISE` | `BLUETOOTH` | dangerous | Android 12+ |
| `ACCESS_FINE_LOCATION` | `LOCATION` | dangerous | Android 10+改为仅前台 |
| `ACCESS_COARSE_LOCATION` | `LOCATION` | dangerous | 配合精确位置使用 |
> **关键点**:从Android 12开始,蓝牙扫描与广告必须使用对应的运行时权限,而`BLUETOOTH`和`BLUETOOTH_ADMIN`仅用于传统蓝牙连接管理。位置权限(`ACCESS_FINE_LOCATION`)在Android 10+仍用于BLE扫描,但Android 12+推荐使用`BLUETOOTH_SCAN`替代。
### 2.3 权限请求流程
标准流程分为三个核心阶段:
1. **检查权限**:调用`ContextCompat.checkSelfPermission()`检测当前是否已授权。
2. **请求权限**:若未授权,通过`ActivityCompat.requestPermissions()`发起请求。
3. **处理回调**:重写`onRequestPermissionsResult()`,根据用户响应执行相应业务逻辑。
#### 核心API
// 检查单个权限
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.BLUETOOTH_SCAN)
!= PackageManager.PERMISSION_GRANTED) {
// 未授权,开始请求
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT},
REQUEST_CODE_BLUETOOTH);
} else {
// 已授权,直接执行蓝牙操作
startBluetoothScan();
}
// 处理授权结果
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions,
@NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_CODE_BLUETOOTH) {
if (grantResults.length > 0 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startBluetoothScan();
} else {
// 权限被拒绝,可展示解释UI或降级功能
showPermissionDeniedDialog();
}
}
}
#### 内联SVG:运行时权限请求流程图
### 2.4 用户授权机制与行为
当系统弹出权限请求对话框时,用户有四种选择(Android 11+):
- **允许**:授予权限,持续有效直到用户撤销或应用被卸载。
- **仅在使用中允许(Foreground Only)**:仅在前台时授权(适用于位置、蓝牙扫描等后台访问受限权限)。
- **拒绝**:本次拒绝,下次请求会再次弹窗。
- **拒绝并不再询问**:永久拒绝,后续`requestPermissions()`不会弹窗,必须引导用户进入系统设置手动授权。
> **警告**:Android 11+ 引入**权限自动重置**,如果用户长时间不打开应用,系统会自动撤销所有运行时权限。应用应每次执行敏感操作前都检查权限状态,不能假设一次授权永久有效。
### 2.5 调试与常见错误
#### ADB 调试权限
# 查看应用已授予的权限
adb shell dumpsys package com.example.yourapp | grep permission
# 手动授予权限(用于测试)
adb shell pm grant com.example.yourapp android.permission.BLUETOOTH_SCAN
adb shell pm grant com.example.yourapp android.permission.BLUETOOTH_CONNECT
# 手动撤销权限
adb shell pm revoke com.example.yourapp android.permission.BLUETOOTH_SCAN
#### 常见错误排查表
| 报错/异常 | 可能原因 | 解决办法 |
|----------|---------|---------|
| `SecurityException: Permission Denial` | 未在Manifest声明权限或缺少运行时请求 | 检查清单文件,确保
完整,并调用requestPermissions |
| 弹窗不出现 | 权限为normal级别无需手动请求,或已自动授予 | 确认权限保护级别为dangerous |
| 用户拒后仍执行 | 未检查grantResults | 确保在onRequestPermissionsResult中判断grantResults[0] |
| 多权限只弹一次对话框 | 属于同一权限分组 | 无需处理,此为系统正常行为 |
| Android 12+ 蓝牙不工作 | 缺少`BLUETOOTH_SCAN`/`BLUETOOTH_CONNECT`/`BLUETOOTH_ADVERTISE` | 升级targetSdkVersion >= 31并声明运行时权限 |
### 2.6 性能与稳定性优化
1. **避免重复请求**:每次执行蓝牙操作前都应检查权限状态,但不要无限制重复弹窗。用户拒绝后应记录状态,等待用户主动触发重试(例如按钮“开启权限”)。
2. **缓存权限状态**:使用`SharedPreferences`或`ViewModel`缓存用户选择(“不再询问”),避免在每次APP启动时都检查,但需注意权限可能被系统撤销。
3. **优雅降级**:当用户拒绝关键权限时,应向用户清晰说明该权限的必要性,并提供按钮引导至系统设置(`Settings.ACTION_APPLICATION_DETAILS_SETTINGS`)。切忌静默失败导致功能不可用。
4. **处理权限撤销**:监听`onRequestPermissionsResult`与`onResume`,系统设置中权限变更后Activity会重建,此时应重新检查权限并更新UI。
5. **后台位置限制**:Android 10+ 的`ACCESS_BACKGROUND_LOCATION`需要单独请求,蓝牙后台扫描应尽量避免依赖位置权限,优先使用`BLUETOOTH_SCAN`(Android 12+)。