6. BLE GATT客户端开发:连接、发现服务、读写与通知

各位同学,今天我们来聊聊BLE开发中最核心的部分——GATT客户端开发。说白了,就是你的手机作为中心设备,去连接外围设备(比如手环、传感器),然后读写数据。

我在做智能穿戴项目时,发现很多开发者把精力都花在了UI上,结果连最基本的连接都做不稳定。嗯,这其实是个大坑。今天我就把GATT客户端的几个关键操作,掰开揉碎了讲清楚。

核心知识点一览:

  • BluetoothGatt的连接与断开
  • 发现服务(discoverServices)
  • 读写Characteristic
  • 开启Notification/Indication
GATT客户端开发 连接与断开 发现服务 读写Characteristic Notification/Indication connectGatt disconnect onServicesDiscovered readCharacteristic writeCharacteristic setCharacteristicNotification

6.1 连接与断开——基础中的基础

先说说连接。调用 BluetoothDevice.connectGatt() 时,很多人以为传个参数就完事了。其实这里有个关键点——autoConnect 参数。

// 标准连接方式
BluetoothGatt gatt = device.connectGatt(context, false, gattCallback);

// 自动重连模式
BluetoothGatt gatt = device.connectGatt(context, true, gattCallback);

我个人习惯在大多数场景下用 false。为什么?因为 true 模式(自动重连)在Android系统上表现不太稳定。我曾经在一个项目中用了自动重连,结果设备断开后系统一直在后台疯狂尝试连接,导致手机发热、电量狂掉。后来改成手动重连,问题就解决了。

小技巧:断开连接时,记得调用 gatt.close() 释放资源。很多开发者只调了 disconnect(),结果导致内存泄漏。我见过一个App,连续连接断开20次后直接OOM了。

6.2 发现服务——拿到设备的"目录"

连接成功后,第一件事就是发现服务。这就像你拿到一本新书,先翻翻目录看看有哪些章节。

@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    if (newState == BluetoothProfile.STATE_CONNECTED) {
        // 连接成功,开始发现服务
        gatt.discoverServices();
    }
}

你想想看,如果不先发现服务,你连设备支持哪些功能都不知道,怎么读写数据?

发现服务的结果会回调到 onServicesDiscovered()。这里有个坑——这个回调可能不会在主线程执行。所以如果你要在回调里更新UI,记得切到主线程。

注意:discoverServices() 是一个异步操作。不要连续调用两次,否则第二次会覆盖第一次的结果。我见过有人在一个循环里调了三次,结果回调只触发了一次。

6.3 读写Characteristic——真正的数据交互

拿到服务列表后,就可以找到你需要的Characteristic了。读写操作是BLE通信的核心。

读取数据

BluetoothGattService service = gatt.getService(UUID.fromString("你的服务UUID"));
BluetoothGattCharacteristic characteristic = service.getCharacteristic(UUID.fromString("你的特征UUID"));
gatt.readCharacteristic(characteristic);

读取结果在 onCharacteristicRead() 回调中获取。这里要注意——一次只能发起一个读操作。如果你连续调用两次 readCharacteristic(),第二次会被忽略。

写入数据

characteristic.setValue(data);
characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT);
gatt.writeCharacteristic(characteristic);

写入类型有两种:

写入类型 特点 适用场景
WRITE_TYPE_DEFAULT 需要等待设备确认 关键数据写入(如配置参数)
WRITE_TYPE_NO_RESPONSE 不需要确认,速度快 大量数据流(如OTA升级)

我个人建议:关键操作用带响应的写入。为什么?因为不带响应的写入,你根本不知道设备有没有收到。我曾经在做一个智能锁项目时,用了无响应写入来发送开锁指令,结果有5%的概率锁没打开——因为数据包丢了。后来改成带响应的写入,问题就解决了。

6.4 开启Notification/Indication——被动接收数据

很多时候,你不想一直轮询读取数据,而是希望设备主动通知你。这时候就需要开启Notification或Indication。

// 第一步:开启本地通知
gatt.setCharacteristicNotification(characteristic, true);

// 第二步:写入CCCD(客户端特征配置描述符)
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
    UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(descriptor);

这里有个常见的坑——很多人只调了 setCharacteristicNotification(),忘了写CCCD。结果就是:回调没反应,数据收不到。

Notification vs Indication 的区别:

  • Notification:设备发送数据后,不需要手机确认。速度快,但可能丢包。
  • Indication:设备发送数据后,必须等手机确认,才会发下一条。可靠,但速度慢。

我记得在一个心率监测项目中,设备每秒钟发送一次心率数据。如果用Indication,因为要等待确认,实际速率降到了每秒2-3次。后来改成Notification,速率就正常了。不过对于关键数据(比如血糖仪的报警),还是用Indication更安全。

避坑指南:我曾经遇到过一个设备,开启Notification后收不到数据。折腾了半天,发现是设备的CCCD写入需要延迟100ms才能生效。所以在 onDescriptorWrite() 回调之后再开始监听,会更稳妥。

6.5 完整流程总结

好了,我们把整个流程串起来看看:

  1. 连接设备connectGatt()
  2. 发现服务discoverServices()
  3. 获取Characteristic → 从Service中查找
  4. 读写数据readCharacteristic() / writeCharacteristic()
  5. 开启通知setCharacteristicNotification() + 写CCCD
  6. 断开连接disconnect() + close()

每一步都有它的讲究。你想想看,如果连接都做不好,后面的操作全是白搭。所以我的建议是:先把连接和断开的逻辑写稳了,再考虑其他功能

最后说一句:BLE开发没有捷径,多踩坑、多总结,慢慢就熟练了。希望今天的内容对你有帮助。


公众号:蓝海资料掘金营,微信deep3321