GATT 特征读写操作:readCharacteristic 与 writeCharacteristic 详解
好,咱们进入正题。前面我们已经学会了如何连接设备、发现服务、拿到特征对象。但说实在的,连接只是开始,真正的“干活”就是读写特征。你回想一下,那些蓝牙心率计、温湿度传感器、智能锁——哪个不是靠读写特征来交互的?今天我详细拆解这两个 API,顺便把我在踩过的坑分享给你们。
readCharacteristic:发起读取请求
先看读。方法签名特别简单:
public boolean readCharacteristic(BluetoothGattCharacteristic characteristic)
参数就是你要读的那个特征对象。返回值是 boolean,反馈命令有没有成功发送出去。注意,它只是“发送读取请求”,真正拿到数据要等 onCharacteristicRead 回调。
BluetoothGattCallback#onCharacteristicRead。
我曾经遇到一个问题:调用 readCharacteristic 返回 true,可回调死活不来。查了半天,原来是设备在另一个线程上断开连接了,而我还在傻等。所以你记住:读操作之前一定要确保 GATT 连接是活跃的,最好用状态机检查一下。
onCharacteristicRead 回调处理
回调方法长这样:
@Override
public void onCharacteristicRead(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
byte[] value = characteristic.getValue();
// 处理数据
} else {
// 读失败,做重试或提示
}
}
status 只有 GATT_SUCCESS 时数据才有意义。其它值(比如 GATT_READ_NOT_PERMITTED 或 GATT_INSUFFICIENT_AUTHENTICATION)说明权限不够或者特征不可读。我看过很多新手直接拿 characteristic.getValue() 不管 status,结果拿到的是上次旧数据。
嗯,这里要注意一个细节:getValue() 返回的是 byte[],你需要根据特征的格式解析。比如有的特征是 uint16,有的是一串 UTF-8 字符串。我曾有个项目,心率计返回的是 2 字节的大端格式,忘记做字节序转换,读出来永远是 0……后来调试了一下午。
writeCharacteristic:写入数据的两种姿势
写操作比读稍微复杂一点,因为要考虑写类型。
public boolean writeCharacteristic(BluetoothGattCharacteristic characteristic,
byte[] value,
int writeType)
这里 writeType 有两个常用选项:
| 写类型常量 | 含义 | 特点 |
|---|---|---|
BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT | 带响应写入 | 发送后等设备确认,否则回调失败。可靠但慢。 |
BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE | 无响应写入 | 发送即完成,不回回调。快但不可靠。 |
我个人习惯:如果是控制命令(比如开锁、切换模式),用 WRITE_TYPE_DEFAULT 保证成功;如果是大量数据(比如 OTA 升级包),用 WRITE_TYPE_NO_RESPONSE 提高速度。你怎么选?得看设备协议。
写回调:onCharacteristicWrite
@Override
public void onCharacteristicWrite(BluetoothGatt gatt,
BluetoothGattCharacteristic characteristic,
int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
// 写入成功
} else {
// 写入失败,可能需要重试
}
}
注意:只有你用了 WRITE_TYPE_DEFAULT 时回调才会被触发。如果你用了无响应写,既不会有成功回调也不会有失败回调——跟扔进黑洞一样。我当初第一次做蓝牙发数据,选的无响应写入,一直等回调,结果等到超时……后来才明白这个设计意图。
完整代码示例:读+写特征
// 读取特征
boolean success = bluetoothGatt.readCharacteristic(characteristic);
if (success) {
Log.d(TAG, "读取请求已发出,等待回调...");
} else {
Log.e(TAG, "读取请求发送失败");
}
// 写入特征
byte[] data = new byte[]{0x01, 0x02};
characteristic.setValue(data);
boolean writeSuccess = bluetoothGatt.writeCharacteristic(characteristic);
if (writeSuccess) {
// 等待 onCharacteristicWrite
}
这里有个隐藏细节:写入前先调用 characteristic.setValue() 把数据设置进去,然后再写。有些人忘了这一步,结果空数据发出去了。别笑,我就在测试时犯过这个低级错误。
读写操作的最佳实践
- 状态机管理:不要在任何状态下都执行读写。只有 GATT 连接成功且服务发现完成后才能操作。我通常用枚举(CONNECTED、SERVICES_DISCOVERED、READING、IDLE 等)控制。
- 超时处理:读或写请求发出后,如果 10 秒内没收到回调,应该超时重置。Android 蓝牙栈偶尔会“丢回调”,你不能无限等待。
- 线程模型:所有 GATT 操作必须在主线程或同一线程串行执行。不要从子线程直接调这些 API,会出并发问题。我用 Handler 或协程封装。
- 重试机制:如果写失败,不要立即重试。等几百毫秒后再试。我见过一些设备在批量写入时,连续失败 3 次就断开连接了。
知识体系图:特征读写核心流程
下面这张图把读写特征的完整流程串起来了。你看一眼就能明白每一步该做什么、回调在哪。
好了,读和写这两个最基础的 API 我们就讲完了。你可能会觉得“不就是调用几个方法吗?”但实际做起来坑真不少。我建议你把今天的代码跑一遍,把回调 log 打全,看看读成功、写失败时分别是什么样子。下次遇到问题,你心里就有底了。