1. GATT协议基础

做BLE开发这么多年,我遇到过不少回调里协程泄漏的案子。要搞清楚这个问题,得先把GATT协议的老底摸清楚。你想想看,连Service和Characteristic都分不清,怎么去排查回调里的协程泄漏?

我个人习惯是,学任何协议都先看它的层级结构——这就像盖楼,得先知道地基在哪、哪层是承重墙。GATT也是这样,它并不是孤立存在的,而是整个BLE协议栈里最上面那层。

BLE协议栈架构与GATT层级结构 BLE 协议栈 物理层 (Physical) 链路层 (Link Layer) HCI L2CAP ATT GATT GATT 层级结构 Profile (配置文件) Service (服务) Characteristic (特征) Descriptor (描述符) 承载关系 关键要点 BLE应用开发主要关注GATT层及以上;Service/Characteristic/Descriptor是核心数据模型 每个Service和Characteristic通过UUID唯一标识,Descriptor描述特征的属性或配置

1.1 BLE协议栈架构

BLE协议栈从下往上分了好几层,每层各司其职。我刚开始做蓝牙开发时,总觉得分层是理论派的东西,直到有一次设备老是断连,我花了三天定位,最后发现是链路层的连接参数没配好——从那以后,我再也不敢轻视每一层了。

从底往上,最核心的几层是这样的:

  • 物理层(Physical Layer)——负责2.4GHz频段上的无线收发。说白了就是管天线的活,调制解调、跳频都在这一层。
  • 链路层(Link Layer)——管理连接状态、广播、扫描、连接参数。你平时调的那个连接间隔,就在这里控制。
  • HCI(Host Controller Interface)——芯片内部主机和控制器之间的通道。嗯,大部分应用开发者不太直接碰它,但底层驱动少不了。
  • L2CAP——负责数据的封装和分段重组。它给上层提供了逻辑通道,ATT的数据就是通过它传的。
  • ATT(Attribute Protocol)——属性协议,定义了如何读写属性数据。GATT就是基于ATT的。
  • GATT(Generic Attribute Profile)——咱们这节课的主角,定义了属性数据组织成Service、Characteristic的规范。

这里要注意,GATT和ATT经常被混着说,其实ATT是传输协议,GATT是数据组织规范。我在项目评审时发现不少人搞混它俩,结果代码里把ATT的回调当GATT用,调试时一头雾水。

1.2 GATT角色与Profile

GATT通信中有两个核心角色:Server和Client。

  • GATT Server——持有数据的一方,提供Service/Characteristic供对方读写。通常传感器设备作为Server。
  • GATT Client——发起读写请求的一方,从Server获取数据。手机App一般是Client。

我在项目中遇到过这样一个场景:一个设备同时需要提供数据又需要读取另一个设备的数据,那它就同时充当Server和Client。这种"双重身份"在复杂拓扑里很常见,不过也容易搞混回调的归属——嗯,这也是协程泄漏的潜在温床。

再说说Profile(配置文件)。Profile不是代码里的实体,而是一份规范文档,定义了设备应该包含哪些Service,以及它们的行为。比如心率Profile规定必须有心率测量Service和设备信息服务。说白了,Profile就是"设备功能说明书"。

小提示: 实际开发中不要自己发明Profile,优先使用蓝牙SIG标准Profile。实在不够用才自定义,但UUID的格式和分配要严格遵循规范。

1.3 Service/Characteristic/Descriptor结构

这是GATT最核心的数据模型,也是排查回调协程泄漏时最需要熟悉的结构。我把它叫做"三层汉堡":

  • Service(服务)——一组相关功能的集合。比如电池服务包含电量百分比和电量状态两个数据点。
  • Characteristic(特征)——单个数据点,是读写操作的最小单元。每个Characteristic有一个值(Value)和若干属性(Properties),比如可读、可写、可通知。
  • Descriptor(描述符)——描述Characteristic的额外信息。最常见的CCCD(Client Characteristic Configuration Descriptor)用来开启通知或指示。

这三者的包含关系:一个Profile包含多个Service,一个Service包含多个Characteristic,一个Characteristic可以包含多个Descriptor。

我曾经在一个项目中,把所有数据都塞进一个Service里,结果Characteristic数量炸了,管理起来痛苦不堪。后来才意识到,合理的Service划分——比如按功能模块拆——对代码可维护性有多重要。那次重构之后,回调逻辑也清晰多了。

层级 作用 包含关系 示例
Service 功能集合,按模块组织数据 包含多个Characteristic 电池服务 (0x180F)
Characteristic 单个数据点,可读/写/通知 从属于某个Service 电池电量 (0x2A19)
Descriptor 描述特征的配置或元数据 从属于某个Characteristic CCCD (0x2902)
注意: 每个Characteristic至少包含一个Value属性,但Descriptor是可选的。不过,如果要支持通知(Notification),就必须有CCCD描述符。我在排查协程泄漏时,发现不少问题是CCCD配置不当导致的回调丢失或重复注册。

讲到这里,GATT的基础框架应该已经清楚了。这一层一层的结构,说白了就是为了让数据能规规矩矩地在设备和手机之间流转。后面咱们讨论回调里的协程泄漏,你得先能在脑子里画出这张"层级地图",才能快速定位问题出在哪一层、哪个节点上。