做安卓定位功能,最尴尬的时刻往往不是写完代码跑不起来,而是App装到真机上,地图一打开,蓝色圆点半天不出来,或者出来了一个位置,但过几分钟就漂到隔壁街去了。我最初做这个东西的时候,就是因为只盯着定位API本身,结果在权限、线程、坐标系、SDK版本这些地方反复折腾,浪费了不少时间。现在回过头看,GPS实时定位这个需求本身不算难,但真正把它做到“能用”“用起来不丢位置”,有一堆细节是在文档里写不清楚的。这篇文章就围绕“Android简单GPS实时定位”这件事,把我实际跑通一个项目时用到的方案、踩过的坑、以及每一步为什么这么做的思考完整写一遍,希望能帮到正在做类似功能的朋友。
1. 动手之前:你要的“实时”到底是什么时延
很多第一次做定位的开发者,看到“实时定位”这四个字,第一反应就是要把GPS数据每一帧都刷出来,最好100毫秒刷新一次,跟玩赛车游戏似的。但真到了实战,如果一开始不把“实时”的标准定清楚,后面性能和耗电都会很难受。
1.1 GPS定位的时延其实由三层决定
第一层是卫星信号本身。GPS模块从开机到锁定卫星、计算出有效经纬度,这个过程叫冷启动,通常要几十秒,有的老旧设备在空旷地带甚至要一两分钟。如果手机开了网络辅助定位,也就是通过基站或者Wi-Fi快速拿到粗略位置再结合卫星修正,冷启动可以缩短到几秒。但注意,网络辅助只能加速首次定位,它本身给的不是高精度GPS位置。
第二层是系统回调的频率。Android系统不是GPS芯片每算出一个位置就马上通知App一次,而是根据App设置的更新间隔来分发。你在代码里告诉系统“我每隔3秒要一次位置”,系统到点之后才会把最新的经纬度回调给你。这个间隔不能无限制缩小,因为GPS芯片一直高功耗运行,耗电会明显上升,系统也可能出于省电策略故意压缩回调频率。
第三层是App自己处理数据的时间。把经纬度从回调线程转到主线程、刷新UI、往地图上画marker,这个时间通常在几十毫秒到几百毫秒之间。对于人眼观察的定位场景,比如外卖配送员的轨迹、人员巡逻路径、车辆行驶记录,1到5秒更新一次完全够用了。做得更细腻一点,可以让UI每秒刷新一次,但GPS回调间隔设置在3到5秒,中间用上一次的位置做平滑插值,观感就很顺滑。
1.2 选对API:直接用LocationManager还是走Google Play服务
Android系统最早提供的定位接口是LocationManager,用起来很直接,拿到LocationListener就开始监听。这个方案的好处是纯系统API,不需要额外引入依赖库,代码量也不大。但它有几个明显的短板:手动要自己处理定位权限检查、高版本系统对后台定位限制苛刻、不同厂商ROM对它的调用频率和精度干预差异很大。
我现在做新项目基本都用FusedLocationProviderClient,也就是融合定位客户端。名字叫Fused,是因为它不只是用GPS,而是把GPS、Wi-Fi、基站、传感器这些信号全部融合起来,由系统统一判断当前哪种方式最可靠,然后返回一个综合出来的位置。这样做的好处很明显:室内能快速定位,室外精度也不差,而且API封装得比较干净,一句getLastLocation()拿最近位置,一句requestLocationUpdates()开始持续更新,代码整体好维护。
如果你只需要一个简单的定位Demo,用LocationManager确实够,但要是项目有后续扩展的可能,比如以后要加轨迹记录、地理围栏、后台定位,建议一开始就上FusedLocationProviderClient,免得后面重写。
2. 权限和基础环境:80%的新手问题都出在这一步
我一直有个观点:安卓定位功能写起来不难,但如果你把线上崩溃日志翻出来,会发现很大一部分都是权限问题——要么清单文件漏写了权限,要么是用户在弹窗里点了拒绝,要么是Android 10以上的后台定位权限没有申请。权限这块没搞定,后面代码再漂亮都白搭。
2.1 Manifest清单要声明哪些权限
在AndroidManifest.xml里,至少需要声明两个与定位相关的权限:ACCESS_COARSE_LOCATION和ACCESS_FINE_LOCATION。前者是粗略定位,精度在小几百米到几公里之间,基于基站和Wi-Fi;后者是精确定位,允许使用GPS卫星信号拿到米级精度。只申请粗略权限是拿不到GPS定位的,要做实时定位,必须把精确定位权限一起加上。
如果你的应用需要在后台持续定位,比如退到桌面之后还要记录轨迹,那还需要额外申请ACCESS_BACKGROUND_LOCATION。这个权限在Android 10(API 29)及以上是单独列出来的,用户要在系统设置里单独授权,弹窗里首次申请时并不会直接出现“允许使用位置信息”的选项,而是要跳转到设置页单独开。很多开发者在这里踩坑,以为前台定位权限拿到了,后台定位就能自动跟上,其实不是这么回事。
清单文件里加权限的代码是这样的:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> </manifest>2.2 运行时权限申请:6.0之后绕不开的流程
从Android 6.0开始,定位权限不再在安装时直接授权,必须在App运行过程中动态弹窗申请。这个流程写起来不算复杂,但有三类情况必须处理清楚:首次授权、拒绝之后再次申请、勾选了“不再询问”。
简单说,用一个ActivityCompat.requestPermissions方法把权限弹窗拉起来,然后在onRequestPermissionsResult回调里根据用户的选择做分支。如果用户拒绝了但没勾“不再询问”,下次进入定位页面可以再弹一次;如果用户勾了“不再询问”,就只能引导用户去系统设置页手动开启。这个引导逻辑如果做不好,用户拒绝一次之后App就永远定不了位,体验会很难受。
申请权限的推荐写法:
private fun requestLocationPermission() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED ) { ActivityCompat.requestPermissions( this, arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), PERMISSION_REQUEST_CODE ) } else { startLocationUpdates() } }2.3 模拟器和真机的测试差异
调试定位功能,最好一开始就在真机上验证。模拟器虽然有“模拟位置信息”的功能,可以在Extended Controls面板里手动踩一个经纬度,但它模拟出来的数据和真实GPS芯片的行为差异很大,尤其体现在回调频率、精度波动、漂移现象上。模拟器适合演示流程,不适合调试定位质量。
我自己习惯的做法是,先在办公室窗边做初次定位,确认权限流程没问题,再拿着手机走到户外测试卫星信号。测试的时候打开系统的“开发者选项”,把“允许模拟位置”关掉,不然App可能拿到的是模拟数据而不是真实GPS位置。
3. 核心定位逻辑:从一行配置到经纬度上屏
权限和环境准备到位之后,核心的定位逻辑反而是最简单的一环。整个流程可以拆成四个步骤:拿到定位客户端、配置定位请求参数、注册位置回调、启动更新。每一步都有值得说清楚的细节。
3.1 初始化定位客户端与请求参数
使用FusedLocationProviderClient,需要先通过LocationServices.getFusedLocationProviderClient(context)拿到客户端实例。这个操作不需要单独初始化,也不耗电,真正决定行为和耗电的是接下来要创建的LocationRequest。
LocationRequest里有几个关键的参数需要理解:interval是期望的更新间隔,单位毫秒;fastestInterval是允许的最快更新间隔,系统在某些情况下可能回调得更频繁,但不会超过这个值;priority是定位优先级,PRIORITY_HIGH_ACCURACY表示优先使用GPS保证精度,PRIORITY_BALANCED_POWER_ACCURACY表示在精度和耗电之间取平衡,适合不需要太高精度的场景。
常见的配置项对比:
| 参数 | 作用 | 我常用的配置 |
|---|---|---|
| interval | 期望更新间隔 | 3000ms |
| fastestInterval | 最快允许间隔 | 1000ms |
| priority | 精度优先还是省电优先 | HIGH_ACCURACY |
| setMaxWaitTime | 批量等待时间 | 5000ms |
Kotlin的初始化代码:
val fusedClient = LocationServices.getFusedLocationProviderClient(this) val locationRequest = LocationRequest.create().apply { interval = 3000 fastestInterval = 1000 priority = LocationRequest.PRIORITY_HIGH_ACCURACY }setMaxWaitTime这个参数新人可能容易忽视。它表示如果系统知道你在短时间内多次请求位置,可以合并成一次回调,最多可等待的时间。对实时刷新要求不高的场景,适当调大这个值可以省电。
3.2 位置回调:onLocationChanged里做了什么
requestLocationUpdates方法需要一个LocationCallback,核心回调是onLocationResult,携带的就是最新的一组位置数据。在回调里拿到经纬度,刷新UI,或者发送给服务端,基本就是这个函数发挥价值的地方。
需要注意线程问题。FusedLocationProviderClient默认回调在主线程,所以如果你在onLocationResult里直接做UI更新,是可以的;但如果做耗时操作,比如网络上传,需要自己切线程。我习惯用协程或Executor处理,避免阻塞界面。
一个我能直接跑通的最小实现:
private val locationCallback = object : LocationCallback() { override fun onLocationResult(locationResult: LocationResult) { val location = locationResult.lastLocation ?: return runOnUiThread { tvLatitude.text = "纬度: ${location.latitude}" tvLongitude.text = "经度: ${location.longitude}" tvAccuracy.text = "精度: ${location.accuracy}米" } } } fusedClient.requestLocationUpdates( locationRequest, locationCallback, Looper.getMainLooper() )Location对象里不只有经纬度,还有accuracy(水平精度,单位米)、speed(速度)、bearing(方向角)。做实时定位的时候,精度值比经纬度本身更有参考意义——如果精度值从5米飙到50米,说明当前环境卫星信号不好,UI上可以加一个状态提示,而不是盲目相信坐标。
3.3 停止更新与生命周期管理
实时定位功能做完了别忘了“关水龙头”。Activity销毁或者退到后台不再需要定位的时候,一定要调removeLocationUpdates把回调注销掉,不然手机会一直维持高功耗的GPS状态,耗电明显上升,系统还可能因为这个在用户不知情的情况下杀掉进程。
正确的生命周期管理:
override fun onDestroy() { super.onDestroy() fusedClient.removeLocationUpdates(locationCallback) }如果需要在后台持续定位,Activity的onDestroy里就不适合直接移除回调了,要转移到Service或前台服务里管理。关于后台定位的更多细节,我放到后面专门讲。
4. 地图上画实时位置:坐标系转换与定位图层
只有经纬度和文字刷新,还算不上真正意义上的“实时定位”。接入地图、把位置以圆点和轨迹的形式显示出来,才是这个功能最直观的呈现方式。
4.1 GPS坐标与地图坐标的纠偏问题
这里有一个很多新手完全不知道、但做定位迟早要踩的坑:GPS芯片输出的坐标是WGS-84坐标系,而国内主流地图服务商为了让地图数据符合测绘规范,使用的都是GCJ-02坐标系(俗称“火星坐标系”)。这两个坐标系之间的差异大约是几百米量级,不处理的话,你的定位点会跟地图上的实际位置明显偏移。
简单举个例子:GPS直接输出的某点经纬度,丢到国内地图上显示,位置可能往旁边偏了三百米。这时候不是定位模块出了问题,而是坐标系没转换。地图SDK通常提供了坐标转换方法,比如把GPS原始坐标传入转换接口,返回适合地图展示的坐标,再更新marker位置。
如果你是自己绘制地图、不依赖国内地图SDK,就需要自己写WGS-84转GCJ-02的算法,网上有公开的转换公式,不是秘密,照着实现就行。但如果你用的是主流地图平台的SDK,一般会内置转换接口,优先用官方方案。
4.2 集成地图定位图层的基础姿势
我最早接入地图定位时,最省事的做法是让地图SDK自己管理定位图层,也就是提供一个“显示定位圆点”的方法,SDK内部会动态更新marker位置,App只需要把定位开关打开。这种方式最快,但灵活性差,比如你不想让圆点在地图上自动跟随移动,而是想控制刷新节奏,就不太好办。
我后来更常用的做法是,把定位数据拿到手,自己控制marker的位置更新。大致流程是:
- 初始化地图View,设置好中心点和缩放级别。
- 拿到定位回调里的经纬度坐标,坐标系转换之后,用一个marker渲染到地图上。
- 每次回调更新时,调用marker的更新位置方法,必要时把地图镜头移过去。
这样做的优势是逻辑在自己手里,比如可以做一个“自动缩放开关”,默认不跟随镜头,点一下才把镜头切到当前位置。用户体验比地图SDK默认的全自动跟随要舒服很多。
一个示意性的核心更新逻辑:
fun updateMapLocation(latLng: LatLng) { if (currentMarker == null) { currentMarker = map.addMarker( MarkerOptions().position(latLng).title("当前位置") ) } else { currentMarker?.position = latLng } }4.3 地图跟随模式与多状态呈现
有些业务场景,比如配送、巡逻监控,不仅需要在地图上看到一个点,还需要看到历史轨迹线。这时候只更新marker还不够,还要维护一条轨迹的坐标集合,用地图的折线图层把历史点连起来。实时定位的“实时”在这种场景下,其实就变成了轨迹的及时追加。
我的策略是,维护一个List<LatLng>,每次拿到新坐标就add进去,然后用折线重绘。为了不让轨迹线无限膨胀,可以考虑只保留最近1000个点,超过之后用首尾截断的方式清掉旧数据。正北方向的方向角也可以用Location.bearing做一个箭头图标,旋转marker的角度,观感上会专业很多。
5. 真机上才暴露的坑:冷启动、室内漂移和后台进程
这部分是我想重点提醒后面做定位开发的人的内容。前期的代码运行在模拟器上往往一切顺利,真正拿到真机上跑,各种奇怪现象才会陆续出现。我在实际项目中遇到的这些问题,基本都有明确的应对思路。
5.1 首次定位为什么这么慢
有次我测试一个定位App,打开之后等了十几秒蓝色圆点还是不出来,差点以为代码写错了。后来我把系统日志打开,发现GPS已经拿到卫星信号,但第一次有效定位就是迟迟不来。这是因为GPS冷启动需要时间下载星历数据、捕获卫星。
解决这个问题的通用做法是开启网络辅助定位。就算App的主要目标是GPS实时定位,首次定位时也可以先通过基站或者Wi-Fi快速拿一个粗略位置把地图撑起来,等GPS准确数据回来之后再替换。很多定位SDK默认就带这个逻辑,如果你自己用原始API,可以检查一下系统设置里“提高定位准确性”这个开关,需要Wi-Fi和蓝牙扫描参与定位,用户体验会明显改善。
5.2 室内定位不准与漂移的处理
进到室内之后,GPS信号衰减很厉害,定位点会在几十米范围内乱跳,这就是漂移。实时定位项目如果不清洗数据,轨迹画出来会是一堆乱麻。
我踩了几次坑之后,摸索出一套简单的过滤策略:设置一个漂移阈值,比如当前位置和上一个可信位置之间的距离超过30米,且当前精度值大于25米,就认为这个点是漂移点,先不更新UI,等下一个点回来再看。用这个方法,即使偶尔丢了几个点,轨迹整体也会很干净。阈值要根据场景调整,户外开阔区域可以收紧到15米,室内或城区可以适当放宽。
计算距离可以用Location.distanceBetween,Android系统已经封装好了:
fun isDrift(prev: Location, cur: Location, threshold: Float): Boolean { val results = FloatArray(1) Location.distanceBetween( prev.latitude, prev.longitude, cur.latitude, cur.longitude, results ) return results[0] > threshold }5.3 后台定位与系统杀进程的博弈
很多做实时定位的项目,最终都要面对一个躲不开的问题:App退到后台之后,定位还能不能继续。Android系统为了省电和内存,会限制后台应用访问定位。直接的做法是开一个前台服务,带一条常驻通知,告诉系统“我这个App正在为用户提供定位服务,不要杀我”。前台服务在Android 8.0之后必须指定通知渠道,在Android 12之后新起的普通前台服务可能还有启动限制,需要适配。
如果你不需要持续后台定位,只是用户在App内看实时位置,就不用折腾前台服务。很多时候产品需求说“实时定位”,实际就是在前台页面刷新,搞清楚这一点,能帮你省下大量后台兼容性工作。
5.4 精度值才是定位质量的直观判断
开发定位功能时,工程师最关心的往往不是经纬度本身,而是accuracy这个字段。我自己调试时一定会把精度值显示在界面上,用来判断当前定位质量。精度值小于10米,说明GPS信号很好;10到30米,处于可接受范围;大于50米,基本可以判定为基站定位或室内环境,不适合做精细轨迹。
最终交付的时候,可以在界面上用不同颜色表达精度状态,比如绿色表示高精度,黄色表示中等精度,红色表示低精度。这个细节不需要额外代码成本,但对使用者的帮助非常明显。
6. 写在最后的实操总结
GPS实时定位这个功能,做一次之后再看任何地图平台的定位SDK,都会觉得思路是通用的:权限检查、定位请求配置、回调处理、坐标系转换、UI展示,核心逻辑跑不出这几步。新手最大的障碍往往不是代码没写好,而是忽略了权限和真机环境这些“看不见的坑”。
我个人比较推荐的做法是,第一版代码尽量简短,先用FusedLocationProviderClient跑通一个能显示经纬度和精度值的最小Demo,确认权限流程没有问题,再考虑接地图、画轨迹、做后台定位这些高级功能。这样每一步都有明确的验证点,出了问题也容易定位。
如果你后面准备把这个Demo扩展成正式项目,可以考虑把定位逻辑封装成一个独立的Manager类,对外暴露start、stop、setCallback这些方法,这样不管页面怎么换,定位核心只有一个入口点,后期维护会轻松很多。测试也记得多在几台不同品牌、系统版本的手机上跑跑,定位模块的兼容性问题,只有真机能告诉你答案。