多设备连接管理:连接池设计、并发控制、设备状态机、资源释放
好,咱们进入一个比较硬核的话题——多设备连接管理。说实话,很多做 BLE 开发的朋友,一开始都是单设备连接,跑通了就觉得自己会了。但一到实际产品里,要同时连三四个设备,各种问题就冒出来了。我当年第一次做多设备项目时,就被坑得不轻。
这一章,我会把多设备连接的核心问题拆开揉碎讲清楚。你想想看,同时管理多个 BLE 连接,本质上就是四个问题:
- 怎么存这些连接?——连接池设计
- 怎么防止它们打架?——并发控制
- 每个设备当前在干嘛?——设备状态机
- 断开后怎么收拾干净?——资源释放
咱们一个一个来。
连接池设计:别用散乱的 Map,要有个池子
很多新手喜欢这么写:
val deviceMap = mutableMapOf<String, BluetoothDevice>()
val gattMap = mutableMapOf<String, BluetoothGatt>()
嗯,看着挺直接,但问题很大。两个 Map 之间没有约束,你删了 device 忘了删 gatt,内存泄漏就来了。我个人习惯的做法,是封装一个 连接池对象。
核心思路:一个设备地址对应一个连接槽位,槽位里包含 GATT 引用、状态、回调、定时器等所有资源。
来看代码:
data class ConnectionSlot(
val address: String,
var gatt: BluetoothGatt? = null,
var state: ConnectionState = ConnectionState.DISCONNECTED,
var retryCount: Int = 0,
var lastActiveTime: Long = 0L,
val callback: BleGattCallback? = null
)
class ConnectionPool(private val maxConnections: Int = 7) {
private val slots = ConcurrentHashMap<String, ConnectionSlot>()
fun allocate(address: String): Boolean {
if (slots.size >= maxConnections) {
// 这里可以触发 LRU 淘汰策略
return false
}
return slots.putIfAbsent(address, ConnectionSlot(address)) == null
}
fun getSlot(address: String): ConnectionSlot? = slots[address]
fun release(address: String) {
slots.remove(address)?.let { slot ->
slot.gatt?.close()
// 清理回调等资源
}
}
}
这里有个细节:ConcurrentHashMap 是必须的。因为 BLE 回调可能来自不同线程,普通 HashMap 在多线程下会出诡异问题。我曾经排查过一个 Bug,就是 HashMap 在并发 put 时导致死循环,CPU 飙到 100%,查了两天才定位到。
并发控制:别让 GATT 操作撞车
Android BLE 有个很坑的设定——同一时刻只能有一个 GATT 操作在进行。什么意思?就是你正在 writeCharacteristic,这时候又调了 readRssi,后者会被静默丢弃,连错误回调都没有。
所以,并发控制的核心就是:每个设备一个操作队列,串行执行。
我建议这样设计:
class GattOperationQueue(private val gatt: BluetoothGatt) {
private val queue = LinkedList<BleOperation>()
private var isBusy = false
@Synchronized
fun enqueue(op: BleOperation) {
queue.add(op)
processNext()
}
@Synchronized
private fun processNext() {
if (isBusy || queue.isEmpty()) return
isBusy = true
val op = queue.poll()
op.execute(gatt) {
@Synchronized
fun onComplete() {
isBusy = false
processNext()
}
}
}
}
你可能会问:那多个设备之间呢?设备之间是独立的,每个设备有自己的 GATT 对象,所以可以并行操作。真正要防的是 同一个设备上的操作重叠。
小技巧:我习惯在 enqueue 时加一个超时机制。如果某个操作 10 秒还没完成,就强制清空队列并重置 GATT。因为有些低端蓝牙芯片会卡死,不发回调也不报错。
设备状态机:每个设备都得知道自己在哪
多设备管理最怕什么?怕你不知道某个设备当前处于什么阶段。是正在连接?已经连上了?还是在做服务发现?
我见过有人用几个 boolean 变量来标记:isConnecting、isDiscovered……结果状态一多就乱套了。正确的做法是用 枚举状态机。
enum class ConnectionState {
DISCONNECTED,
CONNECTING,
CONNECTED,
SERVICE_DISCOVERING,
SERVICE_DISCOVERED,
READY,
DISCONNECTING
}
状态转换图大概是这样的:
有了这个状态机,你在回调里就可以做精确判断:
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
val slot = pool.getSlot(gatt.device.address) ?: return
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
if (slot.state == ConnectionState.CONNECTING) {
slot.state = ConnectionState.CONNECTED
gatt.discoverServices()
} else {
// 异常状态,断开重连
gatt.disconnect()
}
}
BluetoothProfile.STATE_DISCONNECTED -> {
slot.state = ConnectionState.DISCONNECTED
// 触发重连或资源释放
}
}
}
注意:状态机一定要处理异常跳转。比如你在 CONNECTED 状态突然收到 DISCONNECTED 回调,这是正常的。但如果你在 DISCONNECTED 状态收到 onServicesDiscovered,那就有问题了。我建议每个状态转换都加 assert 或日志,方便排查。
资源释放:断开连接只是第一步
很多人以为调了 gatt.disconnect() 就完事了。其实不是。完整的资源释放流程应该是:
- 停止所有操作:清空 GATT 操作队列,取消未完成的定时器。
- 断开 GATT 连接:调用
gatt.disconnect()。 - 关闭 GATT 引用:调用
gatt.close(),释放底层资源。 - 清理回调:移除注册的 BroadcastReceiver 或回调引用,防止内存泄漏。
- 从连接池移除:调用
pool.release(address)。
我建议封装一个统一的方法:
fun releaseDevice(address: String) {
val slot = pool.getSlot(address) ?: return
// 1. 停止队列
slot.gatt?.let { gatt ->
operationQueues[address]?.clear()
}
// 2. 断开
slot.gatt?.disconnect()
// 3. 关闭
slot.gatt?.close()
// 4. 清理回调
slot.callback?.let {
// 从注册表中移除
}
// 5. 从池中移除
pool.release(address)
}
一个血的教训:我曾经在 onDisconnected 回调里直接调了 gatt.close(),结果发现 close 之后系统还会再回调一次 onDisconnected,导致空指针。正确的做法是加一个标志位,或者用 Handler 延迟执行 close。
连接池的淘汰策略
Android 系统对 BLE 连接数有限制,一般是 7 个左右(不同厂商不一样)。所以连接池满了之后,你需要决定踢掉谁。
我常用的策略是 LRU(最近最少使用):
fun evictIfNeeded(): String? {
if (slots.size < maxConnections) return null
return slots.minByOrNull { it.value.lastActiveTime }?.key
}
当然,你也可以根据业务优先级来。比如某个设备是核心设备,永远不踢;其他设备按时间淘汰。这个就看具体场景了。
总结一下
多设备连接管理,说白了就是三个字:池、队、态。
- 池:用 ConnectionPool 统一管理所有连接资源。
- 队:每个设备一个操作队列,保证串行执行。
- 态:用状态机精确跟踪每个设备的生命周期。
再加上完善的资源释放机制,基本就能应对大部分多设备场景了。嗯,这一块内容比较多,但都是实战中必须啃下来的硬骨头。你可以在自己的项目里先搭一个简单的连接池,然后逐步加上队列和状态机,慢慢就会顺手了。