Android BLE 断开重连为什么越来越不稳定

2 阅读8分钟

Android BLE 断开重连为什么越来越不稳定

Android BLE 最容易让人误判的一件事,就是第一次连接通常还挺顺,真正开始崩的是断开之后。

第一次扫到设备,连上,发现服务,开通知,发一条命令,收一条回包,看起来一切正常。然后你开始做真实业务:

  • 蓝牙设备主动断开一次
  • App 切到后台再回来
  • 用户手动点重连
  • 设备断电再开机
  • 页面退出再重新进入

这时候问题就开始出现了。第一次还能连,第二次偶尔失败,第三次越来越玄学,最后就冒出来各种熟悉的状态:连接超时、服务发现失败、通知没了、写入没回调、133

很多人会把这个问题直接归结成“Android BLE 就是不稳定”。这句话不算错,但也不够准确。更接近事实的说法是:Android BLE 的重连之所以越来越不稳定,很多时候不是因为扫描,而是因为连接状态、GATT 资源和重连时机都没收干净。

BLE 的重连问题,真正难的地方不在“怎么再调一次 connectGatt()”,而在“上一次连接到底结束干净了没有”。

1. 断开不等于释放

这是 BLE 重连里最常见的误区。

很多人看到设备断开后,会这样写:

bluetoothGatt?.disconnect()

然后下一次想连的时候,继续拿着原来的 BluetoothGatt 或者原来的状态往下走。这个思路通常会越来越不稳。

在 Android BLE 里,disconnect()close() 不是一回事。

  • disconnect() 是请求断开连接
  • close() 才是真正释放 GATT 相关资源

如果你只断开,不释放,系统蓝牙栈和你自己的代码里都可能还残留上一次连接状态。第一次也许没事,重连几次以后就开始乱。

更靠谱的做法是把资源释放写成统一动作:

private var bluetoothGatt: BluetoothGatt? = null

fun releaseGatt() {
    bluetoothGatt?.disconnect()
    bluetoothGatt?.close()
    bluetoothGatt = null
}

这段代码不复杂,但 BLE 稳定性很多时候就靠这种朴素动作续命。

2. 不要在断开回调里立刻疯狂重连

有些项目为了“自动重连”,会在 onConnectionStateChange() 一断开就立刻连回去:

override fun onConnectionStateChange(
    gatt: BluetoothGatt,
    status: Int,
    newState: Int
) {
    if (newState == BluetoothProfile.STATE_DISCONNECTED) {
        gatt.device.connectGatt(context, false, gattCallback)
    }
}

这类代码短期可能能跑,但长期很容易把状态搞脏。

原因很简单。设备刚断开的时候,系统蓝牙栈、底层连接资源、设备自己那边的状态都可能还没完全回到“干净可重连”的阶段。你在断开回调里立刻重新 connectGatt(),很多时候不是快,而是在抢时机。

更稳的做法通常是:

  1. 先标记状态为断开
  2. 释放旧的 GATT
  3. 稍微延迟一下
  4. 再决定是否重连

例如:

private val mainHandler = Handler(Looper.getMainLooper())

private fun scheduleReconnect(device: BluetoothDevice) {
    mainHandler.postDelayed({
        connect(device)
    }, 1000L)
}

这个 1 秒不是玄学常量,而是在给系统和设备一个缓冲时间。实际项目里这个值可以调,但“断开后不要立刻硬冲回去”这个原则通常是对的。

3. 同一个设备不要并发发起多个连接

很多重连不稳,其实不是断开的问题,而是你自己发出了多个连接请求。

最常见的几种情况:

  • 扫描结果回调里连一次
  • 用户点按钮又连一次
  • 自动重连任务再连一次
  • 页面恢复时又连一次

表面上你觉得自己只是在“确保能连上”,实际上系统看到的是同一个设备的多个连接尝试在互相打架。

所以 BLE 连接最好始终有一个明确的状态保护:

enum class BleConnectionState {
    Idle,
    Scanning,
    Connecting,
    Connected,
    Disconnecting,
    Disconnected
}

然后在真正发起连接前先卡住条件:

private var connectionState = BleConnectionState.Idle

fun connect(device: BluetoothDevice) {
    if (connectionState == BleConnectionState.Connecting ||
        connectionState == BleConnectionState.Connected
    ) {
        return
    }

    connectionState = BleConnectionState.Connecting
    bluetoothGatt = device.connectGatt(context, false, gattCallback)
}

很多 BLE 项目写到后面最缺的,不是某个 API,而是这种最基础的状态约束。

4. 旧回调没处理完,新的连接已经开始了

重连越来越不稳,还有一个很容易被忽略的问题:旧连接的回调可能还没完全结束,你的新连接已经开始了。

比如:

  • 上一次连接刚断
  • 某个 descriptor 写回调还没彻底清理
  • 当前操作队列还残留旧任务
  • 新连接已经重新 discover services

这时候你的逻辑上虽然觉得“我已经开始第二次连接了”,但代码内部其实还混着上一次的任务。最后的结果就是:

  • 旧操作影响新连接
  • 新连接被旧状态卡住
  • 队列推进顺序错乱

这也是为什么我一直更推荐把 BLE 操作收进一层 manager,再加一层操作队列,而不是把所有事情写在 Activity 或 Fragment 里。

断开时,除了 disconnect()close(),还要把这些东西一起清掉:

private val operationQueue: ArrayDeque<BleOperation> = ArrayDeque()
private var currentOperation: BleOperation? = null

private fun clearConnectionState() {
    operationQueue.clear()
    currentOperation = null
    bluetoothGatt = null
    connectionState = BleConnectionState.Disconnected
}

如果只关连接,不清队列,后面还是会越来越乱。

5. 自动重连最好有“为什么重连”的判断

自动重连不是一条简单规则,不是只要断开就重连。

至少要先分清几个情况:

  • 用户主动点断开
  • 设备异常断开
  • 超出范围断开
  • 蓝牙被系统关掉
  • App 主动退出页面

如果不分场景统一重连,就会出现一种很烦的体验:用户明明自己点了断开,系统还在后台拼命连。

更合理的做法是加一个重连原因判断:

private var shouldAutoReconnect = true

fun disconnectByUser() {
    shouldAutoReconnect = false
    releaseGatt()
}

private fun onDisconnected(device: BluetoothDevice) {
    releaseGatt()

    if (shouldAutoReconnect) {
        scheduleReconnect(device)
    }
}

这样你至少能区分“异常断开”和“用户主动断开”。

6. 不要把扫描、连接、重连混成一团

很多 BLE 代码后面越来越难维护,是因为扫描和重连写在同一套逻辑里。

比如:

  • 扫描结果一出来就 connect
  • 断开了就重新 startScan
  • 扫到设备又立刻 connect
  • connect 失败再继续扫

这套流程如果没有状态机,最终一定会变成一团。

更清晰一点的拆法通常是:

  • 扫描层:只负责发现设备
  • 连接层:只负责连接当前目标设备
  • 重连层:决定什么时候再次连接

比如你已经知道目标设备地址时,重连根本不一定需要重新扫描。直接拿地址或缓存设备对象尝试连接,通常会更稳。扫描更适合“找设备”,不是每次断线都先回到扫描状态。

7. 133 往往不是“突然出现”,而是坏状态堆出来的

BLE 重连问题里最著名的一个状态就是 133

很多人把它当成一个神秘错误码,其实它更像是一个总括性的失败信号。背后真正的问题可能是:

  • 旧 GATT 没释放
  • 重连太快
  • 多个连接请求并发
  • 设备状态没准备好
  • 队列没清理
  • 蓝牙栈临时异常

所以如果一个项目后期越来越容易出 133,通常不要先去找某个“一招修复”代码,而是先回头看连接生命周期是不是已经乱了。

8. 重连稳定性本质上是连接生命周期管理

如果把 BLE 连接拆开来看,它其实有一条非常明确的生命周期:

  1. 扫描或获取目标设备
  2. 发起连接
  3. 连接成功
  4. 发现服务
  5. 初始化通知 / MTU / 业务命令
  6. 进入 ready 状态
  7. 异常断开或主动断开
  8. 释放资源
  9. 决定是否重连

BLE 重连不稳,本质上往往不是第 9 步有问题,而是第 7 步和第 8 步没做好。

如果断开时没有真正清理,重连就是在脏状态上继续叠。

9. 一个更像样的重连骨架

如果你想把思路收紧一点,可以把重连流程写成这样:

class BleReconnectManager(
    private val context: Context
) {
    private var bluetoothGatt: BluetoothGatt? = null
    private var targetDevice: BluetoothDevice? = null
    private var shouldAutoReconnect = true
    private var state: BleConnectionState = BleConnectionState.Idle

    private val handler = Handler(Looper.getMainLooper())

    fun connect(device: BluetoothDevice) {
        if (state == BleConnectionState.Connecting ||
            state == BleConnectionState.Connected
        ) return

        targetDevice = device
        state = BleConnectionState.Connecting
        bluetoothGatt = device.connectGatt(context, false, gattCallback)
    }

    fun disconnectByUser() {
        shouldAutoReconnect = false
        releaseGatt()
        state = BleConnectionState.Disconnected
    }

    private fun releaseGatt() {
        bluetoothGatt?.disconnect()
        bluetoothGatt?.close()
        bluetoothGatt = null
    }

    private fun scheduleReconnect() {
        val device = targetDevice ?: return
        if (!shouldAutoReconnect) return

        handler.postDelayed({
            connect(device)
        }, 1000L)
    }

    private val gattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(
            gatt: BluetoothGatt,
            status: Int,
            newState: Int
        ) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                state = BleConnectionState.Connected
                gatt.discoverServices()
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                state = BleConnectionState.Disconnected
                releaseGatt()
                scheduleReconnect()
            }
        }
    }
}

这个版本当然还不完整,但至少它已经抓住了几个关键点:

  • 有明确状态
  • 有目标设备缓存
  • 用户主动断开和异常断开分开
  • 断开后先释放
  • 重连不直接硬冲

对于 BLE 来说,这种“看起来很普通”的结构,往往比一堆技巧更有用。

结尾

Android BLE 的重连为什么越来越不稳定,答案通常不是“第二次连接更难”,而是“第一次连接结束得不够干净”。

你如果只是会扫、会连、会 discover services,BLE 还只是刚入门。真正决定一个 BLE 通信层能不能长期跑稳的,往往是断开之后你怎么收尾,重连之前你怎么恢复状态。

扫描是入口,连接是开始,重连稳定性才是真正考验工程质量的地方。