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)
    }
}
核心要点:连接池必须支持并发安全。我推荐使用ConcurrentHashMap,因为多连接场景下,回调线程和主线程会同时操作这个Map。

我在项目中遇到过一个问题:设备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是最后的底线,如果超过这个时间还没联系上,就默认对方挂断了。

我的经验:对于数据采集类设备(如心率带),建议用短间隔(15-30ms)+ 低延迟(0-1)。对于低功耗传感器(如温湿度计),可以用长间隔(100-200ms)+ 高延迟(5-10)。千万别一刀切!

我曾经踩过一个坑:某个手环在连接后,我设置了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 整体架构:一张图说清楚

下面这张图展示了多连接管理的整体流程。你可以看到,连接池、参数优化、操作队列三者是如何协同工作的。

BLE多连接管理架构图 连接池 Map<地址, Gatt> 状态机管理 连接参数优化 Interval / Latency Supervision Timeout GATT操作队列 串行执行 超时重试 协商 触发 物理设备层 设备A | 设备B | 设备C | ... 回调与事件分发 onConnectionStateChange | onCharacteristicRead | onCharacteristicWrite

从图中可以看到,连接池负责管理生命周期,参数优化负责协商通信节奏,操作队列保证指令有序执行。三者缺一不可。

13.5 实战避坑指南

  • 连接数限制:Android BLE最多支持7个并发连接(取决于芯片和驱动)。我建议实际项目中控制在4-5个,留出余量。
  • 参数协商失败:不是所有设备都支持你请求的参数。我曾经遇到一个设备,请求30ms间隔,它返回了100ms。一定要在onConnectionUpdated回调中确认实际参数。
  • 队列死锁:如果某个操作一直没回调,队列会卡死。加一个超时定时器,超时后跳过当前操作,继续执行下一个。
  • 线程安全:所有对连接池和队列的操作,最好都切到同一个HandlerThread上执行。我习惯用HandlerThread + Handler来序列化所有BLE操作。
总结一下:多连接管理没有银弹。连接池是骨架,参数优化是血肉,操作队列是神经。把这三块做好了,你的BLE App才能稳定运行。

好了,这一章的内容就到这里。记住,纸上得来终觉浅,绝知此事要躬行。赶紧打开Android Studio,动手写一个连接池试试吧。


公众号:蓝海资料掘金营,微信deep3321