4、权限管理:Android 6.0+运行时权限、ACCESS_FINE_LOCATION与ACCESS_COARSE_LOCATION、权限请求最佳实践

做位置开发,绕不开的就是权限。说实话,我见过太多新手在权限这步翻车了。明明代码写得没问题,一运行就崩溃,或者定位死活拿不到数据。嗯,今天我们就来彻底搞定它。

4.1 为什么要有运行时权限?

在 Android 6.0(API 23)之前,你只要在 AndroidManifest.xml 里声明了权限,安装时用户同意一次,App 就能随便用。听起来挺省事对吧?但问题来了——用户根本不知道你什么时候在用他的位置。

我记得有个项目,用户反馈说「你们的 App 为什么半夜还在定位?」。查了半天,原来是后台一个 Service 在持续请求位置更新。用户很生气,后果很严重。所以 Google 在 6.0 引入了运行时权限机制:危险权限不仅要声明,还要在运行时弹窗让用户确认

核心原则:权限请求必须与用户操作直接关联。不要一启动 App 就弹权限框,用户会反感的。

4.2 ACCESS_FINE_LOCATION 与 ACCESS_COARSE_LOCATION

这两个权限是位置服务的入口。我简单解释一下区别:

权限名 精度 典型来源 使用场景
ACCESS_FINE_LOCATION 高精度(米级) GPS + 网络 + 基站 导航、运动轨迹、附近搜索
ACCESS_COARSE_LOCATION 低精度(百米级) Wi-Fi + 基站 天气、新闻、粗略位置展示

你想想看,如果你的 App 只是显示个天气,其实 COARSE 就够了。但很多开发者图省事,直接申请 FINE。用户一看「这个天气 App 要精确定位?」,直接拒绝。何必呢?

我的建议:只申请你真正需要的权限。如果只需要粗略位置,就别碰 FINE。这样用户更容易接受,审核也更容易通过。

4.3 运行时权限请求流程

说白了,就是三步走:检查 → 请求 → 处理回调。但细节里全是坑。

4.3.1 检查权限是否已授予

if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)
    == PackageManager.PERMISSION_GRANTED) {
    // 权限已授予,直接开始定位
    startLocationUpdates()
} else {
    // 权限未授予,需要请求
    requestLocationPermission()
}

这里有个细节:checkSelfPermission 必须在主线程调用。我曾经在子线程里调过,结果返回永远是 DENIED,排查了半天才发现是线程问题。

4.3.2 请求权限

private fun requestLocationPermission() {
    // 判断是否需要展示解释说明
    if (ActivityCompat.shouldShowRequestPermissionRationale(this,
            Manifest.permission.ACCESS_FINE_LOCATION)) {
        // 用户之前拒绝过,需要解释为什么需要这个权限
        showPermissionRationaleDialog()
    } else {
        // 直接请求
        ActivityCompat.requestPermissions(this,
            arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
            LOCATION_PERMISSION_REQUEST_CODE)
    }
}

shouldShowRequestPermissionRationale 这个方法很有意思。它返回 true 表示用户之前拒绝过但没勾选「不再询问」。这时候你应该弹个对话框解释一下。如果返回 false,有两种情况:要么是第一次请求,要么是用户勾选了「不再询问」。后者就比较棘手了。

注意:如果用户勾选了「不再询问」,你再调用 requestPermissions 是不会弹窗的。系统会直接回调 onRequestPermissionsResult 并返回 DENIED。这时候你只能引导用户去设置里手动开启。

4.3.3 处理回调结果

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (requestCode == LOCATION_PERMISSION_REQUEST_CODE) {
        if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            // 用户同意了,开始定位
            startLocationUpdates()
        } else {
            // 用户拒绝了
            showPermissionDeniedFeedback()
        }
    }
}

嗯,这里要注意:grantResults 数组的顺序和 permissions 数组是一一对应的。如果你一次请求了多个权限,一定要按索引匹配。

4.4 权限请求最佳实践

做了这么多年 Android,我总结了几条铁律:

  1. 时机要对:在用户真正需要位置功能时才请求。比如用户点击「开始导航」按钮时,而不是 App 启动时。
  2. 解释要清:如果用户拒绝过,用 shouldShowRequestPermissionRationale 检测到后,弹一个友好的对话框说明原因。
  3. 拒绝要处理:用户拒绝后,不要反复弹窗。可以给个提示,或者降级使用 COARSE 权限。
  4. 区分权限:如果同时需要 FINECOARSE,先请求 COARSE,用户同意后再请求 FINE。这样用户体验更好。
  5. 考虑 Android 10+:从 Android 10 开始,后台位置权限需要单独申请 ACCESS_BACKGROUND_LOCATION。这个权限的弹窗和前台权限是分开的。

我曾经在一个外卖 App 里踩过坑:用户在前台允许了位置权限,但后台定位一直失败。查了半天才发现 Android 10 之后后台权限要单独申请。从那以后,我每次做位置功能都会先检查 targetSdkVersion。

4.5 知识体系图

下面这张图把权限管理的核心逻辑串起来了。你可以对照着梳理自己的代码:

Android 位置权限请求流程 开始定位功能 检查权限是否已授予? 已授予 开始定位更新 未授予 shouldShowRationale? true 显示解释对话框 false requestPermissions() onRequestPermissionsResult() 同意 开始定位更新 拒绝 提示/降级处理

4.6 常见问题与避坑

  • 权限弹窗不显示? 检查一下 targetSdkVersion 是否大于等于 23。如果小于 23,系统会沿用旧版安装时授权机制。
  • 用户拒绝后反复弹窗? 别这么做。用户会烦,甚至可能卸载你的 App。记录一下拒绝次数,超过两次就引导去设置。
  • 同时请求多个权限? 建议分开请求。一次只弹一个权限框,用户更容易接受。我见过一个 App 一次弹了 5 个权限,用户直接拒绝所有。
  • Android 11 的权限自动重置? 如果用户几个月没用你的 App,系统会自动撤销权限。记得在 onResume 里重新检查权限状态。

重要提醒:千万不要在 onCreate 里直接请求权限。这时候 Activity 还没完全初始化,弹窗可能会出问题。我习惯在 onResume 里检查,或者等用户触发某个操作后再请求。

好了,权限管理这块就讲到这里。记住一句话:尊重用户,权限才能拿到。你越是彬彬有礼地解释为什么需要位置,用户越愿意给你授权。反过来,一上来就硬要,用户只会点「拒绝」。


公众号:蓝海资料掘金营,微信deep3321