自己动手写了一个 Android 虚拟定位 App:GPSSimulate 技术实战(v1.0.2)
本文介绍我出于实际需求,从零开发的一款 Android 虚拟定位应用 GPSSimulate。文章涵盖项目起因、技术选型、核心实现思路,以及开发过程中踩过的坑。当前版本:v1.0.2。
写在前面
这篇文章以 技术学习与个人开发实践 为目的,分享 Android 官方模拟定位机制的实现方式。
请合理、合法地使用定位相关能力,尊重目标应用的服务条款。部分 App 会主动检测并拒绝模拟定位,文中也会如实说明。
项目地址:
一、起因:我想「假装」自己在另一个城市
前段时间有个很实际的需求。
我平时会关注一些换电/租车类 App,比如 智租。这类 App 的很多信息是 按城市/地理位置 展示的——月租套餐多少钱、站点覆盖、电池型号(48V / 60V 等)都会因城市不同而变化。
人没到现场,却想提前了解 「如果我在苏州,能看到什么套餐?」 或者 「杭州和苏州的电池类型有什么区别?」
市面上的虚拟定位 App 不少,但要么广告多,要么权限索取激进,要么在小米手机上不稳定。作为一个 Android 开发者,我索性自己写一个:
GPSSimulate —— 地图选点 + 快捷城市定位 + 系统级 GPS 模拟。
核心目标很简单:
- 在地图上拖动选点,直观地把位置改到目标城市
- 支持保存常去城市(如苏州),一键跳转
- 使用 Android 官方支持的模拟定位机制,稳定、可维护
二、技术栈一览(v1.0.2)
| 类别 | 技术 | 说明 |
|---|---|---|
| 语言 | Kotlin | 项目主语言 |
| UI | Jetpack Compose + Material 3 | 声明式 UI,开发效率高 |
| 地图 | 百度地图 SDK | 国内加载快、交互成熟,需配置 API Key |
| 坐标转换 | 自研 GeoCoordinateConverter | 地图 BD-09 ↔ 系统模拟 WGS-84 |
| 定位 | Google Play Services Location | 获取真实 GPS,用于地图初始定位 |
| 模拟定位 | LocationManager Test Provider + Fused Mock | 多 Provider 注入 + 融合定位 Mock |
| Mock 标记处理 | LocationSanitizer | 反射清除 isFromMockProvider 等标记 |
| 后台 | Foreground Service | 持续注入坐标,防止被系统杀掉 |
| 持久化 | SharedPreferences | 保存用户自定义的快捷城市 |
| 发布 | GitHub Actions + Releases + Pages | 推送 v* 标签自动打包 APK |
项目环境:
minSdk = 30(Android 11+)targetSdk = 36compileSdk = 36versionName = 1.0.2
三、虚拟定位的原理:不是黑科技,是官方 API
很多同类型 App 的核心原理其实一样:向系统注册 Test Provider,注入假坐标。
locationManager.addTestProvider(
LocationManager.GPS_PROVIDER,
/* requiresNetwork */ false,
/* requiresSatellite */ true,
/* requiresCell */ false,
/* hasMonetaryCost */ false,
/* supportsAltitude */ true,
/* supportsSpeed */ true,
/* supportsBearing */ true,
Criteria.POWER_LOW,
Criteria.ACCURACY_FINE,
)
locationManager.setTestProviderEnabled(LocationManager.GPS_PROVIDER, true)
locationManager.setTestProviderLocation(LocationManager.GPS_PROVIDER, mockLocation)
前置条件
用户必须在 开发者选项 中,将本 App 设为 「模拟位置信息应用」。
此外,Manifest 中需要声明(否则不会出现在系统候选列表里):
<uses-permission
android:name="android.permission.ACCESS_MOCK_LOCATION"
tools:ignore="ProtectedPermissions" />
我踩过的第一个坑:只 mock GPS 不够
最初只注入了 GPS_PROVIDER,结果发现部分 App 读到的仍是真实位置。
原因是很多 App 使用 融合定位(Fused Location) 或 网络定位(NETWORK_PROVIDER),并不只信 GPS。
v1.0.2 的改进方案:三路同时注入。
| Provider | 作用 |
|---|---|
GPS_PROVIDER | 覆盖传统 GPS 读取路径 |
NETWORK_PROVIDER | 覆盖网络定位路径 |
FUSED_PROVIDER(Android 12+) | 覆盖系统融合定位路径 |
FusedLocationProviderClient.setMockLocation() | 覆盖 Google Play Services 融合定位 |
// 同时向 GPS、NETWORK、FUSED 注入
activeProviders.forEach { config ->
pushToTestProvider(config.name, latitude, longitude, ...)
}
// 再通过 Google 融合定位 API 注入
fusedClient.setMockMode(true)
fusedClient.setMockLocation(location)
前台服务中每 300ms tick() 一次,确保依赖 LocationListener 的 App 能持续收到更新。
启动时还会 burst 推送 5 次(间隔 80ms),让目标 App 在首次打开时更快读到模拟坐标。
四、项目架构
app/
├── MainActivity.kt # 入口
├── ui/
│ └── LocationScreen.kt # 地图 + 控制面板(Compose)
├── location/
│ ├── MockLocationProvider.kt # 核心:多 Provider 注入 + Fused Mock
│ ├── LocationSanitizer.kt # 清除 Location 上的 mock 标记
│ ├── MockLocationChecker.kt # 检测是否已设为模拟定位 App
│ ├── LocationHelper.kt # 获取真实定位(含超时与强制刷新)
│ ├── PresetLocation.kt # 快捷城市数据模型 + 解析
│ └── PresetLocationRepository.kt # 本地持久化
├── map/
│ └── GeoCoordinateConverter.kt # WGS-84 ↔ BD-09 坐标转换
└── service/
└── MockLocationService.kt # 前台服务,每 300ms 持续模拟
五、核心模块拆解
1. 地图选点:百度地图 + 坐标转换
早期版本用过 OSMDroid + OpenStreetMap,国内首次加载偏慢。v1.0.2 切换为 百度地图 SDK,加载速度和交互体验更好。
但有一个关键细节:
- 百度地图使用 BD-09 坐标
- 系统 GPS 模拟使用 WGS-84 坐标
所以地图拖动选点后,需要把 BD-09 转成 WGS-84 再注入系统;显示真实位置时则反向转换。
// 地图中心点(BD-09)→ 系统模拟坐标(WGS-84)
val (wgsLat, wgsLng) = GeoCoordinateConverter.bd09ToWgs84(
target.latitude,
target.longitude,
)
// 真实定位(WGS-84)→ 地图显示(BD-09)
val bd09 = GeoCoordinateConverter.wgs84ToBd09(latitude, longitude)
map.animateMapStatus(MapStatusUpdateFactory.newLatLngZoom(bd09, 16f))
交互参考了打车 App 的常见做法:
- 图钉固定在屏幕中心
- 用户拖动地图,中心点经纬度即为模拟目标
- 停止拖动 300ms 后更新坐标(防抖)
- 模拟过程中拖动地图,坐标自动同步更新
2. LocationSanitizer:处理 Mock 标记
系统注入的 Location 对象默认带有 isFromMockProvider = true,部分第三方 App 会直接拒绝。
LocationSanitizer 在每次推送前尝试清除这些标记:
// API 31 以下
Location::class.java
.getMethod("setIsFromMockProvider", Boolean::class.javaPrimitiveType)
.invoke(location, false)
// API 31+
Location::class.java
.getMethod("setMock", Boolean::class.javaPrimitiveType)
.invoke(location, false)
注意:这只能提升部分 App 的接受度,无法绕过所有商业风控。部分 App 会在弹框提示后才重新发起定位请求,此时模拟坐标才会生效——这是目标 App 自身的时序逻辑,不是弹框「激活」了模拟。
3. 真实定位:三级回退 + 超时兜底
获取真实位置时,LocationHelper 采用多级策略:
lastLocation(缓存,最快)getCurrentLocation(单次高精度)requestLocationUpdates(持续监听,取第一个结果)
并加了两个保护机制:
- 单次回调:防止多个异步路径重复触发
- 3 秒超时:避免一直转圈无反馈
timeoutRunnable = Runnable {
deliverFailure("定位超时,请确保已开启 GPS 并到开阔地带重试")
}
mainHandler.postDelayed(timeoutRunnable, 3_000L)
停止模拟后的恢复问题: 系统 lastLocation 缓存里可能仍是刚才的模拟坐标。因此在点击「停止模拟」后,下一次「当前位置」会 强制跳过缓存、走实时定位:
// 停止模拟时
forceFreshLocateOnce = true
// 定位时
LocationHelper.fetchCurrentLocation(
context = context,
forceFreshLocation = forceFreshLocateOnce,
...
)
4. 快捷定位:预设城市 + 手动添加
内置默认城市「苏州」,并支持用户手动输入:
苏州, 31.3167, 120.6167
格式:城市, 纬度, 经度,逗号分隔,空格自动 trim。
数据通过 SharedPreferences 持久化,重启 App 不丢失。
5. 首次启动引导
若用户未在开发者选项中配置模拟定位 App,会弹出 不可关闭的居中对话框,引导前往设置。打开开发者选项时做了多重 Intent fallback,适配不同厂商 ROM。
开始模拟时若抛出 SecurityException,会提示用户重新检查「模拟位置信息应用」设置。
6. 前台服务保活
模拟定位需要长时间运行时,使用 Foreground Service + 通知栏常驻,避免进程被系统回收:
class MockLocationService : Service() {
// 每 300ms tick,持续注入坐标
private const val TICK_INTERVAL_MS = 300L
}
六、开发过程中踩过的坑
1. 小米手机的 USB 安装限制
INSTALL_FAILED_USER_RESTRICTED: Installation via USB is disabled
解决:开发者选项 → 打开 USB 安装 + USB 调试(安全设置)。
2. 真机定位失败 / 停止模拟后定位不准
- 冷启动
getCurrentLocation()经常返回null - 停止模拟后
lastLocation仍是旧坐标
解决:三级回退 + 3 秒超时 + 停止模拟后强制实时定位。
3. 目标 App「弹框前不生效,弹框后才生效」
这是时序问题:目标 App 首次读到了缓存/真实位置并弹风控提示,用户点掉后 App 重新请求定位,才读到持续注入的模拟坐标。靠 300ms 持续推送 + 启动 burst 可以缓解,但无法完全控制目标 App 的读取时机。
4. Release 打包 Lint 报错
MockLocation: Mock locations should only be requested in a debug manifest
本 App 本身就是模拟定位工具,因此在 build.gradle.kts 中关闭了该 Lint 检查:
lint {
disable += "MockLocation"
}
5. 哪些 App 仍然「骗不过」?
| 类型 | 结果 |
|---|---|
| 普通地图、天气、部分信息类 App | ✅ 通常有效 |
| 使用融合定位的 App | ⚠️ 部分有效 |
| 银行、支付、打车、游戏 | ❌ 多会检测 isFromMockProvider() 或自有风控 |
回到智租这个场景:能否成功取决于它自身的定位策略和风控。本项目的价值更多在于 理解 Android 定位机制 + 自用工具,不保证能绕过所有商业 App 的检测。
七、使用流程(简版)
- 开启开发者模式:设置 → 关于手机 → 连点版本号 7 次
- 设置模拟定位 App:开发者选项 → 选择模拟位置信息应用 → GPSSimulate
- 打开 App,授予位置权限
- 拖动地图选点,或从右上角书签选择快捷城市
- 点击 「开始模拟」
- 打开目标 App 查看效果
建议: 模拟时可在系统设置中 关闭真实 GPS,仅保留网络定位,可减少真实坐标与模拟坐标冲突。
八、版本发布
项目通过 GitHub Actions 自动发布:
# 更新版本号后打标签推送
git tag -a v1.0.2 -m "Release 1.0.2"
git push origin main && git push origin v1.0.2
推送 v* 标签后,Actions 自动构建 Release APK 并上传到 Releases 页面。
本地打包:
./scripts/build-release.sh
# 输出:app/build/outputs/apk/release/app-release.apk
九、总结与收获
这个项目源于一个很生活化的需求:想在异地,提前看到当地 App 展示的信息。
从技术角度看,最大的收获是:
- 理解了 Android Test Provider 官方模拟定位机制
- 认识到 GPS / 网络 / 融合定位 的差异,以及为什么要多 Provider + Fused Mock 同时注入
- 搞清了 BD-09 与 WGS-84 坐标系差异,地图选点和系统注入必须做转换
- 用 Compose + 百度地图 快速搭出可用的地图交互
- 踩遍了小米真机开发、定位缓存、目标 App 风控时序等各种坑
如果你也想做类似工具,不必追求「绕过一切风控」——把 官方 API 用对、用稳,已经能解决不少场景。
标签建议: Android Kotlin Jetpack Compose GPS 虚拟定位 百度地图
封面一句话: 出于想跨城查看智租套餐的需求,我用 Kotlin + Compose + 百度地图写了一个 Android 虚拟定位 App,v1.0.2 支持三路 Provider 注入与融合定位 Mock,本文分享技术方案与踩坑记录。
如果这篇文章对你有帮助,欢迎点赞收藏。有问题可以在评论区交流,我会尽量回复。