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(),很多时候不是快,而是在抢时机。
更稳的做法通常是:
- 先标记状态为断开
- 释放旧的 GATT
- 稍微延迟一下
- 再决定是否重连
例如:
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 连接拆开来看,它其实有一条非常明确的生命周期:
- 扫描或获取目标设备
- 发起连接
- 连接成功
- 发现服务
- 初始化通知 / MTU / 业务命令
- 进入 ready 状态
- 异常断开或主动断开
- 释放资源
- 决定是否重连
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 通信层能不能长期跑稳的,往往是断开之后你怎么收尾,重连之前你怎么恢复状态。
扫描是入口,连接是开始,重连稳定性才是真正考验工程质量的地方。