☰
Android简单GPS实时定位:LocationManager从权限到精度过滤全解析
2026/10/13 1:26:30 网站建设 项目流程

106.android简单的GPS实时定位:从零手写一套不迷路的定位逻辑

写LBS类的App时,“拿到当前经纬度并实时更新”永远是最先要面对的基础功能。这条“106.android简单的GPS实时定位”看着像是某个任务清单里的编号,但只要拆开看,它包含的东西并不简单:权限申请、定位开关检测、LocationManager的provider选择、回调频率控制、精度过滤、还有一大堆真机上才见得到的奇奇怪怪的问题。这篇文章我就把这套流程从头到尾过一遍,直接给到能跑的代码,同时把每一步背后的“为什么”讲清楚。适合刚接触Android定位开发的同学,也适合后端转客户端想快速上手的人。

1. 先把方案定下来:原生LocationManager还是第三方SDK

1.1 “简单实时定位”到底难在哪

先说结论:Android自带的LocationManager完全可以实现简单的实时定位,很多场景根本用不着集成第三方SDK。但“实时”这两个字是坑的源头——GPS冷启动可能需要十几秒甚至更久,Wi-Fi定位精度飘忽不定,基站定位在室内还能用、在郊区就可能给你扔到几公里外。真实产品里用户打开地图类App,秒级就看到自己的位置,那是系统层面做了复杂的融合定位:并行拉取GPS、网络、传感器数据,再根据精度、耗电、场景做取舍。原生LocationManager也能做到类似效果,只是很多参数需要你自己配。

所谓“简单”,指的是不引入额外依赖、不开后台服务、就一个页面里周期性拿坐标。这种需求用原生API反而是最优解:代码量小、没有包体积负担、也不会被各种SDK初始化流程困住。等业务真正需要连续轨迹、地理围栏、逆地理编码的时候,再认真评估第三方方案也不迟。

1.2 原生方案和厂商SDK怎么选

在定位这条路上,业界基本是两种流派:一种是把android.location.LocationManager吃透,手动管理provider;另一种是集成厂商的定位SDK,让SDK内部帮你切换GPS与网络定位。我整理了一张对比表,你选型的时候可以直接参考:

对比项原生LocationManager第三方定位SDK
接入成本零依赖,直接调用需要申请Key、配置依赖、做初始化
GPS+网络自动切换需要自己判断,手动指定provider内部自动融合定位
定位精度与速度依赖系统底层,冷启动较慢有服务端辅助,通常首次定位更快
省电策略参数自由度高,但要自己调内置省电逻辑,按场景自动调整
逆地理/围栏等能力需要自己对接地图服务通常自带配套能力
包体积与合规成本最小会引入一定体积与隐私声明工作

从我的经验看,如果你的目标就是做个Demo、做个小工具、或者App里的定位只是辅助功能,直接原生LocationManager完全够用,省心省力。如果产品是工具类App且定位是核心卖点,那可以在原生流程验证通过后,再考虑接入厂商SDK优化体验。

提示:不管原生还是SDK,最终你拿到的都是Location对象,里面的latitude、longitude、accuracy、time这些字段才是判断数据是否可用的关键。这一步无论哪套方案都绕不开。

1.3 定位原理速览:用开灯找人的例子讲透GPS

GPS定位的原理用生活类比最好理解:你和几个朋友在一个大操场上,每个人手里有一个能报时间的秒表,但表彼此有微小误差。你突然喊一声,大家记录听到声音的时间差,你根据时间差就能算出与每个人的距离,然后画三个圆,交点就是你的位置。卫星定位也一样,卫星不断播报自身位置和精确时间,手机收到信号后测算距离,至少四颗卫星就能解算出经纬度和高度。

但卫星信号怕遮挡。在室内、高楼峡谷、隧道里,信号衰减非常厉害,这时候就需要“辅助定位”出场了。手机通过基站或Wi-Fi热点快速获得一个粗略坐标,再结合GPS卫星数据去缩短定位时间,这就是AGPS。理解这一层,你才知道为什么LocationManager里会有GPS_PROVIDER和NETWORK_PROVIDER两种provider:前者精度高但慢且耗电,后者速度尚可但精度差。简单实时定位的正确姿势,是两者配合,而不是只死磕GPS。

2. 动手前先解决两个前置:权限和定位开关

2.1 Android权限演进:不同版本要命的差别

写定位代码第一个坑就是权限。Android的定位权限经历了好几轮变化,你的targetSdkVersion不同,处理逻辑完全不同:

系统版本相关权限关键变化
Android 6.0以下ACCESS_FINE_LOCATION/ACCESS_COARSE_LOCATION安装时授权,不弹窗
Android 6.0-11上述权限 + 运行时动态申请运行时弹窗,用户可拒绝
Android 12+精确/模糊权限分离新增ACCESS_COARSE_LOCATION的独立模糊定位,用户可只给模糊权限
Android 12+(部分厂商ROM后台定位权限后台获取位置需要单独申请ACCESS_BACKGROUND_LOCATION,并且和前台定位分开展示
Android 13+无大变化但通知权限与定位联动,体验上更严格

清单文件里至少要有这两行,级别取其一即可:

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

ACCESS_FINE_LOCATION包含COARSE的能力,但在Android 12及以上,如果targetSdk>=31且只申请了FINE,系统仍然会同时展示精确与模糊两个选项,用户可以只给模糊。这就意味着你的代码必须能处理accuracy很差的数据。

2.2 用户没开GPS怎么办:检测与跳转引导

权限给了也不代表定位能用,因为系统总开关可能是关闭的。LocationManager提供了isProviderEnabled,用法很直接:

val locationManager = getSystemService(LOCATION_SERVICE) as LocationManager if (!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) { // 提示用户去设置页打开定位服务 val intent = Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS) startActivity(intent) }

这里有个界面体验细节:不要一进页面就弹跳转,太粗暴。我一般先弹一个Dialog,说明“需要开启定位服务才能获取位置”,用户点了“去开启”再跳设置页,返回后若还没有开,再提示一次。如果每次onResume都弹,用户会被搞疯。

2.3 封装一个可复用的权限请求流程

运行时权限请求在Activity里写原生回调略显啰嗦,但为了不引入额外库,可以封装成一个简单方法。核心思路是用ActivityResultContracts.RequestMultiplePermissions这个官方API:

class MainActivity : AppCompatActivity() { private val locationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result -> val fineGranted = result[Manifest.permission.ACCESS_FINE_LOCATION] ?: false val coarseGranted = result[Manifest.permission.ACCESS_COARSE_LOCATION] ?: false if (fineGranted || coarseGranted) { startLocationUpdates() } else { // 用户拒绝了权限,建议引导去应用设置页 showPermissionDeniedDialog() } } private fun checkAndRequestLocationPermission() { val fineGranted = ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_FINE_LOCATION ) == PackageManager.PERMISSION_GRANTED val coarseGranted = ContextCompat.checkSelfPermission( this, Manifest.permission.ACCESS_COARSE_LOCATION ) == PackageManager.PERMISSION_GRANTED if (fineGranted || coarseGranted) { startLocationUpdates() } else { locationPermissionLauncher.launch( arrayOf( Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION ) ) } } }

注意代码里只申请了前台定位权限。如果业务需要在后台持续定位,Android后台权限还涉及ACCESS_BACKGROUND_LOCATION以及前台服务的配合,这在后面第5章再展开。

3. 核心实现:最简单的实时定位循环

3.1 配置三个关键参数:provider、minTime、minDistance

LocationManager.requestLocationUpdates是核心入口,参数决定了实时性到底有多“实时”:

locationManager.requestLocationUpdates( LocationManager.GPS_PROVIDER, // 定位源 1000L, // 最小时间间隔,单位毫秒 1f, // 最小距离变化,单位米 locationListener )

minTime和minDistance很多人搞混。实际上系统会在“时间间隔达到”或“位置位移达到”任一条件满足时回调一次。比如你设置1000ms + 0米,那就近似每秒回调一次;如果你设置1000ms + 100米,那么即使过了1秒,如果手机没移动100米,也不会回调,这样能有效避免静止时的无效刷新。

实操中最常见的问题是:想做成“秒级刷新”,把minTime设成100ms,但实际回调频率并达不到预期。因为Android系统内部对GPS定位频率有下限限制,不同机型差异非常大,主动设极小的数值既费电又不一定能生效。更稳妥的做法是1000ms起步,同时配合NETWORK_PROVIDER作为替补。这一块我后面在第4章会细说真实机型的表现。

3.2 onLocationChanged回调的完整实现

一个最小可用的定位页面,代码可以这样写。这里我用Kotlin,逻辑都在Activity里,结构最直观:

class LocationActivity : AppCompatActivity() { private lateinit var locationManager: LocationManager private lateinit var binding: ActivityLocationBinding private val locationListener = object : LocationListener { override fun onLocationChanged(location: Location) { // 这里就是每次拿到新坐标的回调 val lat = location.latitude val lng = location.longitude val accuracy = location.accuracy val provider = location.provider binding.tvLocation.text = "纬度: $lat\n经度: $lng\n精度: ${accuracy}m\n来源: $provider" } override fun onProviderEnabled(provider: String) {} override fun onProviderDisabled(provider: String) {} } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityLocationBinding.inflate(layoutInflater) setContentView(binding.root) locationManager = getSystemService(LOCATION_SERVICE) as LocationManager checkAndRequestLocationPermission() } private fun startLocationUpdates() { startUpdateFromProvider(LocationManager.GPS_PROVIDER) startUpdateFromProvider(LocationManager.NETWORK_PROVIDER) } private fun startUpdateFromProvider(provider: String) { if (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) { val lastLocation = locationManager.getLastKnownLocation(provider) if (lastLocation != null) { locationListener.onLocationChanged(lastLocation) } locationManager.requestLocationUpdates(provider, 1000L, 0f, locationListener) } } override fun onResume() { super.onResume() // 页面可见时,若权限已授予且定位已开启,重新打开监听 if (hasLocationPermission() && isLocationEnabled()) { startLocationUpdates() } } override fun onPause() { super.onPause() // 页面不可见时及时移除,避免一直定在后台耗电 locationManager.removeUpdates(locationListener) } }

这段代码有几个关键点:

  • 同时注册GPS_PROVIDER和NETWORK_PROVIDER,室内室外都能拿到数据。
  • 先用getLastKnownLocation拿一次历史缓存位置,秒开页面时至少有个“上次位置”可看,避免白屏等GPS冷启动。
  • onPause里必须removeUpdates,否则页面翻到后台还一直请求定位,电池消耗会成问题。
  • onResume里重新开启监听,是因为用户可能刚从设置页返回,此时定位开关已打开,重新注册才能继续拿坐标。

3.3 拿到坐标之后,先别急着用:精度与新鲜度判断

GPS回调进来的Location不一定可信。冷启动时,经常先回调一个精度几百米的值,然后过几秒才慢慢收敛到几十米。如果你拿到第一个垃圾坐标就往地图上打点,用户位置会“瞬移”,体验非常差。

成熟的写法是加一道过滤逻辑:先看时间戳,再横向比较精度。时间戳代表这个定位是不是“新”的,精度代表它可信不可信。

private fun isLocationUsable(location: Location): Boolean { val timeDiff = System.currentTimeMillis() - location.time // 超过10秒的位置视为过期,不能直接使用 if (timeDiff > 10_000L) { return false } // 横向精度超过100米,代表性太差 if (location.accuracy > 100f) { return false } return true }

location.accuracy的单位是米,数值越小精度越高。户外GPS正常情况下能到5~20米,室内网络定位则常常是30~100米。实际需求不同,阈值线要自己划。做一个外卖配送App,50米内都能接受;做道路导航,就必须10米以内。

另外说一个高频误解:拿到多个provider的定位后,location.getProvider()可以帮你判断坐标来源,但不同provider的回调顺序没有保证。简单方案是维护一个“当前最优位置”的变量,每次回调进来后比较精度,精度更小才替换,界面只更新这个最优解。这样能自动把GPS和网络定位结果合并成一条更稳的位置流。

3.4 实时性提升技巧:让首次定位更快

“实时定位”的实时感,很大程度上取决于第一次拿到有效坐标的速度。这里有三招可以组合使用,亲测有效:

第一招是先拿缓存。getLastKnownLocation无论在哪个provider都可能存在,拿来先展示,界面不空等。但要注意缓存时间,超过10分钟的基本无用。

第二招是监听provider切换。GPS和网络定位之间动态切换,比如GPS一直没回调,就只监听NETWORK_PROVIDER的结果,等精度提升后再切换回GPS。原生代码里没有现成的“融合”逻辑,但你可以用上面说的“最优位置”变量实现一个轻量融合。

第三招是主动请求一次单次定位。Android 13及以上可以用getCurrentLocation一次性获取一个位置,比requestLocationUpdates更轻量,适合不需要连续刷新的场景。API长这样:

val currentLocationRequest = CurrentLocationRequest.Builder() .setDurationInMillis(3000) .setQualities(LocationRequest.QUALITY_BALANCED) .build() locationManager.getCurrentLocation(currentLocationRequest, null, executor) { location -> if (location != null) { // 拿到一次定位,刷新UI } }

这种方式只拿一次,不持续回调,不会产生高频电池消耗,适合“用户点一下刷新位置”的交互。

4. 这些坑我基本都踩过:定位失败与“定位不更新”排查

4.1 权限给了、定位也开了,为什么就是不回调

这种事情在真机上遇到的第一反应是怀疑手机坏了,其实多数是下面这几种情况:

症状可能原因解决办法
onLocationChanged一直不触发没有调用requestLocationUpdates,或触发了onPause时被移除了检查生命周期,确认定位注册在onResume调用
只在屋里,GPS没有卫星信号GPS在室内基本不可用同时监听NETWORK_PROVIDER,或改用厂商融合SDK
模拟器上永远不回调模拟器默认不模拟GPS坐标在扩展控制里手动设置经纬度
权限明明授权了,但还是失败targetSdkVersion过高且未处理Android 12的精确/模糊权限分离回看第2章权限流程,确认申请了FINE_LOCATION
系统弹了授权框但瞬间消失厂商ROM的权限白名单机制建议用户去设置里手动允许定位

还有一个经常被忽略的点:requestLocationUpdates的第一参数如果传入的是一个不存在的provider,比如某些定制ROM把GPS provider名字改了,那你注册了个寂寞。所以代码里可以先getAllProviders()打印一下,看看当前设备到底有哪些provider。

4.2 minTime设了1秒,更新频率却不对

很多人在追求“实时”时会把间隔设得非常小,但实测会发现Android并没有严格按这个值来。原因有两个:一是系统内部对定位请求有个最小间隔限制,低版本和高版本不一致;二是部分厂商的省电策略会把高频请求合并或降低优先级。

我的个人建议是:普通UI展示用1秒~2秒间隔就足够了,如果要做连续轨迹记录,可以保持每秒采集一次,但必须配合前台服务,否则App一退后台采集逻辑就被干掉了。如果你需要轨迹很密集,比如骑行配速、跑步App,那最好用第三方SDK,或者在后端做轨迹抽稀,而不是靠客户端疯狂请求。

4.3 室内定位“飘”得离谱,应该怎么处理

网络定位在室内能给个大概,但“飘”是常态,因为Wi-Fi热点列表一变、基站信号反射一多,坐标就会跳。处理办法有两层:

第一层是在客户端做简单滤波。最基础的可以用“移动平均”:维护一个最近5个有效坐标的队列,取平均值作为展示位置。不过这个方法在用户快速移动时会拉长轨迹,仅适合步行等慢速场景。

第二层是结合业务场景判断。比如用户过一会儿再回来,坐标保持连续、逐渐偏离,这时可以做聚类判断,把偏离过大的点丢弃。更复杂的卡尔曼滤波适合轨迹平滑,但代码量明显上升,看你的精力取舍。对“106.简单的GPS实时定位”这个需求来说,移动平均加阈值判断已经完全够用了。

4.4 给自己留条后路:模拟定位与真机测试的两种方式

开发中经常要验证定位回调,最顺手的方式是Android模拟器。直接把经纬度塞进去,回调马上就有反应,适合调试UI逻辑:

  • SDK自带模拟器:在模拟器扩展控制里点“Location”,手动填经纬度,或者用GPS replay模拟移动轨迹。
  • 真机调试:开发者选项里开启“选择模拟位置信息应用”,安装一个模拟定位应用,再把模拟位置打开,你的App就能收到模拟出来的坐标。

这里有个注意事项:模拟定位会挂在isFromMockProvider()状态下面。如果生产环境不想让用户伪造位置,可以在定位回调里判断:

if (location.isFromMockProvider) { // 可能是模拟位置,做风险处理 }

从Android 12开始这个API换成location.isMock,高版本的样例里两个都要兼容。这些测试方式只建议在开发环境用,正式版别留后门。

4.5 问题排查速查表

症状优先排查项次优先排查项
页面刷新出来的是null系统定位总开关未开未申请权限或未授权
只有网络定位,没有GPS在室内,搜索不到卫星GPS_PROVIDER注册失败
精度永远是几十米只监听了网络定位没有过滤accuracy,显示了差坐标
退到后台不更新Activity被销毁或监听被移除需要前台服务支撑
杀进程后定位失效生命周期设计问题需要START_STICKY服务的持久化设计

5. 再进一步:实时定位的工程化建议

5.1 长时间定位必须用前台服务,不然就是死循环

如果你只是页面停留时定位,第3章的写法没问题。但如果你要把定位做成“App在后台继续上传位置”,需要更复杂的服务设计:目前所有主流系统都对后台定位有限制,Android如果不在前台且没有前台服务,系统会很快冻结你的进程。解决办法是启动一个带通知的前台服务,在服务里持有LocationManager并持续监听,同时把最新坐标写入数据库或者上报。前台服务通知栏会一直显示“正在定位”,这也是用户知情权的一部分,任何正规产品都应该这样设计。

5.2 省电策略:按场景动态切换刷新频率

“高实时”和“省电”天然矛盾。做过几个真实项目后我总结出一套切换策略:用户停在页面关注位置变化时,用1秒间隔;页面切后台但业务需要记录轨迹时,用10秒间隔;用户彻底锁屏时,就该改为被动更新——只在位移超过一定距离时才上报一次。这些策略用同一个LocationListener,只是根据场景动态requestLocationUpdates调整参数即可。不需要为每种频率建多个监听器,那样管理起来反而容易出问题。

5.3 别只存经纬度:坐标展示与距离计算的常见坑

拿到经纬度后,千万别直接拼字符串就完事。很多业务需要计算两点距离,这时用Location.distanceBetween或Location.distanceTo即可,别自己写弧度转换甚至直接用勾股定理按经纬度差算距离,那样算出来的结果在南北纬或东经西经附近会差很远。另外,如果要把经纬度展示成“地址”,那就要逆地理编码,这类能力原生并不提供,一般接入地图SDK或走云服务接口。为保持“简单”,正文这里不做展开。


这个需求从表面看极简单,真正做完会发现核心价值不在于调通那几行API,而在于理解定位数据的产生链路、学会判断数据质量、懂得在不同系统版本和机型上做规避。我个人在实际项目里的做法是:先用原生方案搭一个最小功能闭环,等业务模型稳定以后,再决定需不需要引入第三方SDK去优化体验。如果你也正在做类似功能,建议先把我第3章的代码跑起来,然后把4.1的排查表贴在项目群里,很多以前重复解释的问题,一次就能说清楚。最后再分享一个小技巧:调试定位流程时,花点时间把不同provider的坐标与精度同时打印在屏幕上,你就能直观看到GPS到底什么时候可用、什么时候该切网络定位,这个可视化调试方法,比看日志高效太多了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询