四、BLE回调机制:BluetoothGattCallback的生命周期与回调处理
说实话,刚做BLE开发那阵子,我也是被回调搞晕过。你注册了一个回调,但啥时候触发?顺序对不对?哪个线程里跑?这些问题足足让我排查了两天。今天我把自己的经验整理出来,希望能帮你少走弯路。
生命周期回调总览
BluetoothGattCallback里一共定义了13个回调方法,但日常开发常用的就6个。我习惯把这6个方法按"流程阶段"归类:
| 回调方法 | 触发时机 | 我关注的点 |
|---|---|---|
| onConnectionStateChange | 连接/断开连接时 | 一定要检查status参数,status==0才是正常断开 |
| onServicesDiscovered | 服务发现完成后 | 建议在这里转发到主线程处理UI |
| onCharacteristicRead | 读取特征值返回时 | 注意分片读取的情况,MTU协商后有时也会触发这个 |
| onCharacteristicWrite | 写入特征值响应时 | 写入方式要匹配:WRITE_TYPE_NO_RESPONSE时不会触发 |
| onCharacteristicChanged | 收到通知/指示时 | 这是最常用的数据接收入口 |
| onDescriptorWrite | 配置通知描述符完成后 | 很多人忘了等这个回调就发数据,会出问题 |
典型处理流程:我踩过的坑与最佳实践
我在项目中遇到过一个问题:连续调用多个GATT操作,结果回调顺序全乱了。后来我悟出一个道理——BLE回调不是天然的"排队机制"。
来看一个基础但不严谨的写法:
// ❌ 不要这样写
gatt.discoverServices()
gatt.readCharacteristic(characteristic) // 可能services还没发现完
为什么会这样?因为discoverServices是异步的,你调用它之后系统立刻返回true,但onServicesDiscovered还没触发呢。这时候去读特征值,大概率读到的是不完整的数据。
我现在的处理办法是这样的:
核心回调处理代码模板
private val gattCallback = object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
when (newState) {
BluetoothProfile.STATE_CONNECTED -> {
// 连接成功,开始服务发现
gatt.discoverServices()
}
BluetoothProfile.STATE_DISCONNECTED -> {
// 连接断开,清理资源
closeGatt(gatt)
}
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// 服务发现完成,开始配置通知
val service = gatt.getService(UUID_SERVICE)
val characteristic = service.getCharacteristic(UUID_CHAR)
gatt.setCharacteristicNotification(characteristic, true)
// 还要写描述符
val descriptor = characteristic.getDescriptor(UUID_DESCRIPTOR)
descriptor.value = BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE
gatt.writeDescriptor(descriptor)
}
}
override fun onDescriptorWrite(gatt: BluetoothGatt, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// 描述符写入成功,可以开始读或写数据了
gatt.readCharacteristic(targetCharacteristic)
}
}
override fun onCharacteristicRead(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic, status: Int) {
if (status == BluetoothGatt.GATT_SUCCESS) {
val data = characteristic.value
// 处理数据,记得回到主线程更新UI
handler.post { updateUI(data) }
}
}
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
val data = characteristic.value
// 通知数据回调,注意线程安全问题
handler.post { notifyDataChanged(data) }
}
}
回调的线程模型,你搞懂了吗?
这里有个容易被忽略的点:所有这些回调默认跑在Binder线程池里,不是主线程。你想想看,如果直接在回调里更新UI,会怎么样?
没错,直接崩溃:CalledFromWrongThreadException。
解决方案其实很简单:用一个Handler把回调内容post到主线程。或者用Kotlin协程,把回调转成suspend函数。我在项目中倾向于后者,代码更简洁:
suspend fun readCharacteristicSuspend(gatt: BluetoothGatt, char: BluetoothGattCharacteristic): ByteArray {
return suspendCancellableCoroutine { cont ->
gatt.readCharacteristic(char)
val callback = object : BluetoothGattCallback() {
override fun onCharacteristicRead(
gatt: BluetoothGatt,
characteristic: BluetoothGattCharacteristic,
status: Int
) {
if (status == BluetoothGatt.GATT_SUCCESS) {
cont.resume(characteristic.value ?: ByteArray(0))
} else {
cont.resumeWithException(BleException("Read failed, status=$status"))
}
// 注意:这里要主动移除这个临时callback,否则会堆积
gatt.close()
}
}
cont.invokeOnCancellation {
gatt.close()
}
}
}
常见陷阱与避坑指南
- 回调顺序不保证:我曾经连续调用discoverServices和readCharacteristic,以为系统会排队执行。实际上,建议你用队列或者状态机显式控制流程。
- onCharacteristicWrite的status不是100%可靠:有些国产设备返回的status可能是非标准值,不要只检查GATT_SUCCESS,建议记录日志并加一层重试。
- onDescriptorWrite没写对就不能收发通知:很多初学者漏了这一步,结果设备发了数据但app收不到。特别注意:描述符的value要根据设备要求设置ENABLE_NOTIFICATION_VALUE或ENABLE_INDICATION_VALUE。
- 记得调用super:新版本的Android系统在回调里加了默认行为,漏掉super可能导致一些奇怪的兼容性问题。
回调机制的核心思维导图
整个回调流程我用一个SVG串了起来,你看一遍就能记住关键依赖关系:
聊聊测试中的回调验证
写了这么多回调代码,怎么确保它正确?我现在接手项目时,首先会给回调流程加日志:
override fun onCharacteristicChanged(gatt: BluetoothGatt, characteristic: BluetoothGattCharacteristic) {
Log.d(TAG, "onCharacteristicChanged | uuid=${characteristic.uuid} | len=${characteristic.value?.size}")
// 业务处理...
}
日志格式要统一,方便后续用logcat抓取分析。我常用的格式包含:回调名、UUID、数据长度、时间戳。这样在Compose快照回归测试中,可以直接断言某条日志是否按期望顺序出现。
好了,关于BluetoothGattCallback的生命周期和回调处理,核心要点都在这里了。你写回调时记住一个原则:每个回调都是一次独立的事件,不要假设前一个回调已经处理完。用好队列或状态机,你的BLE代码会稳定很多。