25、位置共享:蓝牙定位共享、WebSocket实时位置同步、Firebase Realtime Database集成
位置共享这个功能,说白了就是把一个人的坐标实时推给另一个人。我最早接触这个需求是在一个户外徒步App里——几个人进山后手机信号时有时无,但蓝牙还能用。那时候我就意识到,位置共享不能只依赖网络,得有多套方案兜底。
这一章我们聊三个方向:蓝牙定位共享(近距离)、WebSocket实时同步(中远距离)、Firebase Realtime Database集成(云端持久化)。它们各有各的适用场景,也各有各的坑。
25.1 蓝牙定位共享:近距离的“悄悄话”
蓝牙定位共享,适合两个人距离在10米以内的情况。比如你在商场里找朋友,或者团队在野外作业。它的核心思路是:一台设备广播自己的位置,另一台设备扫描并接收。
我习惯用 Bluetooth LE(低功耗蓝牙) 来做这件事。Android 从 4.3 开始就支持 BLE,API 也比较成熟。
25.1.1 广播端(发送位置)
广播端需要把自己的经纬度编码到广播包里。注意,广播包最大只有31字节,所以得压缩一下数据。
// 广播端:发送位置
private fun startAdvertising(latitude: Double, longitude: Double) {
val advertiseData = AdvertiseData.Builder()
.setIncludeDeviceName(false)
.addServiceUuid(ParcelUuid(LOCATION_SERVICE_UUID))
.addServiceData(ParcelUuid(LOCATION_SERVICE_UUID), encodeLocation(latitude, longitude))
.build()
val advertiseSettings = AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
.setConnectable(false)
.build()
bluetoothLeAdvertiser?.startAdvertising(advertiseSettings, advertiseData, advertiseCallback)
}
private fun encodeLocation(lat: Double, lng: Double): ByteArray {
// 将经纬度转为int,节省空间
val latInt = (lat * 1_000_000).toInt()
val lngInt = (lng * 1_000_000).toInt()
return ByteBuffer.allocate(8).putInt(latInt).putInt(lngInt).array()
}
这里有个细节:setConnectable(false) 表示只广播不连接,功耗更低。我在项目里实测过,开启广播后每小时耗电大约增加 3%-5%,还算能接受。
25.1.2 扫描端(接收位置)
扫描端就简单了,监听广播包,解析出经纬度。
// 扫描端:接收位置
private val scanCallback = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult?) {
result?.let {
val scanRecord = it.scanRecord
val serviceData = scanRecord?.getServiceData(ParcelUuid(LOCATION_SERVICE_UUID))
if (serviceData != null) {
val (lat, lng) = decodeLocation(serviceData)
updateUI(lat, lng)
}
}
}
}
private fun decodeLocation(data: ByteArray): Pair<Double, Double> {
val buffer = ByteBuffer.wrap(data)
val lat = buffer.int / 1_000_000.0
val lng = buffer.int / 1_000_000.0
return Pair(lat, lng)
}
ACCESS_FINE_LOCATION 权限才能扫描到 BLE 设备。Android 10 以上还需要 ACCESS_BACKGROUND_LOCATION 才能在后台扫描。我曾经因为没加后台权限,导致App切到后台后就收不到广播了,排查了半天。
25.2 WebSocket实时位置同步:中远距离的“对讲机”
蓝牙只能覆盖几十米。一旦距离拉远,就得靠网络了。WebSocket 是我个人比较推荐的方式——它比 HTTP 轮询省电,也比 MQTT 简单(不需要额外搭 Broker)。
你想想看,如果两个人都在移动,每隔几秒就要上报一次位置。用 HTTP 的话,每次都要建立连接、发送、断开,开销很大。WebSocket 建立一次连接后,双向推送,延迟通常在 100ms 以内。
25.2.1 客户端实现
Android 上我用 OkHttp 的 WebSocket 客户端,因为它和 Retrofit 配合得好,不用额外引入库。
// WebSocket 客户端
class LocationWebSocket(private val url: String) {
private val client = OkHttpClient.Builder()
.pingInterval(30, TimeUnit.SECONDS) // 心跳保活
.build()
private var webSocket: WebSocket? = null
fun connect() {
val request = Request.Builder().url(url).build()
webSocket = client.newWebSocket(request, object : WebSocketListener() {
override fun onOpen(webSocket: WebSocket, response: Response) {
// 连接成功,可以开始发送位置
}
override fun onMessage(webSocket: WebSocket, text: String) {
// 收到对方的位置
val location = parseLocation(text)
updateUI(location)
}
override fun onFailure(webSocket: WebSocket, t: Throwable, response: Response?) {
// 断线重连
reconnect()
}
})
}
fun sendLocation(lat: Double, lng: Double) {
val json = "{\"lat\": $lat, \"lng\": $lng}"
webSocket?.send(json)
}
private fun reconnect() {
// 延迟3秒后重连
Handler(Looper.getMainLooper()).postDelayed({ connect() }, 3000)
}
}
pingInterval 一定要设。如果不设,运营商的路由器可能会把长时间空闲的连接断开。我遇到过在电梯里断连后重连失败的情况,加了心跳就好了。
25.2.2 服务端(Node.js 示例)
服务端我用 Node.js 的 ws 库,轻量又好用。每个客户端连接后,服务端负责转发位置数据。
// 服务端:位置转发
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
// 存储所有连接的客户端
const clients = new Map();
wss.on('connection', (ws, req) => {
const userId = req.url.split('?userId=')[1];
clients.set(userId, ws);
ws.on('message', (message) => {
// 解析消息,找到目标用户
const data = JSON.parse(message);
const targetUserId = data.targetUserId;
const targetWs = clients.get(targetUserId);
if (targetWs && targetWs.readyState === WebSocket.OPEN) {
// 转发位置
targetWs.send(JSON.stringify({
fromUserId: userId,
lat: data.lat,
lng: data.lng,
timestamp: Date.now()
}));
}
});
ws.on('close', () => {
clients.delete(userId);
});
});
这个方案有个问题:如果服务端重启,所有连接都会断。所以实际项目中我还会加一个 Redis 做 session 持久化,不过那是后话了。
25.3 Firebase Realtime Database 集成:云端的“黑板”
WebSocket 虽然实时性好,但有个硬伤——如果接收方离线,消息就丢了。Firebase Realtime Database 就不一样了,它会把数据持久化在云端,用户上线后自动同步。
说白了,Firebase 就是一个实时同步的 JSON 数据库。你写入数据,所有监听这个路径的客户端都会立刻收到通知。非常适合做位置共享的“历史轨迹”和“离线补发”。
25.3.1 初始化 Firebase
首先在 build.gradle 里加依赖:
implementation 'com.google.firebase:firebase-database:20.3.0'
然后在 Application 里初始化:
FirebaseApp.initializeApp(this)
25.3.2 写入位置数据
我习惯用 push() 方法生成唯一 key,这样不会覆盖之前的数据。
// 写入位置到 Firebase
fun uploadLocation(userId: String, lat: Double, lng: Double) {
val database = FirebaseDatabase.getInstance()
val ref = database.getReference("locations/$userId")
val locationData = hashMapOf(
"lat" to lat,
"lng" to lng,
"timestamp" to ServerValue.TIMESTAMP
)
ref.push().setValue(locationData)
.addOnSuccessListener {
// 写入成功
}
.addOnFailureListener { e ->
Log.e("LocationSync", "写入失败", e)
}
}
ServerValue.TIMESTAMP 而不是 System.currentTimeMillis()。因为不同手机的系统时间可能不一致,用服务器时间戳才能保证时序正确。我在测试时发现两台手机时间差了5秒,导致轨迹回放顺序错乱,改用服务器时间戳后问题解决。
25.3.3 监听位置变化
监听端用 addChildEventListener,每次有新数据都会回调。
// 监听对方的位置
fun observeLocation(targetUserId: String, onLocationUpdate: (Double, Double) -> Unit) {
val database = FirebaseDatabase.getInstance()
val ref = database.getReference("locations/$targetUserId")
// 只监听最新的位置
val query = ref.orderByChild("timestamp").limitToLast(1)
query.addChildEventListener(object : ChildEventListener {
override fun onChildAdded(snapshot: DataSnapshot, previousChildName: String?) {
val lat = snapshot.child("lat").value as Double
val lng = snapshot.child("lng").value as Double
onLocationUpdate(lat, lng)
}
override fun onChildChanged(snapshot: DataSnapshot, previousChildName: String?) {
// 位置更新时也会触发
}
override fun onChildRemoved(snapshot: DataSnapshot) {}
override fun onChildMoved(snapshot: DataSnapshot, previousChildName: String?) {}
override fun onCancelled(error: DatabaseError) {
Log.e("LocationSync", "监听失败: ${error.message}")
}
})
}
这里有个性能问题:如果位置更新很频繁(比如每秒一次),Firebase 的实时同步会带来不小的流量开销。我建议根据场景调整上报频率——步行时每5秒一次,开车时每2秒一次,静止时每30秒一次。
25.4 三种方案的对比与选择
我把这三种方案整理成了表格,方便你根据场景选择:
| 特性 | 蓝牙 BLE | WebSocket | Firebase RTDB |
|---|---|---|---|
| 有效距离 | 10-30米 | 不限(依赖网络) | 不限(依赖网络) |
| 延迟 | < 1秒 | 100-500ms | 200-1000ms |
| 是否需要网络 | 不需要 | 需要 | 需要 |
| 离线消息 | 不支持 | 不支持 | 支持(持久化) |
| 功耗 | 低 | 中 | 中 |
| 实现复杂度 | 中 | 低 | 低 |
| 典型场景 | 室内找朋友、野外无信号 | 实时追踪、车队管理 | 轨迹记录、离线补发 |
我个人建议:如果只是两个人近距离共享,蓝牙就够了。如果需要实时追踪且双方都在线,WebSocket 是性价比最高的方案。如果要做历史轨迹回放或者担心对方掉线,那就上 Firebase。
25.5 知识体系总览
下面这张图把三种方案的核心流程串起来了,你可以对照着看:
嗯,到这里三种方案都讲完了。蓝牙适合“面对面”,WebSocket 适合“在线实时”,Firebase 适合“有历史记录”。实际项目中我经常把它们组合起来用——蓝牙兜底近距离,WebSocket 做主通道,Firebase 做数据备份。这样不管网络好不好,位置都不会丢。
公众号:蓝海资料掘金营,微信deep3321