Android 权限系统详解——跨平台开发者的权限管理指南
深入解析 Android 权限系统的分类、运行时申请流程及各版本变化,帮助 Flutter/RN/鸿蒙开发者在跨平台场景中正确处理权限问题。
一句话概括
Android 权限系统是应用访问敏感数据和系统功能的安全屏障,从声明、申请到运行时处理有一套完整的机制,跨平台开发者必须理解它才能确保插件正常工作和用户体验流畅。
背景与意义
权限系统存在的意义
想象一下,你安装了一个手电筒应用,它却悄悄读取你的通讯录并上传到服务器。这就是 Android 权限系统想要防止的场景——应用只能访问真正需要的系统资源和用户数据。
Android 的权限模型经历了从”安装时一次性授权”到”运行时按需授权”的重大转变:
- Android 5.1 及以前:安装时列出的所有权限必须全部同意才能安装,安装后权限不可撤销
- Android 6.0(API 23):引入了运行时权限模型,用户可以随时拒绝某个危险权限
- Android 10(API 29):引入了分区存储,限制对共享存储的访问
- Android 11(API 30):引入了”一次性权限”,让用户可以临时授权
- Android 12(API 31):后台定位权限收紧,剪切板访问受限
- Android 13(API 33):细粒度媒体权限、通知权限分离
- Android 14(API 34):进一步限制隐式 Intent 和后台 Activity 启动
对跨平台开发者的影响
在 Flutter/RN/鸿蒙跨平台项目中,权限管理面临独特的挑战:
- 多层抽象:跨平台框架对原生权限 API 做了封装,但封装并不总是完美的
- 插件依赖复杂:一个简单的”拍照”功能,可能依赖于相机权限、存储权限,甚至麦克风权限
- 版本碎片化:用户设备可能运行着 Android 6 ~ Android 14 的任何版本,权限行为差异巨大
- 权限请求时机:Flutter 的 UI 线程与原生权限回调之间的生命周期协调容易出问题
根据 2024 年 Google Play Console 的数据,权限相关问题导致的应用拒绝率高达 28%。而在跨平台应用中,由于权限处理不当引发的运行时崩溃占整体崩溃的 12% 左右。
Android 权限模型的核心原则
Android 权限系统遵循三个核心原则:
- 最小权限原则:应用只应请求完成任务所需的最少权限
- 用户可知原则:任何敏感操作都应让用户知晓并同意
- 用途透明原则:应用应在请求权限时说明为什么要使用该权限
核心知识点拆解
权限的三级分类
Android 权限分为三个等级:
1. 普通权限(Normal Permissions)
系统自动授予,不需要用户明确同意。这类权限不会直接威胁用户隐私或设备安全。
常见普通权限:
INTERNET(访问网络)ACCESS_NETWORK_STATE(获取网络状态)VIBRATE(控制震动)BLUETOOTH(蓝牙连接)SET_ALARM(设置闹钟)NFC(近场通信)
声明方式:在 AndroidManifest.xml 中声明即可,用户不会收到授权提示。
1
2
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
2. 危险权限(Dangerous Permissions)
涉及用户隐私或可能造成安全风险的权限,需要运行时动态申请。
危险权限按功能分组(权限组概念稍后详述):
| 权限组 | 权限列表 | 说明 |
|---|---|---|
| CALENDAR | READ_CALENDAR、WRITE_CALENDAR | 读取/写入日历 |
| CAMERA | CAMERA | 相机 |
| CONTACTS | READ_CONTACTS、WRITE_CONTACTS、GET_ACCOUNTS | 联系人 |
| LOCATION | ACCESS_FINE_LOCATION、ACCESS_COARSE_LOCATION、ACCESS_BACKGROUND_LOCATION | 位置信息 |
| MICROPHONE | RECORD_AUDIO | 麦克风录音 |
| PHONE | READ_PHONE_STATE、CALL_PHONE、READ_CALL_LOG、WRITE_CALL_LOG、ADD_VOICEMAIL、USE_SIP、PROCESS_OUTGOING_CALLS | 电话功能 |
| SENSORS | BODY_SENSORS | 传感器 |
| SMS | SEND_SMS、RECEIVE_SMS、READ_SMS、RECEIVE_WAP_PUSH、RECEIVE_MMS | 短信 |
| STORAGE | READ_EXTERNAL_STORAGE、WRITE_EXTERNAL_STORAGE(Android 13 之前) | 存储 |
3. 特殊权限(Special Permissions)
最敏感的权限类型,需要用户通过系统设置页面手动授予,无法通过标准 API 弹窗申请。
常见的特殊权限:
SYSTEM_ALERT_WINDOW(在其他应用上层显示悬浮窗)WRITE_SETTINGS(修改系统设置)REQUEST_INSTALL_PACKAGES(安装未知来源应用)MANAGE_EXTERNAL_STORAGE(管理所有文件访问)ACCESS_BACKGROUND_LOCATION(后台位置访问,Android 10+)
特殊权限的申请流程:
1
2
3
4
5
6
7
8
9
// 1. 检查是否有特殊权限
if (!Settings.canDrawOverlays(this)) {
// 2. 调起系统设置页面让用户手动授权
val intent = Intent(
Settings.ACTION_MANAGE_OVERLAY_PERMISSION,
Uri.parse("package:$packageName")
)
startActivity(intent)
}
权限组(Permission Groups)概念
权限组是 Google 引入的一个概念,旨在简化权限管理。同一组的权限,只要用户授予了其中一个,系统会自动授予该组中的所有其他权限。
但实际上,权限组的”自动授予”机制只在同一个组内且在同一个运行时段有效。许多开发者对这个机制有误解:
误区澄清:
- ❌ “只要用户同意了相机权限,以后所有相机设备都可以用了”——这不对,CAMERA 权限是具体的权限名称,不是设备控制。
- ❌ “用户同意了一个权限,同组的其他权限也自动授予”——这在 Android 11+ 中并不保证。权限组自动授予仅作用于第一次弹窗的上下文内。
- ❌ “权限组可以用于避免重复授权”——这不完全可靠,不同版本的 Android 行为不同。
实际行为:
- 如果用户授权了
CAMERA权限,在同一个运行时授权会话中(同一组 API 调用),RECORD_AUDIO会被自动授权(因为两者同属一个旧版分组方案,但在 Android 11+ 已被取消) - 在 Android 11+ 中,Google 实际上已经废弃了权限组的自动授权行为。每次申请的权限需要单独获得用户同意。
运行时权限申请流程
这是跨平台开发者最需要掌握的内容。以下是完整的危险权限运行时申请流程:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Step 1: 检查权限是否已授予
if (ContextCompat.checkSelfPermission(
this, Manifest.permission.CAMERA
) == PackageManager.PERMISSION_GRANTED
) {
// 权限已授予,直接执行操作
openCamera()
return
}
// Step 2: 判断是否需要展示权限说明(用户之前拒绝过)
if (ActivityCompat.shouldShowRequestPermissionRationale(
this, Manifest.permission.CAMERA
)
) {
// 用户之前拒绝过,展示详细说明为什么需要这个权限
showRationaleDialog("我们需要相机权限来拍摄商品照片")
}
// Step 3: 请求权限
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA),
CAMERA_PERMISSION_REQUEST_CODE
)
// Step 4: 处理权限请求结果(在 Activity 中)
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
when (requestCode) {
CAMERA_PERMISSION_REQUEST_CODE -> {
if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED
) {
// 用户同意
openCamera()
} else {
// 用户拒绝
handlePermissionDenied()
}
}
}
}
shouldShowRequestPermissionRationale 的三种状态
shouldShowRequestPermissionRationale() 方法返回 boolean,其含义与用户权限操作历史相关:
| 场景 | 返回值 | 说明 |
|---|---|---|
| 用户从未请求过该权限 | false | 首次请求,不需要解释 |
| 用户拒绝了上一次请求,但没有勾选”不再询问” | true | 展示说明为何需要此权限 |
| 用户拒绝了上一次请求,并勾选了”不再询问”或系统策略禁止再次提示 | false | 不能再次弹窗,需引导到系统设置 |
跨平台注意:在 Flutter 中,permission_handler 插件封装了 shouldShowRequestPermissionRationale,但不同版本的插件对这个 API 的调用时机有差异。某些旧版本的插件在 shouldShowRequestPermissionRationale 返回 true 时不会自动触发说明逻辑,需要你在 Dart 端手动处理。
Android 11+ 的权限自动重置
Android 11 引入了”权限自动重置”功能:如果用户几个月未使用某个应用,系统会自动撤销之前授予的运行时权限。
这意味着即使之前用户同意了某个权限,也不能永久依赖它。应用需要在上次授权后仍然做好权限被撤销的兜底处理。
Android 各版本的权限变化
Android 6(API 23, 2015)——运行时权限革命
- 引入运行时权限模型
targetSdkVersion >= 23的应用必须运行时申请危险权限- 兼容方式:
targetSdkVersion < 23的应用继续使用安装时授权
Android 8(API 26, 2017)——隐式广播与后台限制
- 大部分隐式广播接收器不再生效
- 后台服务启动限制(
startService限制,需使用JobScheduler)
Android 10(API 29, 2019)——分区存储与后台定位
- 分区存储(Scoped Storage):应用默认只能访问自己创建的文件,访问共享媒体文件需特殊权限
- 后台位置权限:从位置权限中分离,需要单独申请
ACCESS_BACKGROUND_LOCATION - 后台启动 Activity 的限制
跨平台影响:
- 使用
image_picker插件时,Android 10+ 不需要存储权限也能保存图片 - 使用
location插件时,需要明确区分前台和后台定位
Android 11(API 30, 2020)——一次性权限
- 一次性权限:用户在授权弹窗中选择”仅此一次”,权限在应用离开前台后被撤销
- 权限自动重置:长期未使用的应用,系统自动撤销其权限
- 包可见性:应用默认看不到其他应用列表,需在清单文件中声明
<queries>或申请QUERY_ALL_PACKAGES权限
Android 12(API 31, 2021)——剪切板与网络定位
- 剪切板访问:后台访问剪切板受限,前台读取剪切板时系统会显示 Toast 提示
- 后台定位收紧:通过蓝牙/WiFi 扫描获取的位置信息同样受
ACCESS_BACKGROUND_LOCATION限制 - 近似位置:用户可以仅授予近似位置(
ACCESS_COARSE_LOCATION)而非精确位置
Android 13(API 33, 2022)——通知权限与细粒度媒体权限
这是近年来权限变化最大的一次:
1. 通知权限分离
1
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
所有 targetSdk >= 33 的应用必须运行时申请这个新权限,才能在通知栏发送通知。这意味着:
- 应用的推送功能需要用户明确同意通知
- 第一次启动时弹出通知权限请求,很多用户会选择拒绝
- 传统的推送到达率大大降低
2. 细粒度媒体权限 READ_EXTERNAL_STORAGE 被拆分为三个独立的权限:
READ_MEDIA_IMAGES:读取图片READ_MEDIA_VIDEO:读取视频READ_MEDIA_AUDIO:读取音频
3. 附近的 Wi-Fi 设备权限
1
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
无需位置权限就能扫描附近的 Wi-Fi 设备。
Android 14(API 34, 2023)——进一步收紧
- 后台 Activity 启动受限
- 隐式 Intent 必须显式指定 package 或经过系统验证
- 进一步限制媒体文件的访问
权限声明的最佳实践
条件性权限声明
使用 android:required 或 android:maxSdkVersion 来限定权限的适用范围:
1
2
3
4
5
6
7
8
9
10
11
12
<!-- 只在 Android 12 及以下需要存储权限 -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- 只在 Android 13+ 需要细粒度媒体权限 -->
<uses-permission
android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission
android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission
android:name="android.permission.READ_MEDIA_AUDIO" />
理解 uses-permission vs uses-permission-sdk-23
<uses-permission>:应用在所有版本上都会声明这个权限<uses-permission-sdk-23>:只在运行 API 23+ 的设备上生效
1
2
3
<!-- 只在 Android 6+ 设备上声明 "请求安装包" 权限 -->
<uses-permission-sdk-23
android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
实战案例
案例一:Flutter 中处理相机权限
场景:在 Flutter 应用中使用 image_picker 插件拍照,需要处理不同 Android 版本的权限差异。
Dart 代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'package:image_picker/image_picker.dart';
import 'package:permission_handler/permission_handler.dart';
Future<void> takePhoto() async {
// 在 Android 13+ 上只需要相机权限,不需要存储权限
if (await Permission.camera.request().isGranted) {
final image = await ImagePicker().pickImage(source: ImageSource.camera);
// 处理图片
} else {
// 权限被拒绝
if (await Permission.camera.isPermanentlyDenied) {
// 用户选择了"不再询问",引导到设置页面
openAppSettings();
} else {
showSnackBar('需要相机权限才能拍照');
}
}
}
AndroidManifest 配置:
1
2
3
4
5
6
7
8
9
10
<!-- 相机权限(所有版本都需要) -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 存储权限(只在 Android 12 及以下需要) -->
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<!-- Android 13+ 细粒度媒体权限 -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
案例二:React Native 中处理后台定位
场景:在 RN 应用中实现后台位置跟踪功能。
原生 Android 端(MainActivity.java):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Override
public void onRequestPermissionsResult(
int requestCode,
String[] permissions,
int[] grantResults
) {
super.onRequestPermissionsResult(
requestCode, permissions, grantResults
);
// 处理后台定位权限的特殊逻辑
if (requestCode == LOCATION_PERMISSION_CODE) {
if (grantResults.length > 0 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 还需要单独请求后台定位权限
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
requestPermissions(
new String[]{
Manifest.permission.ACCESS_BACKGROUND_LOCATION
},
BACKGROUND_LOCATION_CODE
);
}
}
}
}
AndroidManifest 配置:
1
2
3
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
注意:后台定位在 Android 10+ 上需要单独申请,而且 Google Play 对上架应用使用后台定位有严格审核,必须证明应用的核心功能需要后台定位。
案例三:处理 Android 13 通知权限
场景:Flutter 应用的推送通知在 Android 13+ 上默认不会显示。
Flutter 端代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import 'package:permission_handler/permission_handler.dart';
Future<void> requestNotificationPermission() async {
if (Platform.isAndroid) {
// Android 13+ 需要运行时申请通知权限
if (await Permission.notification.request().isGranted) {
// 可以发送通知
initializePushNotifications();
} else {
// 用户拒绝了通知权限
log('用户拒绝了通知权限');
}
} else {
// Android 12 及以下自动授予通知权限
initializePushNotifications();
}
}
案例四:鸿蒙兼容层的权限适配
场景:HarmonyOS 应用通过 Android 兼容层运行,需要同时适配两套权限体系。
关键适配点:
- 鸿蒙权限模型与 Android 不完全一致,需要在兼容层做映射
- 部分权限在鸿蒙上可能不存在(如
QUERY_ALL_PACKAGES) - 运行时权限的弹窗样式和交互逻辑不同
建议做法:在代码层做好版本检测,根据运行平台的 API 级别选择合适的权限处理策略。
常见问题
Q1: “Permission Denial” 类崩溃
症状:调用相机等敏感 API 时崩溃,Logcat 显示 Permission Denial。
原因:没有在 AndroidManifest.xml 中声明所需的权限,或者运行时没有正确处理权限申请。
解决:
- 检查
AndroidManifest.xml中是否有对应的uses-permission - 确认运行时已经调用了
requestPermissions - 检查合并后的清单文件(
build/intermediates/merged_manifests/)确认插件声明的权限已正确合并
Q2: 用户拒绝权限后应用不应该崩溃
最佳实践:
- “优雅降级”(Graceful Degradation):权限被拒后,应用应提示用户替代方案
- 不要无限弹窗请求权限
- 如果用户选择了”不再询问”,应引导用户到系统设置中手动授权
Q3: Android 13 上推送通知不显示
原因:Android 13 引入 POST_NOTIFICATIONS 权限,需要运行时申请。
解决:
- 在应用启动时或需要推送功能时,弹出通知权限请求
- 如果用户拒绝了第三方推送,改用应用内消息或通知渠道
- 引导用户到系统设置中开启通知权限
测试技巧:使用 adb 命令模拟权限状态:
1
2
3
4
5
# 授予通知权限
adb shell pm grant com.example.myapp android.permission.POST_NOTIFICATIONS
# 撤销通知权限
adb shell pm revoke com.example.myapp android.permission.POST_NOTIFICATIONS
Q4: 跨平台插件之间的权限冲突
场景:同时使用 image_picker 和 camera 两个插件,它们在清单文件中都声明了相机权限,但某些配置不同。
解决:
- 检查合并后的清单文件,确认没有权限相关的冲突
- 使用
tools:replace或tools:remove解决冲突 - 确保两个插件的版本兼容
Q5: 权限请求对话框不弹出
可能原因:
android:exported属性未正确设置- Activity 的配置有问题
- 在 Fragment 中请求权限但使用的是 Fragment 的
requestPermissions而不是 Activity 的 - 系统权限已被手动设置为”不再询问”
排查步骤:
- 使用
adb shell dumpsys package com.example.myapp查看权限状态 - 检查
PermissionController应用是否正常 - 在设置中清除应用的缓存数据
总结
Android 权限系统历经多次迭代,从简单粗暴的”全有或全无”模型演变为今天精细化的”按需授权”模型。对于跨平台开发者来说,权限管理的难度在于:
- 碎片化:需要同时处理从 Android 6 到 Android 14 的多种权限模型
- 抽象层不完善:Flutter/RN 的权限插件封装并不总是覆盖所有边界情况
- 用户体验要求:不恰当的权限请求时机和方式会导致用户拒绝,甚至卸载应用
核心应对策略是:
- 分层处理:在原生层确保
AndroidManifest.xml声明正确,在跨平台层封装完整的权限申请逻辑 - 版本感知:使用
Build.VERSION.SDK_INT判断运行版本,分版本处理权限逻辑 - 优雅降级:始终假设权限可能被拒绝,做好无权限状态下的兜底方案
- 测试覆盖:在不同 Android 版本上全面测试权限相关功能
记住一个简单的原则:用户永远是对的。如果用户拒绝了某个权限,应用应该优雅处理,而不是崩溃或反复弹窗。毕竟,在 App Store 评分中,权限请求骚扰是导致差评的常见原因之一。