定位权限这块我从入行到现在踩的坑比写过的业务代码还多。早些年做一个打卡类应用测试机上跑得好好的用户一反馈才发现后台跑十分钟就被系统掐了后来又做物流轨迹上报前台定位丝滑锁屏之后直接失联。这些问题的根子都不在业务逻辑而在权限申请、定位模式选择、后台存活策略这三件事上没吃透。这篇就把 Android 定位权限从基础配置到后台持续定位的完整链路拆开讲一遍包括权限分级、申请时机、前台服务绑定、厂商保活差异、耗电与精度的取舍以及实测中那些文档里不会写的坑。适合正在做地图、打卡、轨迹、附近推荐、运动记录这类功能的开发者也适合被后台定位折磨过、想一次性把方案理顺的人。1. 定位权限到底分几层别再用一套代码打天下很多人写定位权限习惯性就是requestPermissions一把梭把ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION一起丢进去回调里判断granted就完事。这套写法在 Android 6 到 Android 9 上勉强能跑到了 Android 10 之后就开始出各种幺蛾子。根本原因是定位权限不是一个维度而是三个维度叠加精度维度、前台/后台维度、单次/持续维度。不理解这三层代码写得再漂亮也是碰运气。1.1 精度维度粗略与精确不是二选一而是递进关系ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION这两个权限新手最容易误解成随便选一个就行。实际上它们是递进关系申请了FINE就自动隐含了COARSE的能力但反过来不成立。系统在弹窗上给用户的选项也不一样粗略定位只给大致位置精确定位才会给精确位置的开关。实测下来有个细节值得注意在 Android 12 及以上用户在系统设置里可以单独把精确位置关掉只保留大致位置。这时候你申请的是FINE但checkSelfPermission返回的可能是COARSE已授权、FINE未授权。如果你代码里只判断FINE就会误判成用户拒绝了定位然后反复弹窗骚扰用户体验极差。正确的判断逻辑应该是分层的fun hasLocationPermission(context: Context): LocationPermissionLevel { val fine ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_FINE_LOCATION ) PackageManager.PERMISSION_GRANTED val coarse ContextCompat.checkSelfPermission( context, Manifest.permission.ACCESS_COARSE_LOCATION ) PackageManager.PERMISSION_GRANTED return when { fine - LocationPermissionLevel.PRECISE coarse - LocationPermissionLevel.APPROXIMATE else - LocationPermissionLevel.NONE } }这样你的业务就能根据精度等级降级处理精确模式走 GPS 混合定位粗略模式走网络定位两者都不给才提示用户去设置页。1.2 前台与后台Android 10 之后的分水岭ACCESS_BACKGROUND_LOCATION是 Android 10API 29引入的这个权限的出现直接把后台定位的难度拉高了一个档次。在这之前只要拿到FINE应用退到后台照样能拿位置在这之后后台定位必须单独申请ACCESS_BACKGROUND_LOCATION而且申请方式和前台权限完全不同。这里有个非常关键的坑后台定位权限不能和前台定位权限一起申请。如果你在同一个requestPermissions数组里同时放FINE和BACKGROUND_LOCATION在 Android 11 及以上系统会直接忽略后台权限的申请用户看到的弹窗只有前台定位。正确做法是分两步先申请前台定位拿到之后再单独申请后台定位。而且后台定位的授权弹窗在 Android 11 之后变成了跳转到系统设置页用户需要手动选择始终允许。这个流程没法用代码绕过只能引导。我一般会在申请前用一个自定义说明页告诉用户接下来会跳到系统设置请选择始终允许把预期管理做好通过率能明显提升。1.3 单次授权Android 11 的仅此一次Android 11 引入了单次授权机制用户在弹窗上可以选择仅这一次。这个选项对开发者来说是个隐形炸弹用户选了之后你的应用在前台能正常定位但一旦退到后台权限就被系统自动回收定位直接失效。判断单次授权不能用checkSelfPermission因为它返回的是GRANTED。正确方式是结合shouldShowRequestPermissionRationale和实际定位结果来判断或者在 Android 11 及以上用ActivityCompat.shouldShowRequestPermissionRationale配合权限使用记录。更稳妥的做法是每次进入需要后台定位的场景前先做一次轻量的定位探测如果发现拿不到位置且权限状态异常就重新引导用户授权。下面这张表把三个维度的权限关系理清楚方便对照权限引入版本授权方式典型场景ACCESS_COARSE_LOCATIONAPI 1弹窗城市级天气、粗略推荐ACCESS_FINE_LOCATIONAPI 1弹窗导航、打卡、轨迹ACCESS_BACKGROUND_LOCATIONAPI 29跳设置页后台轨迹、地理围栏单次授权API 30弹窗选项临时查附近2. 从零配置Manifest 声明与运行时申请的完整链路权限配置这件事看起来是体力活但顺序错了后面全是坑。我见过太多项目在 Manifest 里把三个定位权限一股脑写上运行时又按错误顺序申请结果在 Android 12 上直接崩。这一节把配置和申请的完整链路按顺序捋一遍。2.1 Manifest 声明哪些必须写哪些写了反而有害基础声明是这样的uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.ACCESS_BACKGROUND_LOCATION /但这里有个容易被忽略的点如果你在 Manifest 里声明了ACCESS_BACKGROUND_LOCATION但没有在运行时申请Google Play 上架审核时会被标记为声明了后台定位但未使用可能被拒。所以声明要和实际使用严格对应用不到就别写。另外ACCESS_FINE_LOCATION和ACCESS_COARSE_LOCATION建议都声明。虽然FINE隐含COARSE但显式声明COARSE能让系统在用户只给粗略权限时正常降级避免出现权限已给但定位失败的诡异情况。还有一个隐藏项如果你的应用需要在前台服务里持续定位Android 14API 34开始要求声明前台服务类型service android:name.LocationService android:foregroundServiceTypelocation android:exportedfalse /这个foregroundServiceType不写在 Android 14 上启动前台服务会直接抛MissingForegroundServiceTypeException。这是很多老项目升级 targetSdk 后突然崩溃的原因。2.2 运行时申请分步走的正确姿势前面说过后台权限不能和前台一起申请具体代码结构应该是这样的// 第一步申请前台定位 private val foregroundLauncher registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { result - val fine result[Manifest.permission.ACCESS_FINE_LOCATION] true val coarse result[Manifest.permission.ACCESS_COARSE_LOCATION] true if (fine || coarse) { // 前台权限拿到根据业务决定是否申请后台 if (needBackground) requestBackgroundLocation() } else { // 用户拒绝走降级或引导 } } // 第二步申请后台定位Android 10 private val backgroundLauncher registerForActivityResult( ActivityResultContracts.RequestPermission() ) { granted - if (granted) startLocationService() else showBackgroundGuide() }注意RequestMultiplePermissions和RequestPermission的区别后台权限单独用单权限申请这样在 Android 11 上才会正确跳转到设置页。2.3 申请时机的选择为什么一进页面就弹是最差策略很多应用一进首页就弹定位权限用户还没搞明白你要干嘛拒绝率极高。我的经验是在用户触发具体功能时再申请。比如用户点了打卡按钮这时候弹权限用户心理预期是我要打卡需要定位通过率能高出一大截。更进一步申请前加一个自定义的说明弹窗用一句话讲清楚为什么需要定位和定位数据怎么用。这个说明弹窗不是系统弹窗是你自己画的 Dialog内容要具体比如用于记录你的打卡位置数据仅保存在本地而不是空泛的为了更好的服务。实测数据上加了说明弹窗之后首次授权通过率大概能从 50% 出头提升到 70% 以上。这个投入产出比非常高值得每个做定位的应用都加上。3. 后台持续定位前台服务是唯一正解但细节决定成败后台定位这件事Android 的官方态度很明确想持续拿位置就必须用前台服务并且显示常驻通知。想绕过这个限制的各种黑科技在 Android 8 之后基本都被堵死了。所以正确的思路不是找漏洞而是把前台服务这套机制用到位。3.1 前台服务的启动与通知配置前台服务的核心是startForeground必须在服务启动后 5 秒内调用否则系统会抛ANR并杀掉服务。代码骨架class LocationService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val notification buildNotification() if (Build.VERSION.SDK_INT Build.VERSION_CODES.Q) { startForeground( NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION ) } else { startForeground(NOTIFICATION_ID, notification) } startLocationUpdates() return START_STICKY } }Android 10 及以上startForeground要带上服务类型FOREGROUND_SERVICE_TYPE_LOCATION这个类型要和 Manifest 里声明的foregroundServiceType对应否则会抛异常。通知这块有个细节Android 8 之后通知必须走 NotificationChannel而且渠道的重要性等级会影响通知的显示方式。定位服务的通知建议用IMPORTANCE_LOW这样不会响铃打扰用户但通知栏能看到。用IMPORTANCE_MIN的话通知会被折叠用户看不到反而容易被系统判定为隐藏前台服务而杀进程。3.2 定位请求参数精度、频率、耗电的三角平衡后台定位最怕的就是耗电用户一看电池排行里你的应用排第一反手就是一个卸载。所以参数配置要克制。LocationRequest的关键参数val request LocationRequest.Builder(Priority.PRIORITY_BALANCED_POWER_ACCURACY, 30000L) .setMinUpdateIntervalMillis(15000L) .setMaxUpdateDelayMillis(60000L) .setMinUpdateDistanceMeters(10f) .build()这里几个参数的含义和取舍PRIORITY_BALANCED_POWER_ACCURACY平衡模式走网络基站少量 GPS精度百米级耗电低。后台轨迹类场景够用。如果要做运动轨迹记录才需要PRIORITY_HIGH_ACCURACY。interval设 30 秒后台没必要秒级更新30 秒一次对轨迹类应用足够耗电能降一个数量级。minUpdateDistanceMeters设 10 米用户没动就不上报进一步省电。我做过对比测试同样跑一小时后台定位HIGH_ACCURACY 5 秒间隔的耗电大概是BALANCED 30 秒间隔的 4 到 5 倍。所以参数一定要按业务实际需求来别默认用高精度。3.3 厂商保活差异国内环境的现实问题国内 Android 生态有个绕不开的现实各家厂商都有自己的后台管理策略前台服务在部分机型上依然会被杀。这不是代码问题是系统层面的限制。能做的优化有几点第一引导用户把你的应用加入厂商的白名单。主流厂商都有自启动管理后台运行管理这类设置可以在应用内做一个引导页用Intent跳转到对应的设置页。不同厂商的跳转 Intent 不一样需要做机型适配这块网上有整理好的对照表可以直接参考。第二用WorkManager做兜底。前台服务被杀之后WorkManager的周期任务还能在系统允许的窗口期唤醒应用补一次定位。虽然不能做到实时但能保证轨迹不断档太严重。第三START_STICKY返回值配合onTaskRemoved处理。用户从最近任务划掉应用时START_STICKY会让系统在资源允许时重启服务。但要注意Android 8 之后后台启动服务有限制重启的服务如果没及时startForeground还是会被杀。4. 权限被拒之后降级策略与用户引导的实战设计权限被拒是常态不是异常。把被拒之后的流程设计好比纠结怎么提高授权率更实际。这一节讲降级和引导的具体做法。4.1 三种拒绝状态的区分与应对用户的拒绝分三种首次拒绝、拒绝且不再询问、永久拒绝系统设置里关掉。这三种状态的处理方式完全不同但很多代码只判断了一个granted布尔值导致体验割裂。区分方式fun getPermissionState(activity: Activity, permission: String): PermissionState { val granted ContextCompat.checkSelfPermission( activity, permission ) PackageManager.PERMISSION_GRANTED if (granted) return PermissionState.GRANTED val shouldShow ActivityCompat.shouldShowRequestPermissionRationale( activity, permission ) return if (shouldShow) PermissionState.DENIED_CAN_ASK else PermissionState.DENIED_PERMANENT }shouldShowRequestPermissionRationale返回true说明用户拒绝过但没勾选不再询问可以再次申请返回false且权限未授予说明是永久拒绝只能引导去设置页。对应策略状态处理方式GRANTED正常走定位流程DENIED_CAN_ASK弹说明弹窗再次申请DENIED_PERMANENT引导跳系统设置页4.2 降级方案没有精确定位时业务怎么跑不是所有功能都强依赖精确定位。设计降级方案时先问自己这个功能没有定位能不能用能用到什么程度以打卡为例降级路径可以是精确定位 - 粗略定位提示位置可能不准确- 手动选择位置 - 纯手动输入地址。每一级降级都要给用户明确的提示而不是静默失败。再以附近推荐为例精确定位 - 粗略定位推荐范围扩大- 手动选择城市 - 展示热门内容。这样即使权限被拒核心功能依然可用用户不会因为一个权限问题直接流失。4.3 跳转系统设置页的正确写法引导用户去设置页Intent 要写得兼容fun openAppSettings(context: Context) { val intent Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data Uri.fromParts(package, context.packageName, null) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } context.startActivity(intent) }注意FLAG_ACTIVITY_NEW_TASK在非 Activity 上下文里启动必须加否则会崩。另外从设置页返回后要在onResume里重新检查权限状态因为用户可能在里面改了授权。5. 实测踩坑记录那些文档不会告诉你的问题这一节是我这些年实际踩过的坑按问题现象、排查过程、根因、解决方案的结构写方便对照复现。5.1 锁屏后定位停止不是权限问题是 Doze 模式现象应用在前台定位正常锁屏几分钟后位置不再更新解锁后恢复。排查过程一开始怀疑是权限被回收检查checkSelfPermission一直是GRANTED。后来用adb shell dumpsys deviceidle查看设备状态发现进入了 Doze 模式。Doze 是 Android 6 引入的省电机制设备静止且锁屏一段时间后进入会限制网络和后台任务。根因Doze 模式下普通的后台任务被暂停但前台服务不受影响。问题在于我的定位服务当时没有正确用前台服务而是用普通 Service 定时器所以被 Doze 掐了。解决方案改用前台服务并且把定位请求的setWaitForAccurateLocation(false)让系统在 Doze 窗口期也能拿到位置。另外WorkManager的周期任务在 Doze 下会被延迟不能作为实时定位的依赖。5.2 Android 12 上定位返回 null精度开关被单独关闭现象Android 12 设备上权限显示已授予但getLastKnownLocation一直返回 nullrequestLocationUpdates也不回调。排查过程打印权限状态发现FINE是GRANTED但定位就是没数据。后来在系统设置里发现用户把精确位置开关关掉了只保留了大致位置。这时候系统实际给的是粗略定位但我的代码用的是PRIORITY_HIGH_ACCURACY系统认为精度不满足直接不回调。根因Android 12 的精确位置开关是独立于权限的权限授予不代表精确位置可用。解决方案定位前先检查精度等级如果只有粗略权限就把LocationRequest的优先级降到PRIORITY_BALANCED_POWER_ACCURACY或PRIORITY_LOW_POWER这样系统才会正常回调。5.3 前台服务通知不显示渠道被用户关闭现象前台服务启动了但通知栏看不到通知服务运行一段时间后被系统杀掉。排查过程startForeground没抛异常但通知就是不显示。检查发现用户把该通知渠道关掉了。Android 8 之后用户可以单独关闭某个通知渠道渠道关闭后startForeground依然成功但通知不显示系统会认为前台服务没有有效通知进而杀进程。根因通知渠道被关闭前台服务的可见性不满足系统要求。解决方案启动服务前检查通知渠道是否被关闭如果关闭了引导用户去开启或者换一个渠道。检查方式val channel notificationManager.getNotificationChannel(CHANNEL_ID) if (channel ! null channel.importance NotificationManager.IMPORTANCE_NONE) { // 渠道被关闭引导用户开启 }5.4 后台定位权限申请无反应申请顺序错了现象Android 11 设备上调用后台定位申请没有任何弹窗回调直接返回拒绝。排查过程单独申请后台权限代码没问题但就是不弹。后来发现是申请时机不对——在onCreate里直接申请此时前台权限还没拿到。Android 11 要求申请后台定位前必须先有前台定位权限否则系统直接拒绝且不弹窗。根因后台定位权限的申请有前置条件必须先持有前台定位权限。解决方案严格按先前台、后后台的顺序申请并且在前台权限的回调里再触发后台申请。5.5 定位漂移严重坐标系与滤波没做现象定位点在地图上跳来跳去静止时也在漂移。排查过程原始 GPS 数据本身就有噪声加上网络定位和 GPS 定位切换时坐标系不一致导致漂移。根因没有做坐标转换和滤波。解决方案国内地图要用 GCJ-02 坐标系GPS 原始数据是 WGS-84需要转换。另外加一个简单的卡尔曼滤波或滑动平均把异常点过滤掉。实测加滤波之后静止漂移能从几十米降到几米。6. 精度、耗电与合规上线前必须过的几道关功能跑通只是第一步上线前还有几道关要过尤其是涉及用户位置这种敏感数据。6.1 耗电优化别让定位成为电池杀手除了前面说的参数配置还有几个优化点用FusedLocationProviderClient而不是LocationManager前者是 Google 的融合定位会自动在 GPS、WiFi、基站之间切换省电且精度好。用Geofencing替代持续定位。如果业务是进入某区域触发用地理围栏比持续定位省电得多系统会在进出围栏时唤醒应用。定位数据本地缓存避免重复请求。同一位置短时间内多次请求直接用缓存。我做过一个对比持续定位一小时耗电约 8% 到 12%地理围栏方案在同样场景下耗电不到 2%。所以能用围栏就别用持续定位。6.2 隐私合规权限说明与数据使用现在各大应用市场对定位权限的审核都很严几个硬性要求申请权限前必须有明确的用途说明且说明要和实际使用一致。后台定位必须有显著的通知不能静默采集。用户拒绝权限后不能影响其他无关功能的使用。隐私政策里要写清楚位置数据的收集、使用、存储、共享方式。我见过不少应用因为申请定位权限但实际未使用或后台定位无通知被下架这些都是可以避免的。6.3 测试清单上线前逐项过一遍测试项测试方法预期结果首次授权全新安装触发定位功能弹窗正常说明清晰拒绝后再次申请首次拒绝再次触发弹说明弹窗可再次申请永久拒绝勾选不再询问后拒绝引导跳设置页后台定位锁屏后观察持续更新通知可见单次授权选择仅这一次前台正常后台失效有提示精确位置关闭设置里关精确位置降级到粗略定位不崩溃通知渠道关闭关闭定位通知渠道有引导服务不被杀低电量模式开启省电模式定位降频不崩溃这份清单基本覆盖了定位功能的主要异常路径上线前跑一遍能挡掉大部分线上问题。定位权限这套东西说到底就是理解系统规则顺着规则设计。想绕过限制的取巧方案最后都会在某个系统版本上翻车。把前台服务、分步申请、降级策略这三件事做扎实后台持续定位的稳定性就能到 95% 以上。剩下的 5%交给厂商白名单引导和 WorkManager 兜底。我个人在实际项目里的体会是与其花时间研究怎么保活不如把参数调优和降级体验做好用户感知到的稳定性提升更明显。