做过IoT或者有过室内定位需求的同学应该都经历过这个场景:
设备在室外跑得好好的,GPS坐标稳定,精度两三米。一进地下停车场、一进仓库内部、一进大型商场,GPS信号直接衰减到不可用,getLastKnownLocation() 返回null,或者返回几分钟前的老坐标,系统完全失去对设备的感知。
这不是代码问题,是物理限制。GPS信号是L波段,穿透力差,混凝土墙壁和金属屋顶是天然屏蔽层,室内场景本来就不是GPS的主场。
这篇文章想聊清楚几件事:融合定位在技术上是怎么工作的、工程上有哪些实现坑、以及怎么用现成的API快速把这个能力接进自己的系统。
一、融合定位的信号来源:三层逻辑
融合定位(也叫Network-based Positioning或Hybrid Positioning)本质上是在GPS不可用时,用其他射频信号来估算位置。主要有三种信号来源:
1. 基站定位(Cell Tower Positioning)
每个移动基站都有唯一的识别码,由 MCC(移动国家码)+ MNC(移动网络码)+ LAC/TAC(区域码)+ Cell ID 共同构成。终端在任意时刻都在某个基站的覆盖范围内,通过查数据库匹配这个基站对应的坐标,就能得到一个粗略位置。
精度通常在100米~几公里级别,取决于基站密度。城市中心密度高,精度相对好;农村基站稀疏,误差可能达到几公里。
技术上还可以利用信号强度(RSSI / dBm)做多基站加权:同时扫到多个基站时,根据各自的信号功率做三角估算,精度能提升到50~200米。
2. WiFi定位(WiFi Fingerprinting / WiFi-RSSI)
这是室内定位精度最高的网络信号方案。原理是:WiFi AP的MAC地址是全局唯一的,商业位置数据库(包括各大地图厂商、以及通过众包方式积累的数据)记录了海量AP的地理位置。扫描到周边AP列表后,用MAC地址查位置,再根据RSSI做加权,精度通常在15~50米。
密集的商业环境(商场、写字楼、地铁站)里WiFi AP部署多,精度可以做到15米甚至更好。这也是为什么你在大型商场里,即使没开GPS,手机也大概知道你在几楼哪个区域。
3. 数据融合层
实际工程里不会只用一种信号。WiFi信号不稳定(AP可能关机、信道拥塞),基站信号受地形影响,单一来源的定位结果方差很大。成熟的融合定位服务会同时接收多种信号输入,在服务端做加权融合,输出一个置信度更高的结果。
二、工程实现的几个真实问题
问题1:Android扫WiFi的限制越来越严
Android 9(API 28)之后,WifiManager.startScan() 受到了严格的节流限制:前台App最多每2分钟扫4次,后台App更少。Android 10之后部分厂商ROM直接禁掉了后台WiFi扫描。
这对IoT设备影响不算大(因为很多IoT设备跑的是定制系统或者低层直接读硬件),但如果你是在做消费端App的室内定位,这一块要提前考虑,老的WifiScan方案在新Android上基本废了。
问题2:基站参数收集
调用融合定位API时,你需要传入基站信息,通常包括:Cell ID、MCC、MNC、LAC/TAC,以及信号强度(asu或dbm二选一)。
Android上拿这些数据:
val telephonyManager = getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
// 需要 READ_PHONE_STATE 权限
val cellInfoList = telephonyManager.allCellInfo
cellInfoList?.forEach { cellInfo ->
when (cellInfo) {
is CellInfoLte -> {
val identity = cellInfo.cellIdentity
val signal = cellInfo.cellSignalStrength
// identity.ci => Cell ID
// identity.tac => TAC (区域码,LTE)
// identity.mcc => MCC
// identity.mnc => MNC
// signal.dbm => 信号强度 dBm
// signal.asuLevel => ASU
}
is CellInfoGsm -> {
val identity = cellInfo.cellIdentity
// identity.cid => Cell ID
// identity.lac => LAC
}
// 还有 CellInfoWcdma / CellInfoNr (5G)
}
}
注意:不同网络制式(GSM、WCDMA、LTE、NR)的字段名不完全一样,这里要做好类型判断。
问题3:坐标系输出要确认清楚
融合定位API输出的坐标,不同服务商用的坐标系不同。有的直接给WGS84,有的给GCJ02。如果你直接拿着WGS84坐标去高德地图上显示,会有50~300米的系统性偏移——不是精度问题,是坐标系没对齐。
三、接一个真实可用的融合定位API
说完原理,看看怎么快速把这个能力接进系统。下面用迈云位置服务 LTS 的融合定位接口来演示,端点是 POST /api/service/location,认证用Bearer Token,返回GCJ02坐标。
接口结构
POST https://lts.maiyun.net/api/service/location
Authorization: Bearer YOUR_API_KEY
Content-Type: application/json
请求体支持传 wifis(WiFi热点列表)或 cellulars(基站列表),二选一或同时传都行:
javascript
// WiFi方式
{
"from": 3, // 返回坐标系:3 = GCJ02
"time": 1768535120038, // 当前毫秒时间戳
"asset": "device-001", // 设备唯一标识,用于轨迹追踪
"wifis": [
{ "mac": "9E:2B:A6:86:2A:0E", "signal": -74 },
{ "mac": "54:52:84:86:03:A8", "signal": -79 },
{ "mac": "A8:6D:AA:12:34:56", "signal": -85 }
]
}
// 基站方式(LTE)
{
"from": 3,
"time": 1768535120038,
"asset": "device-001",
"cellulars": [
{
"cell": 12345678, // Cell ID
"primary": true,
"dbm": -85, // 信号强度
"country": 460, // MCC: 中国
"network": 0, // MNC: 中国移动
"area": 4321 // TAC
}
]
}
返回结构:
json
{
"result": 1,
"name": "某商业广场B2层",
"point": { "lng": 117.334369, "lat": 39.116094 },
"address": {
"name": "天津市东丽区万新街道平盈路8号",
"context": {
"province": { "name": "天津市", "code": "120000" },
"city": { "name": "天津市", "code": "120100" },
"district": { "name": "东丽区", "code": "120110" },
"township": { "name": "万新街道", "code": "120110006" }
}
}
}
address里的行政区划上下文直接到街道级,对于物流末端、区域分析这类场景很实用,不需要再做一次逆地址解析。
四、一个完整的Android端采集 + 上报封装
把前面说的串起来,写一个完整的工具类:
class FusedLocationReporter(
private val context: Context,
private val apiKey: String,
private val deviceId: String
) {
private val client = OkHttpClient()
private val json = Json { ignoreUnknownKeys = true }
// 采集WiFi列表
private fun scanWifis(): List<Map<String, Any>> {
val wm = context.applicationContext
.getSystemService(Context.WIFI_SERVICE) as WifiManager
return wm.scanResults.map {
mapOf("mac" to it.BSSID, "signal" to it.level)
}
}
// 采集基站列表
private fun scanCellulars(): List<Map<String, Any>> {
val tm = context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
val result = mutableListOf<Map<String, Any>>()
tm.allCellInfo?.forEach { info ->
when (info) {
is CellInfoLte -> result.add(mapOf(
"cell" to info.cellIdentity.ci,
"primary" to info.isRegistered,
"dbm" to info.cellSignalStrength.dbm,
"country" to (info.cellIdentity.mccString?.toIntOrNull() ?: 0),
"network" to (info.cellIdentity.mncString?.toIntOrNull() ?: 0),
"area" to info.cellIdentity.tac
))
// 可扩展 GSM、WCDMA、NR
}
}
return result
}
// 上报并返回位置
suspend fun report(): LocationResult? = withContext(Dispatchers.IO) {
val wifis = scanWifis()
val cellulars = scanCellulars()
// 优先用WiFi(精度更高),降级用基站
val body = buildJsonObject {
put("from", 3) // GCJ02
put("time", System.currentTimeMillis())
put("asset", deviceId)
if (wifis.isNotEmpty()) {
putJsonArray("wifis") {
wifis.forEach { w ->
addJsonObject {
put("mac", w["mac"] as String)
put("signal", w["signal"] as Int)
}
}
}
} else if (cellulars.isNotEmpty()) {
putJsonArray("cellulars") {
// … 同理
}
} else return@withContext null // 无信号数据,放弃
}
val request = Request.Builder()
.url("https://lts.maiyun.net/api/service/location")
.addHeader("Authorization", "Bearer $apiKey")
.post(body.toString().toRequestBody("application/json".toMediaType()))
.build()
try {
val response = client.newCall(request).execute()
val raw = response.body?.string() ?: return@withContext null
// 解析返回,映射到你自己的LocationResult数据类
parseResponse(raw)
} catch (e: Exception) {
Log.e("FusedLocation", "上报失败", e)
null
}
}
}
几个注意点:
- WiFi和基站采集都是耗时操作,withContext(Dispatchers.IO) 不能省
- WiFi扫描结果是上一次扫描的缓存,要在合适时机触发 startScan()
- asset 字段传设备唯一ID,这样在后端可以按设备维度查历史轨迹
五、误差码处理:别忽略 -3
接口的错误码里有一个 -3,含义是"信号数据不足,无法完成定位"。这种情况在极端弱信号环境(全屏蔽的机房、电梯轿厢深处)会出现。
建议的降级策略:
GPS可用 → 优先用GPS
GPS弱/不可用 → 调融合定位(WiFi优先,基站兜底)
融合定位返回-3 → 使用上一次成功定位的坐标 + 时间戳标记为"历史位置"
所有来源都失败 → 上报"定位失败"状态,不上报坐标
切忌在 -3 的时候把 point 字段设成 {0, 0} 然后当成有效坐标存进数据库——这个问题在轨迹回放的时候会非常明显,轨迹线突然飞到几内亚湾再飞回来。
总结
融合定位解决的是一个特定场景问题:GPS信号不足时维持位置感知能力。理解了信号来源和精度边界,才能在系统设计时做出合理的降级策略,不至于在出问题时手足无措。
从接入成本来看,用现成的融合定位API(比如迈云LTS)比自己维护一套WiFi/基站数据库要划算得多——20亿+ WiFi AP 数据自己积累基本不可能,直接用成品服务是正确选择。
如果你的项目有室内定位、IoT资产追踪、弱GPS场景覆盖的需求,这个方向值得认真评估一下。
有问题欢迎评论区讨论。

