1.1 BLE GATT 回调机制:原理与核心要素

BLE GATT(通用属性协议)回调是 Android 蓝牙框架中 主机(Central/GATT Client) 与 外设(Peripheral/GATT Server) 异步通信的关键桥梁。当应用发起属性读写、通知使能、MTU 更新等操作时,系统不会立即返回结果,而是通过注册的 BluetoothGattCallback 代理对象,在特定事件触发时回调对应方法。

理解回调的四种核心维度——类型、线程、数据格式、陷阱,是后续构建可靠 Room 存储管道的基础。

1.1.1 回调类型

回调方法 触发条件 典型用途
onConnectionStateChange 连接建立 / 断开 / 重连 管理连接状态机
onServicesDiscovered 服务发现完成 获取 Service / Characteristic 句柄
onCharacteristicRead 读取特征值成功 获取传感器数据、设备信息
onCharacteristicWrite 写入特征值完毕 确认配置指令已送达
onCharacteristicChanged 外设主动推送通知 / 指示 实时数据流(心率、温度、姿态)
onDescriptorWrite 配置 CCCD 完成 使能 / 禁用通知通道
onMtuChanged MTU 协商结束 调整最大单包传输长度

1.1.2 回调线程

所有 GATT 回调默认在 系统 Binder 线程池 中执行,并非主线程(UI 线程)。这意味着:

  • 回调内部不可以直接更新 UI / 访问 View;
  • 不可以执行长时间阻塞操作(如文件 I/O、网络请求);
  • 必须通过 Handler / LiveData / Coroutine 切换到指定线程再处理数据。

线程切换示例(Handler 方案)

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

override fun onCharacteristicChanged(
    gatt: BluetoothGatt,
    characteristic: BluetoothGattCharacteristic
) {
    val data = characteristic.value
    mainHandler.post {
        // 在此处安全更新 UI 或发射 LiveData
        viewModel.onDataReceived(data)
    }
}

1.1.3 回调数据格式

特征值数据通过 characteristic.value 属性返回,类型为 ByteArray。常见数据格式与解析场景:

数据场景 长度 解析方法 示例(原始 Hex)
心率测量 2~8 字节 按 BLE 心率规范解析 Flags + HR Value 16 54 → 84 bpm
温度传感器 4 字节 Little-Endian Int32 / Float32 E1 7A 48 42 → 50.12°C
自定义指令 1~512 字节 按协议头+负载解析 AA 01 02 03 55 → 命令=0x01

注意:characteristic.value 在 API 33+ 可能为 null,应使用 characteristic.getValue() 替代。

1.1.4 常见回调陷阱

  • 陷阱 1:多次回调同一特征值
    连续两次调用 readCharacteristic 可能导致第二次回调丢失或数据错乱。应使用排队机制确保同一特征值串行读取。
  • 陷阱 2:回调未释放导致内存泄漏
    在 Activity/Fragment 销毁后未调用 gatt.close(),回调中持有 View 引用引发泄漏。应在 onDestroy 中主动清理。
  • 陷阱 3:多特征通知乱序
    多个特征同时使能通知时,onCharacteristicChanged 可能交叉触发。需在回调入口通过 characteristic.uuid 做分发路由。
  • 陷阱 4:线程安全
    回调在 Binder 线程执行,如果直接写入 Room DAO 而不做线程切换,会触发 IllegalStateException: Cannot access database on main thread 或死锁。

特别注意:Android 12+ 对后台 BLE 扫描和连接有严格限制。若 App 在后台收到回调后立即写入 Room,可能因进程优先级被降级导致写入失败。建议使用 WorkManager 延迟处理离线数据。

1.1.5 回调全流程关系图

下图展示了典型 BLE GATT 回调的完整数据流向,从外设事件到应用层数据解析,以及最终进入 Room 存储的必经路径。

BLE GATT 回调全流程与数据流向 BLE 外设 (GATT Server) 系统 Binder 线程池 (回调执行线程) Handler / Coroutine (线程切换层) 切换至主线程或 IO 线程 数据分发/协议解析 按 UUID 路由、字节解析 Room 数据库 (异步 DAO 写入) ⚠ 常见回调陷阱分布 • 陷阱1:多次读取同一特征 → 在 Binder 线程池添加排队队列(Node: Binder 线程池) • 陷阱2:内存泄漏 → 在 Handler/Coroutine 切换层检查生命周期 • 陷阱3:多特征通知乱序 → 在数据分发层按 UUID 分组 • 陷阱4:线程安全 Room 写入 → 必须从 IO 线程而非 Binder 线程操作 DAO

1.1.6 实践要点:回调与 Room 的第一层对接

在回调函数中接收数据后,应立刻完成以下两件事:

  1. 数据校验:检查 status 是否为 GATT_SUCCESS,特征值非空且长度合理;
  2. 线程切换:将解析后的数据对象传递给 ViewModel,由 ViewModel 在 IO 协程中调用 Room DAO 写入。

以下是一个最小可用实现框架(省略了 Service/Characteristic 查找):

override fun onCharacteristicChanged(
    gatt: BluetoothGatt,
    characteristic: BluetoothGattCharacteristic
) {
    if (characteristic.uuid == HEART_RATE_MEASUREMENT_UUID) {
        val hrData = parseHeartRate(characteristic.value)
        viewModelScope.launch(Dispatchers.IO) {
            heartRateDao.insert(hrData)
        }
    }
}