13、BLE多连接管理:连接池设计、连接参数优化与GATT并发操作队列
各位同学,今天我们来聊聊BLE多连接管理。说实话,这个主题在市面上很多教程里都被一笔带过,但实际项目中,它往往是决定App稳定性的关键。
我记得刚入行那会儿,接手一个智能家居项目,需要同时连接手环、体脂秤、血压计三台设备。当时我天真地以为,只要把单连接的代码复制三份就行了。结果呢?设备频繁断连,数据采集乱成一锅粥。嗯,从那以后,我彻底明白了——多连接管理,绝不是简单的“复制粘贴”。
13.1 连接池设计:管好你的“连接资源”
什么叫连接池?说白了,就是用一个容器统一管理所有活跃的BLE连接。你想想看,如果每个连接都各自为政,谁来协调资源?谁来处理冲突?
我个人习惯用Map<String, BluetoothGatt>来维护连接池,key是设备地址,value是Gatt对象。但光有Map还不够,还得配套一个状态机。
class BleConnectionPool {
// 连接池核心:设备地址 -> Gatt对象
private val pool = ConcurrentHashMap<String, BluetoothGatt>()
// 连接状态:IDLE, CONNECTING, CONNECTED, DISCONNECTING
private val states = ConcurrentHashMap<String, ConnectionState>()
fun connect(device: BluetoothDevice) {
val address = device.address
if (states[address] == ConnectionState.CONNECTED) {
Log.w("BlePool", "设备 $address 已连接,跳过")
return
}
states[address] = ConnectionState.CONNECTING
// 发起连接...
}
fun disconnect(address: String) {
pool[address]?.disconnect()
pool[address]?.close()
pool.remove(address)
states.remove(address)
}
}
我在项目中遇到过一个问题:设备A断开后,连接池没有及时清理,导致设备B重连时拿到了一个过期的Gatt对象。后来我加了一个cleanup()方法,在onConnectionStateChange回调中统一处理。
13.2 连接参数优化:别让设备“睡死”过去
连接参数有三个核心指标:Connection Interval、Slave Latency、Supervision Timeout。这三个参数决定了功耗、实时性和稳定性之间的平衡。
| 参数 | 含义 | 典型值 | 影响 |
|---|---|---|---|
| Connection Interval | 两次连接事件之间的间隔 | 7.5ms ~ 4000ms | 值越小,延迟越低,功耗越高 |
| Slave Latency | 从设备可以跳过的事件次数 | 0 ~ 499 | 值越大,从设备越省电,但响应变慢 |
| Supervision Timeout | 连接超时时间 | 100ms ~ 32000ms | 超过此时间无通信则断开连接 |
为什么会这样?我解释一下。Connection Interval就像两个人打电话的频率——你每隔1分钟打一次,还是每隔10分钟打一次?Slave Latency则允许从设备“偷懒”,比如你允许它每3次通话中只接1次。Supervision Timeout是最后的底线,如果超过这个时间还没联系上,就默认对方挂断了。
我曾经踩过一个坑:某个手环在连接后,我设置了7.5ms的间隔,结果手机发热严重,电量哗哗往下掉。后来改成30ms + 2次延迟,功耗降了40%,数据也没丢。
13.3 GATT并发操作队列:别让指令“打架”
多连接场景下,最头疼的问题就是GATT操作冲突。你想想看,如果同时给设备A发读指令,给设备B发写指令,Android底层的GATT队列会怎么处理?
答案是:它会乱掉。Android的BluetoothGatt本身不是线程安全的,多个线程同时调用readCharacteristic()或writeCharacteristic(),轻则回调错乱,重则直接崩溃。
我的解决方案是:为每个连接维护一个独立的操作队列,串行执行。
class GattOperationQueue(private val gatt: BluetoothGatt) {
private val queue = LinkedList<GattOperation>()
private var isProcessing = false
@Synchronized
fun enqueue(operation: GattOperation) {
queue.add(operation)
if (!isProcessing) {
processNext()
}
}
private fun processNext() {
if (queue.isEmpty()) {
isProcessing = false
return
}
isProcessing = true
val op = queue.poll()
when (op.type) {
READ -> gatt.readCharacteristic(op.characteristic)
WRITE -> gatt.writeCharacteristic(op.characteristic, op.value, op.writeType)
NOTIFY -> gatt.setCharacteristicNotification(op.characteristic, true)
}
}
fun onOperationComplete() {
processNext() // 上一个操作完成后,自动执行下一个
}
}
onCharacteristicRead回调中直接发起新的GATT操作。Android要求每个GATT操作必须等待上一个操作的回调完成后才能发起。否则会收到GATT_ERROR。
嗯,这里有个细节:操作队列不仅要串行,还要支持超时。我曾经遇到过某个设备卡在readCharacteristic上,后续所有操作都堵死了。后来我加了一个超时机制,超过5秒没回调就清空队列并重连。
13.4 整体架构:一张图说清楚
下面这张图展示了多连接管理的整体流程。你可以看到,连接池、参数优化、操作队列三者是如何协同工作的。
从图中可以看到,连接池负责管理生命周期,参数优化负责协商通信节奏,操作队列保证指令有序执行。三者缺一不可。
13.5 实战避坑指南
- 连接数限制:Android BLE最多支持7个并发连接(取决于芯片和驱动)。我建议实际项目中控制在4-5个,留出余量。
- 参数协商失败:不是所有设备都支持你请求的参数。我曾经遇到一个设备,请求30ms间隔,它返回了100ms。一定要在
onConnectionUpdated回调中确认实际参数。 - 队列死锁:如果某个操作一直没回调,队列会卡死。加一个超时定时器,超时后跳过当前操作,继续执行下一个。
- 线程安全:所有对连接池和队列的操作,最好都切到同一个HandlerThread上执行。我习惯用
HandlerThread+Handler来序列化所有BLE操作。
好了,这一章的内容就到这里。记住,纸上得来终觉浅,绝知此事要躬行。赶紧打开Android Studio,动手写一个连接池试试吧。