ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Android手机定位源码解析:从LocationManager到地理围栏实战

2026/9/13 14:12:59 拓冰建站 浏览量
Android手机定位源码解析:从LocationManager到地理围栏实战 简介一份面向Android开发者的手机定位功能实现源码适合需要集成定位服务或学习定位原理的中高级开发者参考。压缩包大小约2.72MB核心代码围绕LocationManager、位置提供者、LocationListener等接口展开并包含运行时权限请求、单次与持续定位、最小更新距离与时间间隔设置等典型处理逻辑。目前已有203人学习下载可用于快速理解Android定位服务的调用流程与参数配置。源码覆盖GPS定位、网络定位、被动定位与地理围栏等模块同时展示了定位回调处理、异常场景应对以及与地图API结合的思路。通过分析源码的Activity、Service、BroadcastReceiver等组件结构开发者可以掌握事件监听、状态切换与后台定位服务的设计方法直接借鉴可运行的定位方案减少从零调试的时间成本。1. Android 手机定位源码从 LocationManager 到地理围栏的一次完整拆解如果你手头也有一份Android手机定位源码.zip打开后最该关心的不是那个 MainActivity 有多长而是它到底走的是系统原生LocationManager路线还是已经迁移到 Google Play 服务的FusedLocationProvider。这两条路决定了权限模型、省电表现、真机调试方式甚至 zip 里那几份 gradle 文件的复杂度。很多人在第一步就选错把网络定位和 GPS 定位写成互斥分支结果室内永远拿不到位置室外又慢得让人想摔机。这篇文章会以这套源码为线索把 Provider 选型、权限、单次与连续定位、地理围栏以及 adb 模拟位置验证讲透。适合正要把定位能力接进项目的 Android 开发也适合想读懂别人定位代码的维护者。2. LocationManager 的骨架Provider、Listener 与权限生命周期2.1 四种 Provider 的选用逻辑LocationManager是 Android 系统提供的位置服务入口源码里最常见的初始化写法是LocationManager locationManager (LocationManager) getSystemService(Context.LOCATION_SERVICE);拿到实例后系统会暴露四种 Provider它们的定位来源、精度和功耗差异很大Provider定位来源典型精度功耗适用场景GPS_PROVIDER卫星5-50 米高室外、车载、轨迹记录NETWORK_PROVIDERWi-Fi、基站20-500 米低室内、城市街道、快速定位PASSIVE_PROVIDER其他应用请求的定位结果不定极低被动接收不主动发起FUSED_PROVIDERGoogle Play 服务多源融合10-50 米中需要兼顾精度与功耗时在源码工程里你通常能看到一个getBestProvider()的调用它接收一个Criteria对象返回系统认为最合适的 Provider。常见的错误是拿到 GPS 就死等卫星完全不考虑用户此刻在室内。我的建议是把 GPS 和网络定位当作互补通道优先用网络结果垫底再用 GPS 修正。2.2 运行时权限FINE 与 COARSE 的边界Android 6.0 之后定位权限从安装时授权变成了运行时授权。ACCESS_FINE_LOCATION授权后可以拿到 GPS 和网络定位只授ACCESS_COARSE_LOCATION时系统会屏蔽 GPS 提供者只给网络粗定位结果。很多源码里为了省事只申请 FINE其实并不合规。2.2.1 权限请求代码if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{ Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION }, 1001); } else { startLocation(); }这段代码把 FINE 和 COARSE 一起申请避免用户拒绝 FINE 后连粗定位都用不了。1001是请求码回调里要判断grantResults是否全部大于等于 0。注意 Android 12 之后模糊定位会被系统单独列出来如果用户只给ACCESS_COARSE_LOCATION代码里再去请求 GPS 更新会直接收到SecurityException所以回调里必须再次检查locationManager.getProviders(true)中 GPS 是否可用。2.3 监听器接线回调线程与 removeUpdates 时机LocationListener的onLocationChanged()默认运行在调用requestLocationUpdates()的线程。如果是在 Activity 里调用回调在主线程执行意味着你不能在里面做耗时操作。源码里常见的写法是把定位逻辑放进一个Service然后通过HandlerThread接收位置更新这样能避免主线程卡顿。LocationListener listener new LocationListener() { Override public void onLocationChanged(Location location) { if (location ! null) { Log.d(LocSdk, lat location.getLatitude() , lng location.getLongitude()); } } Override public void onStatusChanged(String provider, int status, Bundle extras) { // 状态切换OUT_OF_SERVICE / TEMPORARILY_UNAVAILABLE / AVAILABLE } Override public void onProviderEnabled(String provider) { } Override public void onProviderDisabled(String provider) { } };这里的onProviderDisabled是排错关键。GPS 被用户从设置里关掉后代码不会收到任何异常只有这个回调会把状态推过来。我一般会在回调里弹出指引对话框而不是等到定位超时再提示。removeUpdates(listener)必须在onDestroy()或任务结束时调用否则 Service 里会累积多个监听器导致 CPU 唤醒和电量消耗都异常。3. 写一个能跑的定位模块GPS 与网络定位的双通道实现3.1 单次定位与连续定位的取舍源码里通常会同时出现两种方法requestSingleUpdate()适合「打开 App 只想知道自己在哪」的场景拿到一个 Location 就立刻释放。requestLocationUpdates()适合导航、运动轨迹、骑行记录这类需要持续跟踪的场景。单次定位的问题在于如果第一次请求恰好发生在 GPS 刚启动时返回的可能是一个精度很差的缓存位置。较完整的源码会先判断location.getAccuracy()如果精度大于 100 米就继续等待下一次更新直到满足阈值或者超时。3.2 参数设定minTime、minDistance 与省电策略requestLocationUpdates()有两个关键参数很多新手把它们当成固定值其实它们直接影响定位效果和功耗参数作用常见取值说明minTime最小更新间隔毫秒1000-60000不是严格定时系统可能提前或延后触发minDistance最小位移米0-1000 表示只要时间到了就更新适合移动速度快的场景我在做室外轨迹记录时会把 GPS 的minTime设为 3000、minDistance设为 5但如果是室内展示型应用直接设 10000 和 10 就够避免过于频繁的唤醒。你还要使用LocationRequest配合requestLocationUpdates(provider, minTime, minDistance, listener)的老接口Android 12 之后系统对后台位置更新做了更严格限制连续定位必须在前台服务里声明foregroundServiceTypelocation。3.3 一份可直接抄的定位服务代码下面是一份精简但完整的定位 Service放在Android手机定位源码里作为核心模块很合适public class LocationService extends Service { private LocationManager lm; private LocationListener listener; private void startLocation() { lm (LocationManager) getSystemService(LOCATION_SERVICE); Criteria criteria new Criteria(); criteria.setAccuracy(Criteria.ACCURACY_FINE); criteria.setPowerRequirement(Criteria.POWER_LOW); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { return; } ListString providers lm.getProviders(true); for (String provider : providers) { if (LocationManager.GPS_PROVIDER.equals(provider) || LocationManager.NETWORK_PROVIDER.equals(provider)) { lm.requestLocationUpdates(provider, 5000, 5, listener); } } } Override public void onDestroy() { if (lm ! null listener ! null) { lm.removeUpdates(listener); } super.onDestroy(); } }这段代码的逻辑是先用Criteria声明需求再遍历getProviders(true)找到当前可用的 Provider只要 GPS 或网络其中一种可用就都挂上监听。这样做的好处是 GPS 冷启动过程中能先用网络定位把大致位置填上等卫星锁定后再用 GPS 结果覆盖。requestLocationUpdates的第二个参数是 minTime这里给的 5000 表示 5 秒一次第三个参数 5 表示位移超过 5 米才更新如果两个条件其中一个满足系统都会回调。3.4 定位失败的常见原因拿到源码后如果发现一直走onLocationChanged却拿不到位置先按这个顺序排查权限对话框是否真的点了允许Android 11 以上还要检查「仅限这一次」授权是否会随 App 退出而失效。当前 Provider 是否可用locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)不可用时要跳到系统设置页。是否在室内且网络定位也关了这种情况下没有任何 Provider 能返回结果。是否把onLocationChanged里的location判空后直接 return 了某些源码在模拟器上会收到location null但真机上不一定。我曾经调试过一份定位源码问题出在getProviders(true)的布尔参数上。这个enabledOnly参数为 true 时只返回已经开启的 Provider如果用户在设置里关掉 GPS网络 Provider 又不可用providers就是空列表代码里没有兜底逻辑整个 Service 就静默失效了。修正方法是先判断 providers 是否为空为空就去请求ACCESS_COARSE_LOCATION或直接提示用户打开位置开关。4. 从源码到实战FusedLocationProvider 与地理围栏的接入4.1 为什么源码里会出现融合定位Android手机定位源码.zip里的 build.gradle 如果包含com.google.android.gms:play-services-location说明作者用的是FusedLocationProviderClient。它把 GPS、Wi-Fi、基站、传感器数据合并处理由系统决定哪条链路最省电、最准确。这套 API 不在 AOSP 里而是属于 Google Play 服务所以国产 ROM 上需要确认设备是否包含对应框架。FusedLocationProviderClient client LocationServices.getFusedLocationProviderClient(this); if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) PackageManager.PERMISSION_GRANTED) { client.getLastLocation() .addOnSuccessListener(location - { if (location ! null) { Log.d(Fused, String.valueOf(location.getLatitude())); } }); }getLastLocation()返回的是缓存位置来源可能是一小时前的定位结果但它的优点在于不消耗任何电量适合 App 启动后立刻展示地图。要拿实时位置需要用LocationRequest构建请求LocationRequest request LocationRequest.create(); request.setInterval(10000); request.setFastestInterval(5000); request.setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY); client.requestLocationUpdates(request, locationCallback, Looper.getMainLooper());这里setInterval(10000)是预期更新间隔setFastestInterval(5000)是上限PRIORITY_HIGH_ACCURACY会优先使用 GPS。如果室内不需要太高精度改成PRIORITY_BALANCED_POWER_ACCURACY可以显著降低功耗这也解释了为什么同一份源码在室外和室内表现差异巨大。4.2 Geofence 的定义与触发条件地理围栏是这套源码里最容易让人迷惑的部分。Geofence对象不是画一个圆形区域而是把「区域 触发事件 过期时间」打包成一个请求对象Geofence fence new Geofence.Builder() .setRequestId(office) .setCircularRegion(31.2304, 121.4737, 200) .setExpirationDuration(12 * 60 * 60 * 1000) .setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER | Geofence.GEOFENCE_TRANSITION_EXIT) .build();setCircularRegion的第三个参数是半径 200 米setExpirationDuration表示 12 小时后自动失效setTransitionTypes决定进入和离开都触发。官方文档没有特别强调的一点是如果设备没有 GPS 或者其他定位信号围栏不会触发这点和requestLocationUpdates依赖 Provider 的逻辑完全一致。4.3 地理围栏的注册与回调代码注册围栏需要依赖GeofencingClient并且要先确认设备支持地理围栏GeofencingClient geofencingClient LocationServices.getGeofencingClient(this); if (ActivityCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) PackageManager.PERMISSION_GRANTED) { geofencingClient.addGeofences( new GeofencingRequest.Builder() .addGeofence(fence) .setInitialTrigger(GeofencingRequest.INITIAL_TRIGGER_ENTER) .build(), geofencePendingIntent ); }这里的geofencePendingIntent是关键节点。它通常是PendingIntent.getService()指向一个处理围栏事件的IntentService而不是 Activity。因为围栏可能在 App 退到后台时触发Activity 已经被回收Service 才能保证回调不丢。在回调的onHandleIntent里你才能从GeofencingEvent中取出真实触发的围栏int transition GeofencingEvent.fromIntent(intent).getGeofenceTransition(); if (transition Geofence.GEOFENCE_TRANSITION_ENTER) { Log.d(Geofence, enter office area); } else if (transition Geofence.GEOFENCE_TRANSITION_EXIT) { Log.d(Geofence, exit office area); }4.4 并发位置更新与场景化处理复杂 App 里经常同时存在普通定位和地理围栏这时容易踩两个坑多个模块各自调用requestLocationUpdates底层会创建多个监听器系统会重复计算位置源。围栏回调里再启动一次全量定位导致位置更新风暴。处理方式是在源码里加一个全局单例的定位管理器所有模块都通过它注册回调LocationListener内部用CopyOnWriteArrayList保存外部回调。这样 GPS 每 5 秒产生一个位置就能同时派发给地图模块、围栏模块和记录模块功耗只涨一次。另一个常见坑是Geofence的setExpirationDuration传了Geofence.NEVER_EXPIRE长期不失效的围栏会持续占用地理位置服务。如果业务只要求当天有效我建议显式设置过期时间并在onDestroy里调用removeGeofences(pendingIntent)清理。5. 验证与排错用 adb 模拟位置测通整套源码拿到Android手机定位源码.zip后第一件事不是编译而是先用模拟器或者真机把定位链路打通。在 Android Studio 里打开项目后点开 Device Explorer 找到终端执行下面三条命令就能伪造当前设备位置adb shell appops set com.example.locapp android:mock_location allow adb emu geo fix 121.4737 31.2304第一条命令给目标 App 开启模拟定位权限第二条把 GPS 坐标固定到上海人民广场。注意adb emu geo fix只对模拟器有效真机需要先在开发者选项里开启「允许模拟位置」。顺带一句市面上那些手机模拟定位 App 的原理大多也是这样通过后台服务调用LocationManager.addTestProvider()注入坐标但普通 App 没有android.permission.ACCESS_MOCK_LOCATION权限是跑不通的。验证时打开 logcat过滤关键字LocSdk或者Fusedadb logcat -s LocSdk:I如果能看到lat31.2304, lng121.4737这样的日志说明定位链路已经通了。接下来测精度连续执行三次geo fix每次间隔 5 秒观察日志里location.getAccuracy()的变化。原厂源码里精度字段是 0 是很正常的说明模拟器没有提供真实的误差模型不代表真机上也是这样。最后一招是验证后台定位。把 App 按 Home 键退到后台再执行adb shell dumpsys location | grep -A 10 last location看看目标包名是否还在位置监听列表里。如果这份源码用了前台 Service这里会出现foregroundServiceTypelocation如果只是普通 ServiceAndroid 8 以上大概率会被系统杀掉这也帮你确定了后续改造方向。本文还有配套的精品资源点击获取