2. GATT 对象概念
做 BLE 开发,GATT 是绕不开的核心概念。我早期刚接触蓝牙时,总把 GATT 跟 GAP 搞混。后来踩了几次坑才彻底搞明白——GATT 负责“数据怎么组织、怎么读写”,而 GAP 管的是“设备怎么发现、怎么连接”。这两者分工明确,不能混为一谈。
这章我们把 GATT 拆开揉碎,讲清楚它的角色、服务和特征。因为后面《LeakCanary 分析 GATT 对象泄漏链》的课程里,很多泄漏的根源就藏在 GATT 对象的生命周期里。你想想看,一个 Service 或 Characteristic 没被正确释放,整个蓝牙连接就可能卡住,甚至导致内存泄漏。
2.1 GATT 在 BLE 协议栈中的位置
BLE 协议栈是分层结构。从下往上:物理层 -> 链路层 -> HCI -> L2CAP -> ATT -> GATT。GATT 是最高层,直接面向开发者。
我个人的习惯是,把 GATT 想象成“数据库”。服务器(Server)里存着多张表(Service),每张表里有字段(Characteristic),字段还有属性(Descriptor)。客户端(Client)去查询或写入这些字段。
2.2 GATT 角色:Server 与 Client
GATT 定义了两种角色:
- GATT Server — 保存数据的一方。通常是某些传感器、外设。它把数据组织成 Service / Characteristic,供别人读写。
- GATT Client — 主动读写数据的一方。通常是手机 App。它发现 Server 上的 Service,然后读取或写入 Characteristic 的值。
| GAP 角色 | 常见 GATT 角色 | 典型设备 | 内存泄漏风险点 |
|---|---|---|---|
| Central | Client | 手机 | Client 端未释放已发现的 Service 引用 |
| Peripheral | Server | 心率带、温度计 | Server 端未销毁 GattService |
| Central (双模) | 亦可作为 Server | IoT 网关 | 同一对象同时持有 Client 和 Server 引用 |
2.3 服务、特征与描述符
GATT 数据模型是树形结构。我习惯把这些概念记成:
- Service(服务) — 功能模块。比如「电量服务」「设备信息服务」。每个 Service 由 16-bit 或 128-bit UUID 标识。
- Characteristic(特征) — 具体的数据点。比如「电量百分比」。一个 Service 包含多个 Characteristic。每个 Characteristic 也有 UUID、属性(可读、可写、通知等)和一个值。
- Descriptor(描述符) — 特征的补充说明。最常见的是 CCCD(客户端特征配置描述符),用于开启/关闭通知。
典型的结构代码
Android 中使用 BluetoothGatt 获取服务列表的流程:
// 假设 gatt 已经连接成功
gatt.discoverServices();
gatt.setBluetoothGattCallback(new BluetoothGattCallback() {
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
if (status == BluetoothGatt.GATT_SUCCESS) {
List<BluetoothGattService> services = gatt.getServices();
for (BluetoothGattService service : services) {
// 建议只记录 service 的 UUID,而非整个 service 引用
String uuid = service.getUuid().toString();
// 处理特征...
for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) {
// ...
}
}
}
}
});
2.4 为什么 GATT 对象容易泄漏?
GATT 对象泄漏的核心原因有两个:
- 生命周期不一致。 BluetoothGatt 是系统服务创建的 Binder 代理,它的生命周期由蓝牙连接状态决定。但开发者往往在 Activity 或 Fragment 的 onDestroy 中才去释放,如果中间重建了 Activity,旧 gatt 仍被回调持有,新 gatt 还在路上——泄漏就产生了。
- 回调引用泄漏。 BluetoothGattCallback 是个匿名内部类,默认持有外部类引用。如果你在 Activity 里 new 了 callback 却没及时解注册,Activity 就无法被 GC 回收。我在排查一个线上 OOM 问题时发现,用户连续开关蓝牙页面 20 次,堆里滞留了 20 个 BluetoothGattCallback 对象,每个都还牵着 Activity。
2.5 与 LeakCanary 的关系
LeakCanary 能检测到的 GATT 泄漏链,通常表现为:
Activity → BluetoothGattCallback → BluetoothGatt 或 Service → BluetoothGattService → GattCharacteristic。
当你看到泄漏链中包含 BluetoothGatt 或 BluetoothGattService 时,基本就是 GATT 对象没有被正确关闭或引用没清空。
接下来几章,我们会用 LeakCanary 实际抓一个 GATT 泄漏案例,一步步分析引用链。现在先把这些基础概念刻在脑子里——别等到看堆栈时才去想 Service 和 Characteristic 是什么。