连接与断开:BluetoothDevice、connectGatt、BluetoothGattCallback、连接状态管理、重连机制

好,咱们进入正题。上一章我们搞定了扫描,拿到了设备对象——BluetoothDevice。接下来要做的,就是跟这个设备建立连接。

连接这块,说简单也简单,调一个 connectGatt 就完事了。但说复杂也复杂——连接失败怎么办?断连了怎么处理?要不要自动重连?这些坑,我当年都踩过。

1. BluetoothDevice:你扫描到的那个“人”

BluetoothDevice 是什么?说白了,它就是远端蓝牙设备的一个抽象。你通过扫描回调拿到它,然后拿着它去建立连接。

它有几个关键信息:

  • 地址device.address,比如 00:11:22:33:44:55
  • 名称device.name,可能为 null
  • 绑定状态device.bondState

我个人习惯,在扫描到设备后,先把地址和名称存下来。万一后面连接出问题,还能根据地址重试。

小提示BluetoothDevice 是系统管理的对象,同一个物理设备在不同时间扫描到,可能是同一个实例(系统会复用)。所以你可以放心用它做 key 来管理连接状态。

2. connectGatt:发起连接的入口

拿到 BluetoothDevice 后,调用它的 connectGatt 方法就能发起连接。这个方法有多个重载,最常用的是这个:

fun connectToDevice(device: BluetoothDevice, context: Context): BluetoothGatt? {
    return device.connectGatt(
        context,
        false,  // autoConnect
        object : BluetoothGattCallback() {
            // 回调处理
        }
    )
}

这里有个关键参数 autoConnect。我刚开始做 BLE 时,一直没搞懂它到底干嘛的。

  • autoConnect = false:直接发起连接,如果连不上就报错。适合你主动去连一个已知在附近的设备。
  • autoConnect = true:系统会在后台监听设备广播,等设备出现时自动连接。适合需要长期监听、设备可能不在身边的场景。

嗯,这里要注意:autoConnect = true 的连接速度很慢,因为系统要等广播。我一般在需要快速交互的场景下都用 false

3. BluetoothGattCallback:连接后的“消息中心”

连接不是一锤子买卖。你发起连接后,系统会通过 BluetoothGattCallback 告诉你结果。这个回调接口里,你必须关注这几个方法:

回调方法 触发时机 说明
onConnectionStateChange 连接状态变化时 最重要的回调,连接成功/断开都走这里
onServicesDiscovered 服务发现完成 连接成功后,需要调用 discoverServices 触发
onCharacteristicRead 读取特征值完成 读写操作的结果回调
onCharacteristicWrite 写入特征值完成 同上

你想想看,整个 BLE 通信的生命周期,其实都围绕着这个回调展开。所以设计好回调的处理逻辑,是重中之重。

4. 连接状态管理:别让状态乱掉

我在项目中遇到过一个问题:用户快速点击连接/断开按钮,结果状态乱了——界面显示已连接,实际上已经断开了。

为什么会这样?因为 onConnectionStateChange 是异步回调,你点一下连接,系统还没回调,你又点了一下断开,这时候状态就打架了。

我的做法是:用一个状态机来管理连接状态

enum class ConnectionState {
    DISCONNECTED,
    CONNECTING,
    CONNECTED,
    DISCONNECTING
}

class BleManager {
    private var connectionState = ConnectionState.DISCONNECTED
    private var gatt: BluetoothGatt? = null

    fun connect(device: BluetoothDevice) {
        if (connectionState != ConnectionState.DISCONNECTED) {
            // 已经在连接中或已连接,忽略
            return
        }
        connectionState = ConnectionState.CONNECTING
        gatt = device.connectGatt(context, false, callback)
    }

    private val callback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            when (newState) {
                BluetoothProfile.STATE_CONNECTED -> {
                    connectionState = ConnectionState.CONNECTED
                    // 通知 UI
                }
                BluetoothProfile.STATE_DISCONNECTED -> {
                    connectionState = ConnectionState.DISCONNECTED
                    // 清理资源
                }
            }
        }
    }
}

这样,不管用户怎么点,只要状态不是 DISCONNECTED,新的连接请求就会被忽略。状态不会乱。

注意onConnectionStateChange 中的 status 参数也很重要。如果 status != GATT_SUCCESS,说明连接过程出错了(比如超时、设备不在范围内)。这时候即使 newState == STATE_DISCONNECTED,你也应该做特殊处理,比如提示用户或触发重连。

5. 重连机制:让连接更健壮

BLE 连接其实挺脆弱的。设备走远了、信号被遮挡、甚至周围蓝牙干扰太多,都可能导致断连。

我曾经做过一个项目,设备在口袋里,手机在手上,走几步就断连了。用户反馈说「怎么老断?」,我只好加上重连机制。

重连的核心思路很简单:

  1. 检测到断连
  2. 判断是否需要重连(比如用户正在操作、设备在重连白名单里)
  3. 延迟一段时间后重新调用 connectGatt
  4. 设置最大重连次数,避免无限重试

代码大概长这样:

class ReconnectManager(private val maxRetries: Int = 3) {
    private var retryCount = 0
    private var handler = Handler(Looper.getMainLooper())

    fun onDisconnected(device: BluetoothDevice) {
        if (retryCount < maxRetries) {
            retryCount++
            handler.postDelayed({
                // 重新连接
                device.connectGatt(context, false, callback)
            }, 2000) // 延迟2秒
        } else {
            // 超过最大重试次数,放弃
            retryCount = 0
            // 通知 UI 连接失败
        }
    }

    fun onConnected() {
        retryCount = 0
        handler.removeCallbacksAndMessages(null)
    }
}

这里有几个细节:

  • 延迟时间:我一般用 1-3 秒。太短了会频繁重试,耗电;太长了用户等不及。
  • 重连次数:3 次差不多了。超过 3 次还连不上,说明设备真的不在附近,别白费力气。
  • 清除回调:连接成功后,记得把 pending 的重连任务取消掉,不然会重复连接。

6. 断开连接:优雅地收尾

断开连接比连接简单,但也有一些讲究。

fun disconnect() {
    gatt?.let {
        it.disconnect()  // 发起断开请求
        it.close()       // 释放资源
        gatt = null
    }
    connectionState = ConnectionState.DISCONNECTED
}

注意顺序:先 disconnect,再 closedisconnect 是告诉系统我要断开,close 是释放 BluetoothGatt 占用的资源。如果先 closedisconnect,系统可能会报错。

核心要点BluetoothGatt 对象在用完后一定要 close,否则会泄漏。每个 connectGatt 调用都会创建一个新的 BluetoothGatt 实例,不 close 的话,内存会越占越多。

7. 整体流程梳理

说了这么多,咱们用一张图把整个连接断开流程串起来:

BLE 连接与断开流程 扫描到设备 connectGatt onConnectionStateChange 连接成功 → discoverServices 连接失败 → 重连/提示 断开连接 → close

这张图把整个流程串起来了。从扫描到设备开始,调用 connectGatt,然后在回调里判断成功还是失败。成功了就去发现服务,失败了就触发重连。断开连接时记得 close 释放资源。

8. 避坑指南

最后,分享几个我踩过的坑:

  • 不要在主线程做耗时操作onConnectionStateChange 虽然跑在 Binder 线程,但如果你在里面做数据库操作或网络请求,还是可能卡住。我一般用 Handler 切到子线程处理。
  • 注意 Android 版本差异:Android 10 以后,后台扫描和连接的限制更严了。如果你的 App 在后台,连接可能会失败。记得处理好 onConnectionStateChange 中的 status 为 133 的情况(这是系统拒绝连接的标志)。
  • 多设备连接时注意区分:如果你同时连接多个设备,每个设备都有自己的 BluetoothGatt 和回调。别搞混了。我习惯用 device.address 作为 key 来管理。

好了,连接与断开这块就讲到这里。下一章我们会深入 BluetoothGatt 的细节,看看怎么发现服务、读写特征值。到时候见。


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