GATT 特征读写操作:readCharacteristic 与 writeCharacteristic 详解

好,咱们进入正题。前面我们已经学会了如何连接设备、发现服务、拿到特征对象。但说实在的,连接只是开始,真正的“干活”就是读写特征。你回想一下,那些蓝牙心率计、温湿度传感器、智能锁——哪个不是靠读写特征来交互的?今天我详细拆解这两个 API,顺便把我在踩过的坑分享给你们。

readCharacteristic:发起读取请求

先看读。方法签名特别简单:

public boolean readCharacteristic(BluetoothGattCharacteristic characteristic)

参数就是你要读的那个特征对象。返回值是 boolean,反馈命令有没有成功发送出去。注意,它只是“发送读取请求”,真正拿到数据要等 onCharacteristicRead 回调。

核心要点:readCharacteristic 是非阻塞的,调用后立刻返回。数据到达时系统会回调 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 时回调才会被触发。如果你用了无响应写,既不会有成功回调也不会有失败回调——跟扔进黑洞一样。我当初第一次做蓝牙发数据,选的无响应写入,一直等回调,结果等到超时……后来才明白这个设计意图。

避坑提醒:我曾经在一个项目里连续调用 writeCharacteristic 两次,间隔只有几毫秒。结果第二条数据被蓝牙栈丢弃了。原因:底层队列满了。解决办法是维护一个“写锁”,等上一次回调完成后再发下一条。

完整代码示例:读+写特征

// 读取特征
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 次就断开连接了。
我的小技巧:用队列封装所有操作。把读、写请求做成一个个任务塞进队列,每次只弹出一个执行,等到回调后再弹下一个。这样既不会丢命令,也不用担心并发。我在多个项目中都用这套模式,很稳。

知识体系图:特征读写核心流程

下面这张图把读写特征的完整流程串起来了。你看一眼就能明白每一步该做什么、回调在哪。

特征读写完整流程 1. 连接设备 (onConnectionStateChange) 2. 发现服务 (onServicesDiscovered) 3. 获取特征对象 4. readCharacteristic 5. writeCharacteristic 6. onCharacteristicRead 7. onCharacteristicWrite 8. 解析数据 / 业务处理 ⚠️ 写回调仅对 WRITE_TYPE_DEFAULT 类型有效 建议:使用队列 保证读写串行

好了,读和写这两个最基础的 API 我们就讲完了。你可能会觉得“不就是调用几个方法吗?”但实际做起来坑真不少。我建议你把今天的代码跑一遍,把回调 log 打全,看看读成功、写失败时分别是什么样子。下次遇到问题,你心里就有底了。