文章

Android 权限系统详解——跨平台开发者的权限管理指南

深入解析 Android 权限系统的分类、运行时申请流程及各版本变化,帮助 Flutter/RN/鸿蒙开发者在跨平台场景中正确处理权限问题。

Android 权限系统详解——跨平台开发者的权限管理指南

一句话概括

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/鸿蒙跨平台项目中,权限管理面临独特的挑战:

  1. 多层抽象:跨平台框架对原生权限 API 做了封装,但封装并不总是完美的
  2. 插件依赖复杂:一个简单的”拍照”功能,可能依赖于相机权限、存储权限,甚至麦克风权限
  3. 版本碎片化:用户设备可能运行着 Android 6 ~ Android 14 的任何版本,权限行为差异巨大
  4. 权限请求时机:Flutter 的 UI 线程与原生权限回调之间的生命周期协调容易出问题

根据 2024 年 Google Play Console 的数据,权限相关问题导致的应用拒绝率高达 28%。而在跨平台应用中,由于权限处理不当引发的运行时崩溃占整体崩溃的 12% 左右。

Android 权限模型的核心原则

Android 权限系统遵循三个核心原则:

  1. 最小权限原则:应用只应请求完成任务所需的最少权限
  2. 用户可知原则:任何敏感操作都应让用户知晓并同意
  3. 用途透明原则:应用应在请求权限时说明为什么要使用该权限

核心知识点拆解

权限的三级分类

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)

涉及用户隐私或可能造成安全风险的权限,需要运行时动态申请。

危险权限按功能分组(权限组概念稍后详述):

权限组权限列表说明
CALENDARREAD_CALENDARWRITE_CALENDAR读取/写入日历
CAMERACAMERA相机
CONTACTSREAD_CONTACTSWRITE_CONTACTSGET_ACCOUNTS联系人
LOCATIONACCESS_FINE_LOCATIONACCESS_COARSE_LOCATIONACCESS_BACKGROUND_LOCATION位置信息
MICROPHONERECORD_AUDIO麦克风录音
PHONEREAD_PHONE_STATECALL_PHONEREAD_CALL_LOGWRITE_CALL_LOGADD_VOICEMAILUSE_SIPPROCESS_OUTGOING_CALLS电话功能
SENSORSBODY_SENSORS传感器
SMSSEND_SMSRECEIVE_SMSREAD_SMSRECEIVE_WAP_PUSHRECEIVE_MMS短信
STORAGEREAD_EXTERNAL_STORAGEWRITE_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:requiredandroid: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 兼容层运行,需要同时适配两套权限体系。

关键适配点:

  1. 鸿蒙权限模型与 Android 不完全一致,需要在兼容层做映射
  2. 部分权限在鸿蒙上可能不存在(如 QUERY_ALL_PACKAGES
  3. 运行时权限的弹窗样式和交互逻辑不同

建议做法:在代码层做好版本检测,根据运行平台的 API 级别选择合适的权限处理策略。

常见问题

Q1: “Permission Denial” 类崩溃

症状:调用相机等敏感 API 时崩溃,Logcat 显示 Permission Denial

原因:没有在 AndroidManifest.xml 中声明所需的权限,或者运行时没有正确处理权限申请。

解决

  1. 检查 AndroidManifest.xml 中是否有对应的 uses-permission
  2. 确认运行时已经调用了 requestPermissions
  3. 检查合并后的清单文件(build/intermediates/merged_manifests/)确认插件声明的权限已正确合并

Q2: 用户拒绝权限后应用不应该崩溃

最佳实践

  • “优雅降级”(Graceful Degradation):权限被拒后,应用应提示用户替代方案
  • 不要无限弹窗请求权限
  • 如果用户选择了”不再询问”,应引导用户到系统设置中手动授权

Q3: Android 13 上推送通知不显示

原因:Android 13 引入 POST_NOTIFICATIONS 权限,需要运行时申请。

解决

  1. 在应用启动时或需要推送功能时,弹出通知权限请求
  2. 如果用户拒绝了第三方推送,改用应用内消息或通知渠道
  3. 引导用户到系统设置中开启通知权限

测试技巧:使用 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_pickercamera 两个插件,它们在清单文件中都声明了相机权限,但某些配置不同。

解决

  1. 检查合并后的清单文件,确认没有权限相关的冲突
  2. 使用 tools:replacetools:remove 解决冲突
  3. 确保两个插件的版本兼容

Q5: 权限请求对话框不弹出

可能原因

  1. android:exported 属性未正确设置
  2. Activity 的配置有问题
  3. 在 Fragment 中请求权限但使用的是 Fragment 的 requestPermissions 而不是 Activity 的
  4. 系统权限已被手动设置为”不再询问”

排查步骤

  1. 使用 adb shell dumpsys package com.example.myapp 查看权限状态
  2. 检查 PermissionController 应用是否正常
  3. 在设置中清除应用的缓存数据

总结

Android 权限系统历经多次迭代,从简单粗暴的”全有或全无”模型演变为今天精细化的”按需授权”模型。对于跨平台开发者来说,权限管理的难度在于:

  1. 碎片化:需要同时处理从 Android 6 到 Android 14 的多种权限模型
  2. 抽象层不完善:Flutter/RN 的权限插件封装并不总是覆盖所有边界情况
  3. 用户体验要求:不恰当的权限请求时机和方式会导致用户拒绝,甚至卸载应用

核心应对策略是:

  1. 分层处理:在原生层确保 AndroidManifest.xml 声明正确,在跨平台层封装完整的权限申请逻辑
  2. 版本感知:使用 Build.VERSION.SDK_INT 判断运行版本,分版本处理权限逻辑
  3. 优雅降级:始终假设权限可能被拒绝,做好无权限状态下的兜底方案
  4. 测试覆盖:在不同 Android 版本上全面测试权限相关功能

记住一个简单的原则:用户永远是对的。如果用户拒绝了某个权限,应用应该优雅处理,而不是崩溃或反复弹窗。毕竟,在 App Store 评分中,权限请求骚扰是导致差评的常见原因之一。

本文由作者按照 CC BY 4.0 进行授权