什么是 GATT 服务和特征(Characteristic)?如何读写数据?

解读

国内 Android 面试中,一旦简历出现“低功耗蓝牙”或“BLE”字样,GATT 几乎是必考题。面试官想确认三件事:

  1. 你是否真的理解 BLE 的“服务端-客户端”模型,而不是把蓝牙当成串口;
  2. 你是否清楚 Android SDK 里 Service/Characteristic/Descriptor 三者的层级与 UUID 角色;
  3. 你是否踩过“写失败、收不到通知”这些国产机碎片化坑,并能给出定位思路。
    回答时先一句话点明 GATT 是 BLE 的通信协议,再分层解释 Service 与 Characteristic 的定义与作用,最后用“发现-读写-订阅”三步法把 API 串起来,并主动提国产 ROM 的兼容经验,基本就能拿到高分。

知识点

  1. GATT(Generic Attribute Profile)

    • 基于 ATT 协议,采用“服务端-客户端”架构;服务端是外围设备(如手环、门锁),客户端是手机。
    • 数据以“属性表”形式存在,每条属性用 16/128 bit UUID 标识,通过 Handle 寻址。
  2. Service

    • 一组相关 Characteristic 的集合,必须包含至少一个 Characteristic。
    • 标准 Service 如 0x180A(设备信息)、0x180D(心率),厂商可自定义 128 bit UUID。
  3. Characteristic

    • 最小数据单元,包含 Value、Properties、Security 三要素。
    • Properties 决定支持的操作:READ、WRITE、WRITE_NO_RESPONSE、INDICATE、NOTIFY 等。
    • 可附加 Descriptor(如 Client Characteristic Configuration,0x2902)用于开关通知。
  4. Android 关键类

    • BluetoothGatt:客户端角色,提供 connect()、discoverServices()、readCharacteristic()、writeCharacteristic()、setCharacteristicNotification()。
    • BluetoothGattServer:服务端角色,用于手机向外设广播数据。
    • 所有 I/O 都在 BluetoothGattCallback 异步回调中返回。
  5. 读写流程

    • 发现服务:onConnectionStateChange → discoverServices() → onServicesDiscovered。
    • 读:直接调用 readCharacteristic(),结果在 onCharacteristicRead。
    • 写:setValue() → writeCharacteristic(),结果在 onCharacteristicWrite;注意 WRITE_TYPE_DEFAULT 与 WRITE_TYPE_NO_RESPONSE 区别。
    • 订阅:setCharacteristicNotification(true) 后,还要写 0x2902 Descriptor 为 ENABLE_NOTIFICATION_VALUE(部分国产机不写无效)。
  6. 国内碎片化经验

    • 华为/小米后台扫描需要定位权限并弹窗授权。
    • 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 角色,流程如下:

  1. 通过 BluetoothDevice#connectGatt(Context, false, callback) 建立连接;
  2. 在 onConnectionStateChange 中调用 gatt.discoverServices(),待 onServicesDiscovered 回调后,使用 gatt.getService(UUID) 拿到目标 Service;
  3. 从 Service 中通过 getCharacteristic(UUID) 取得 Characteristic;
  4. 读数据:直接调用 gatt.readCharacteristic(characteristic),结果在 onCharacteristicRead 返回;
  5. 写数据:characteristic.setValue(byte[]) 设置数据,若需要无回复写则 characteristic.setWriteType(WRITE_TYPE_NO_RESPONSE),再调用 gatt.writeCharacteristic(characteristic),结果在 onCharacteristicWrite 回调;
  6. 订阅通知:先 gatt.setCharacteristicNotification(characteristic, true),再手动写 Client Characteristic Configuration Descriptor(UUID 0x2902)为 ENABLE_NOTIFICATION_VALUE,随后 onCharacteristicChanged 将收到外设主动推送的数据。

所有 API 均为异步,需在主线程外统一队列化,避免国产 ROM 的限频与写冲突问题。

拓展思考

  1. 如果手环一次广播 20 字节以上数据,如何利用“Write Long Characteristic”或“Prepare Write”分段?
  2. 当 MTU 协商到 247 字节后,如何动态调整 BLE 数据包大小以降低国产机重传率?
  3. Android 12 引入的 BLUETOOTH_CONNECT、BLUETOOTH_SCAN 运行时权限与“模糊定位”策略,对后台 BLE 保活有何影响?
  4. 在车载场景中,手机作为 GATT Server 同时广播多服务,如何与车机 ACL 链路共存并保证 4 dBm 发射功率不超标?