文章

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

Android 权限分普通/危险/特殊三级,危险权限必须运行时申请,Android 13+ 又拆出通知与细粒度媒体权限; 跨平台应用"权限被拒崩溃""收不到推送""后台定位拿不到",根因都在权限时序与版本差异。

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

一句话概括

Android 权限是应用访问敏感能力(相机、定位、通讯录、存储)的”门禁”:系统按风险把权限分成普通(装好即给)、危险(要弹窗问用户)、特殊(要跳设置页手动开)三级。

对跨平台开发者,权限是最容易”看起来配了却用不了”的地带——危险权限光在 AndroidManifest 声明不够,还得运行时弹窗;Android 6 引入运行时模型、Android 13 又把通知和媒体权限拆细。配错 = 崩溃或功能静默失效,配对 = 用户顺滑授权。面试里它几乎必考,因为坑又多又真实。

一句话结论:声明只是”申请资格”,运行时授权才是”拿到钥匙”;而 Android 13+ 之后,钥匙还被拆成一把把小的。

核心知识点

1. 三级权限模型

1
2
3
4
5
6
7
8
9
10
<!-- 普通权限:声明即授予,用户无感知 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<!-- 危险权限:还要运行时弹窗 -->
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />

<!-- 特殊权限:无法用标准弹窗,必须跳系统设置页 -->
<!-- SYSTEM_ALERT_WINDOW / WRITE_SETTINGS / MANAGE_EXTERNAL_STORAGE -->
级别代表权限授予方式
普通(Normal)INTERNET、VIBRATE、ACCESS_NETWORK_STATE、WAKE_LOCK安装/声明即自动给
危险(Dangerous)CAMERA、READ_CONTACTS、ACCESS_FINE_LOCATION、RECORD_AUDIO运行时弹窗问用户
特殊(Special)SYSTEM_ALERT_WINDOW、WRITE_SETTINGS、MANAGE_EXTERNAL_STORAGE跳设置页手动开

危险权限按”权限组”归类(如 CAMERA、LOCATION、MICROPHONE),组主要影响系统设置里怎么展示给用户,以及安装期(Android 6 之前)的分组授权。

2. 运行时申请:现代写法(Activity Result API)

onRequestPermissionsResult 已废弃,现代用 registerForActivityResult:

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
class MainActivity : AppCompatActivity() {
    // 单个权限
    private val reqCamera = registerForActivityResult(
        ActivityResultContracts.RequestPermission()
    ) { granted ->
        if (granted) openCamera() else handleDenied()
    }

    // 多个权限一起申请
    private val reqMedia = registerForActivityResult(
        ActivityResultContracts.RequestMultiplePermissions()
    ) { result ->
        val images = result[READ_MEDIA_IMAGES] == true
        val video  = result[READ_MEDIA_VIDEO]  == true
        // ...
    }

    fun askCamera() {
        when {
            // ✅ 已授权:直接用
            ContextCompat.checkSelfPermission(this, CAMERA) == PERMISSION_GRANTED ->
                openCamera()
            // ✅ 之前拒绝过且还能再问:先解释为什么需要
            shouldShowRequestPermissionRationale(CAMERA) ->
                showRationale { reqCamera.launch(CAMERA) }
            // ✅ 首次 / 用户未"永久拒绝":直接弹窗
            else -> reqCamera.launch(CAMERA)
        }
    }
}

shouldShowRequestPermissionRationale 的三种含义:

场景返回值该做什么
从未请求过false直接弹窗
拒绝过但没”不再询问”true先展示说明,再弹窗
拒绝过且”不再询问”(API 30+ 连拒两次)false引导去系统设置页

3. 权限组自动授权:Android 11 已改

早年(Android 6~10)有个”便利”:同组里只要有一个权限已授予,再请求同组另一个会静默自动给,不弹窗。这点 Android 11(API 30)起已废弃——现在请求任意危险权限都会正常弹窗,组内不再自动连带授权。

1
2
Android 6~10:组内已授权 → 新权限静默通过
Android 11+ :每次请求都弹窗,组内不再"搭便车"

另外两个 API 30 的关键变化:

  • 一次性授权:位置/相机/麦克风弹窗出现”仅此一次”,离开前台即失效。
  • 自动重置:应用几个月没被打开,系统自动收回其运行时权限,下次启动要重新申请——所以永远假设权限可能被收回,用前先 check。

4. Android 13(API 33):通知 + 细粒度媒体

这是近年改动最大的一版,跨平台推送”突然收不到”多半因为它。

1
2
3
4
5
6
7
8
9
10
<!-- 通知权限:必须运行时申请,否则收不到推送 -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<!-- 媒体被拆成三个,替代老的 READ_EXTERNAL_STORAGE -->
<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" />

<!-- 附近 Wi-Fi 设备:不再需要位置权限 -->
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />

向后兼容写法(老版本用存储权限,Android 13+ 用媒体权限):

1
2
3
4
<uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />

5. 后台定位:危险权限里的”特殊户”

ACCESS_BACKGROUND_LOCATION 虽属危险权限,但获取流程更苛刻:

1
2
3
4
5
6
7
8
// 1) 必须先拿到前台位置(精/粗)
reqFine.launch(ACCESS_FINE_LOCATION)
// 2) 再单独申请后台位置;Android 10+ 不能靠标准弹窗拿到,需引导去设置
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS,
        Uri.parse("package:$packageName"))
    // 实际常跳 Settings > 应用 > 权限 > 位置 > 始终允许
}

⚠️ Google Play 对使用后台定位的应用严审,必须证明核心功能依赖它,否则上架被拒。

6. 声明技巧:maxSdkVersion 与 uses-permission-sdk-23

1
2
3
4
5
6
7
8
<!-- 只在 Android 12 及以下声明老存储权限 -->
<uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />

<!-- 只在 API 23+ 设备声明(低版本根本没这权限概念) -->
<uses-permission-sdk-23
    android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
  • maxSdkVersion 让某权限只在指定版本以下出现,避免在高版本”挂着不用”惹审计怀疑。
  • uses-permission-sdk-23 让权限仅对运行时模型设备(API 23+)生效。

其实你每天都在用

  • 拍照(image_picker):要 CAMERA;Android 12- 还需 READ_EXTERNAL_STORAGE,Android 13+ 换成 READ_MEDIA_IMAGES。
  • 定位导航:前台用 ACCESS_FINE_LOCATION/ACCESS_COARSE_LOCATION,后台还得单独申请 ACCESS_BACKGROUND_LOCATION 并过商店审核。
  • 收推送(Android 13+):不申请 POST_NOTIFICATIONS 就完全收不到通知,且用户可在设置里单独关掉。
  • 选相册/文件:Android 13+ 按图片/视频/音频三类细粒度授权,不再是一个”存储”大权限。
  • 录音/语音:RECORD_AUDIO 危险权限,运行时申请;麦克风灯会亮。
  • 悬浮窗(悬浮球/悬浮播放):SYSTEM_ALERT_WINDOW 是特殊权限,必须跳设置页手动开。
  • 读通讯录/日历:READ_CONTACTS/READ_CALENDAR 危险权限,用户随时可收回。
  • 权限被拒时:别死循环弹窗,做”优雅降级”(如提示手动开启或换替代方案)。

常见误解(FAQ)

❌ 误区一:”清单里写了 uses-permission 就能直接用”

只对普通权限成立。INTERNET 这类声明即给;但 CAMERA、READ_CONTACTS 等危险权限只是”拿到申请资格”,真正能用还要在运行时弹窗、用户点同意(Android 6+)。漏了运行时这一步,调用即 SecurityException / Permission Denial 崩溃。

❌ 误区二:”用户同意了相机,同组麦克风也自动有了”

这是 Android 11 之前的旧行为,现已废弃。API 30+ 每个危险权限都要单独弹窗、单独授权,组内不再”搭便车”。所以申请多个权限时,该用 RequestMultiplePermissions 一个个问清楚,别指望授权一个带一串。

❌ 误区三:”用户拒绝一次,以后就再也弹不出来了”

不是一次,是连续拒绝两次(API 30+)才被视为”不再询问”,此后系统不再弹窗、直接走”引导去设置”。首次拒绝、或只拒绝过一次,shouldShowRequestPermissionRationale 仍会返回 true,你还能解释后再次请求。

❌ 误区四:”通知权限一直都有,推送照常”

Android 13(API 33)起,POST_NOTIFICATIONS 变成需要运行时申请的危险权限。没申请或超过半数用户拒绝,你的推送就石沉大海——这也是很多 App 升级后”推送到达率腰斩”的真凶。而且它能被用户在设置里单独关,要处理”未授权”分支。

❌ 误区五:”后台定位和前台定位一起申请就行”

不行。ACCESS_BACKGROUND_LOCATION 必须在前台位置已授予之后单独申请,且 Android 10+ 无法靠标准弹窗拿到——通常要引导用户去系统设置把位置设为”始终允许”。加上 Play 对后台定位严审,滥用会被拒。普通场景用前台定位就够了。

一句话总结

Android 权限是一条”声明 → 运行时授权 → 按版本细分”的链路:普通权限装好即给,危险权限要弹窗、且 Android 11+ 逐个体授权、Android 13+ 又把通知与媒体拆细,特殊权限还得跳设置页;记住”用前先 check、被拒要降级、版本要分流”,跨平台权限问题就从玄学变成可预期的标准流程。

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