什么是 GATT 服务和特征(Characteristic)?如何读写数据?
解读
国内 Android 面试中,一旦简历出现“低功耗蓝牙”或“BLE”字样,GATT 几乎是必考题。面试官想确认三件事:
- 你是否真的理解 BLE 的“服务端-客户端”模型,而不是把蓝牙当成串口;
- 你是否清楚 Android SDK 里 Service/Characteristic/Descriptor 三者的层级与 UUID 角色;
- 你是否踩过“写失败、收不到通知”这些国产机碎片化坑,并能给出定位思路。
回答时先一句话点明 GATT 是 BLE 的通信协议,再分层解释 Service 与 Characteristic 的定义与作用,最后用“发现-读写-订阅”三步法把 API 串起来,并主动提国产 ROM 的兼容经验,基本就能拿到高分。
知识点
-
GATT(Generic Attribute Profile)
- 基于 ATT 协议,采用“服务端-客户端”架构;服务端是外围设备(如手环、门锁),客户端是手机。
- 数据以“属性表”形式存在,每条属性用 16/128 bit UUID 标识,通过 Handle 寻址。
-
Service
- 一组相关 Characteristic 的集合,必须包含至少一个 Characteristic。
- 标准 Service 如 0x180A(设备信息)、0x180D(心率),厂商可自定义 128 bit UUID。
-
Characteristic
- 最小数据单元,包含 Value、Properties、Security 三要素。
- Properties 决定支持的操作:READ、WRITE、WRITE_NO_RESPONSE、INDICATE、NOTIFY 等。
- 可附加 Descriptor(如 Client Characteristic Configuration,0x2902)用于开关通知。
-
Android 关键类
- BluetoothGatt:客户端角色,提供 connect()、discoverServices()、readCharacteristic()、writeCharacteristic()、setCharacteristicNotification()。
- BluetoothGattServer:服务端角色,用于手机向外设广播数据。
- 所有 I/O 都在 BluetoothGattCallback 异步回调中返回。
-
读写流程
- 发现服务:onConnectionStateChange → discoverServices() → onServicesDiscovered。
- 读:直接调用 readCharacteristic(),结果在 onCharacteristicRead。
- 写:setValue() → writeCharacteristic(),结果在 onCharacteristicWrite;注意 WRITE_TYPE_DEFAULT 与 WRITE_TYPE_NO_RESPONSE 区别。
- 订阅:setCharacteristicNotification(true) 后,还要写 0x2902 Descriptor 为 ENABLE_NOTIFICATION_VALUE(部分国产机不写无效)。
-
国内碎片化经验
- 华为/小米后台扫描需要定位权限并弹窗授权。
- OPPO/vivo 对 writeCharacteristic() 连续调用限频,需做 150 ms 节流。
- 部分机型通知开关写 Descriptor 会返回 GATT_WRITE_NOT_PERMITTED,需 catch 并降级到轮询。
答案
GATT(Generic Attribute Profile)是 BLE 的通信协议,把数据组织成“服务-特征”两级树形结构:
- Service 是逻辑功能单元,用 UUID 标识,例如 0x180D 代表心率服务;
- Characteristic 是服务里的最小数据点,包含真正的业务值、读写权限与通知能力,例如心率测量特征值 UUID 0x2A37。
在 Android 端,手机默认处于 GATT Client 角色,流程如下:
- 通过 BluetoothDevice#connectGatt(Context, false, callback) 建立连接;
- 在 onConnectionStateChange 中调用 gatt.discoverServices(),待 onServicesDiscovered 回调后,使用 gatt.getService(UUID) 拿到目标 Service;
- 从 Service 中通过 getCharacteristic(UUID) 取得 Characteristic;
- 读数据:直接调用 gatt.readCharacteristic(characteristic),结果在 onCharacteristicRead 返回;
- 写数据:characteristic.setValue(byte[]) 设置数据,若需要无回复写则 characteristic.setWriteType(WRITE_TYPE_NO_RESPONSE),再调用 gatt.writeCharacteristic(characteristic),结果在 onCharacteristicWrite 回调;
- 订阅通知:先 gatt.setCharacteristicNotification(characteristic, true),再手动写 Client Characteristic Configuration Descriptor(UUID 0x2902)为 ENABLE_NOTIFICATION_VALUE,随后 onCharacteristicChanged 将收到外设主动推送的数据。
所有 API 均为异步,需在主线程外统一队列化,避免国产 ROM 的限频与写冲突问题。
拓展思考
- 如果手环一次广播 20 字节以上数据,如何利用“Write Long Characteristic”或“Prepare Write”分段?
- 当 MTU 协商到 247 字节后,如何动态调整 BLE 数据包大小以降低国产机重传率?
- Android 12 引入的 BLUETOOTH_CONNECT、BLUETOOTH_SCAN 运行时权限与“模糊定位”策略,对后台 BLE 保活有何影响?
- 在车载场景中,手机作为 GATT Server 同时广播多服务,如何与车机 ACL 链路共存并保证 4 dBm 发射功率不超标?