蓝牙基础:经典蓝牙与BLE的区别、蓝牙协议栈架构、GAP/GATT/ATT核心概念
在开始Android蓝牙服务项目开发之前,必须首先厘清蓝牙技术体系中的几个关键概念。许多初学者在接触Android蓝牙API时,常常对经典蓝牙(BR/EDR)与低功耗蓝牙(BLE)的API差异感到困惑,对GAP、GATT、ATT等术语更是似懂非懂。本章将从协议栈架构出发,系统梳理经典蓝牙与BLE的本质区别,深入剖析GAP/GATT/ATT在蓝牙通信中的角色与协作关系,为后续的Android蓝牙服务开发打下坚实的理论基础。
2.1 经典蓝牙与BLE:同源异流的技术分支
核心原理:从射频到协议的双轨演进
蓝牙技术联盟(SIG)在蓝牙4.0版本中正式引入了低功耗蓝牙(BLE),从此蓝牙技术分化为两条独立演进的轨道。经典蓝牙(BR/EDR,Basic Rate/Enhanced Data Rate)继承自蓝牙2.0/3.0时代,其设计目标是高数据吞吐量与持续连接,适用于音频流传输、文件交换等场景。而BLE则完全重新设计了物理层与协议栈,以极低功耗、快速连接、短数据包为核心设计目标,适用于物联网传感器、可穿戴设备、智能家居等电池供电场景。
从物理层看,经典蓝牙使用79个信道(2.402GHz-2.480GHz,每信道1MHz),采用跳频扩频技术(1600跳/秒),而BLE仅使用40个信道(每信道2MHz),其中3个为广播信道(37/38/39),37个为数据信道。BLE的广播机制使得设备无需建立连接即可发送数据,这是其功耗远低于经典蓝牙的关键原因之一。在Android开发中,BluetoothAdapter同时管理两种模式,但API调用路径完全不同:经典蓝牙使用BluetoothSocket进行RFCOMM通信,而BLE则通过BluetoothGatt进行属性操作。
| 对比维度 | 经典蓝牙 (BR/EDR) | BLE (低功耗蓝牙) |
|---|---|---|
| 信道数量 | 79个,1MHz间隔 | 40个,2MHz间隔(3广播+37数据) |
| 连接建立时间 | 约100ms(需查询+寻呼) | 约3-10ms(广播+扫描) |
| 峰值功耗 | 约100mW(连续传输) | 约10-15mW(事件驱动) |
| 数据速率 | 1-3Mbps(EDR可达3Mbps) | 125kbps-2Mbps(取决于PHY) |
| 拓扑结构 | 微微网(Piconet)最多7个从设备 | 广播/连接,理论上无限制 |
| 典型应用 | 蓝牙耳机、车载免提、文件传输 | 心率计、Beacon、智能锁、传感器 |
| Android API | BluetoothSocket / BluetoothServerSocket | BluetoothGatt / BluetoothGattCallback |
判断方法:如何为项目选择正确的蓝牙类型
在Android项目立项时,技术选型是第一道决策关卡。判断依据应围绕数据量、功耗约束、连接时延、设备形态四个维度展开。如果应用需要持续传输音频流(如蓝牙音箱)或大文件(如打印机),应选择经典蓝牙;如果应用是周期性上报传感器数据(如温度计每5秒上报一次)、需要纽扣电池供电、或需要快速广播存在性(如Beacon),则必须选择BLE。此外,还需考虑兼容性:Android 4.3(API 18)开始支持BLE,但直到Android 5.0(API 21)才提供完善的扫描API;经典蓝牙则在所有Android版本中均有良好支持。
BLUETOOTH_SCAN权限,连接设备需要BLUETOOTH_CONNECT权限。如果应用同时使用经典蓝牙和BLE,需要分别声明权限,并在运行时动态请求。切勿为了省事而只申请旧版权限,否则在Android 12+设备上会直接崩溃。
案例拆解:智能手环的蓝牙选型决策
假设我们正在开发一款智能手环,需要与Android手机通信。手环的功能包括:实时心率监测(每1秒上报一次)、步数统计(每30秒同步一次)、消息通知推送(手机推送到手环)。约束条件:手环电池容量为100mAh,目标续航7天;手环与手机距离通常在1米以内。
分析过程:心率数据每秒产生约8字节(心率值+时间戳),步数数据每30秒产生约20字节,消息通知每次约100字节。这些数据量极小,远低于经典蓝牙的传输能力。功耗方面,如果使用经典蓝牙,持续连接将消耗约50-80mW,100mAh电池仅能支撑约2-3小时;而BLE在广播+连接事件模式下,平均功耗可控制在0.5-1mW,满足7天续航要求。连接时延方面,BLE的7.5ms最小连接间隔完全满足每秒一次的数据上报。
决策结果:选择BLE作为通信方案。具体实现时,手环作为GATT Server,手机作为GATT Client。心率数据通过Heart Rate Measurement特征(UUID: 0x2A37)以通知(Notification)方式实时推送;步数数据通过自定义服务在连接后读取;消息通知则通过Write操作由手机下发。该方案在实际项目中实现了5-7天续航,且连接稳定。
常见误区与总结复盘
- 误区一:认为BLE只能传小数据。实际上BLE 5.0支持2Mbps速率,且通过扩展广播(Extended Advertising)可发送255字节的广播数据,足以应对大部分传感器场景。
- 误区二:在Android中混用经典蓝牙和BLE的API。两者是完全独立的协议栈,
BluetoothSocket不能用于BLE设备,BluetoothGatt也不能连接经典蓝牙设备。 - 误区三:忽视BLE的兼容性差异。不同厂商的BLE芯片(如Nordic、TI、Dialog)在MTU大小、连接参数协商上存在差异,Android设备与不同外设的兼容性需要充分测试。
复盘要点:技术选型不是简单的"BLE更好",而是基于数据特征、功耗预算、实时性要求的综合权衡。在项目初期,建议制作一张类似上表的对比清单,与硬件团队共同确认关键参数,避免后期返工。
2.2 蓝牙协议栈架构:分层设计与Android实现
核心原理:协议栈的分层协作
蓝牙协议栈采用分层架构,每一层只负责特定的功能,层与层之间通过标准接口交互。这种设计使得上层应用无需关心底层射频细节,也使得协议栈可以灵活适配不同的硬件平台。以BLE为例,从下到上依次为:物理层(PHY)负责2.4GHz射频收发;链路层(Link Layer)负责信道管理、广播/扫描、连接状态机;HCI(Host Controller Interface)是主机与控制器之间的标准接口;L2CAP负责数据包的分段与重组、多路复用;ATT定义属性数据的组织与访问方法;GATT基于ATT构建服务/特征/描述符的层次结构;GAP则负责设备的发现、连接与安全。
在Android系统中,蓝牙协议栈的实现分为两部分:系统服务层(位于packages/modules/Bluetooth)和应用框架层(android.bluetooth包)。应用开发者主要与框架层API交互,系统服务层则通过HCI与蓝牙芯片通信。Android从8.0开始将蓝牙协议栈迁移到Gabeldorsche架构,但应用层API保持稳定,开发者无需关心底层变化。
操作流程:Android蓝牙API的调用链路
在Android中开发BLE应用,典型的调用链路如下:
- 获取
BluetoothManager并从中获取BluetoothAdapter; - 检查蓝牙是否开启,若未开启则请求用户授权;
- 使用
BluetoothLeScanner.startScan()扫描设备,在回调中获取ScanResult; - 通过
BluetoothDevice.connectGatt()建立GATT连接; - 在
BluetoothGattCallback中处理连接状态变化、服务发现、特征读写等事件; - 操作完成后调用
gatt.disconnect()和gatt.close()释放资源。
// 典型BLE连接流程(简化版)
BluetoothManager manager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
BluetoothAdapter adapter = manager.getAdapter();
BluetoothDevice device = adapter.getRemoteDevice("AA:BB:CC:DD:EE:FF");
BluetoothGatt gatt = device.connectGatt(context, false, new BluetoothGattCallback() {
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices(); // 发现服务
}
}
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
// 获取服务与特征,进行读写操作
}
});
案例拆解:协议栈分层在排障中的应用
某项目中,Android手机与BLE设备连接后,偶尔出现"连接成功但无法发现服务"的问题。通过分层排查:首先检查物理层——使用蓝牙抓包工具(如Ellisys、Frontline)确认射频信号正常,无干扰;其次检查链路层——确认连接参数(间隔、延迟)协商成功,无异常断开;接着检查L2CAP——发现MTU协商失败,设备端MTU为23字节(默认值),而Android端请求了247字节,导致ATT层数据包被丢弃。最终通过gatt.requestMtu(247)并等待onMtuChanged回调确认协商结果,问题解决。
requestMtu()是异步操作,必须在onMtuChanged回调中确认实际协商值。部分低端BLE设备不支持MTU协商,会返回默认值23,此时如果应用层发送超过20字节的数据(ATT默认最大有效载荷为23-3=20字节),会导致写入失败。建议在应用层做数据分包处理。
常见误区与总结复盘
- 误区一:认为协议栈分层与Android开发无关。实际上,理解分层有助于快速定位问题——例如连接失败可能是链路层参数不匹配,而读写失败则可能是ATT层问题。
- 误区二:忽视HCI日志。Android开发者选项中的"蓝牙HCI抓包"功能可以生成
btsnoop_hci.log文件,用Wireshark打开即可查看完整的HCI事件,是排障的利器。 - 误区三:在UI线程中执行蓝牙操作。所有蓝牙回调都在Binder线程中执行,如果直接操作UI会崩溃,必须通过Handler或runOnUiThread切换到主线程。
复盘要点:协议栈分层不仅是理论框架,更是排障的思维工具。建议开发者在项目初期就熟悉HCI日志分析方法,这将在后续的联调中节省大量时间。
2.3 GAP/GATT/ATT:BLE通信的三驾马车
核心原理:从角色定义到数据组织的完整链路
GAP(Generic Access Profile)定义了BLE设备的角色、发现流程、连接参数和安全机制。在GAP中,设备可以是Broadcaster(仅广播)、Observer(仅扫描)、Peripheral(从设备,可被连接)或Central(主设备,发起连接)。Android设备通常作为Central,而传感器等外设作为Peripheral。GAP还定义了广播数据格式,包括设备名称、服务UUID、厂商自定义数据等。
ATT(Attribute Protocol)是BLE数据读写的底层协议。它将数据组织为属性(Attribute),每个属性包含:句柄(Handle,16位)、类型(UUID)、权限(读/写/通知等)和值(Value)。ATT定义了6种PDU(协议数据单元):请求、响应、命令、通知、指示、确认。其中通知(Notification)不需要接收方确认,而指示(Indication)需要确认,前者适合高频数据,后者适合可靠传输。
GATT(Generic Attribute Profile)在ATT之上定义了数据组织的层次结构:服务(Service)包含多个特征(Characteristic),特征包含值(Value)和多个描述符(Descriptor)。特征值就是实际传输的数据,描述符则描述特征值的属性(如客户端特征配置描述符CCCD,用于开启通知)。GATT还定义了操作流程:服务发现、特征读取、特征写入、通知/指示订阅等。
判断方法:理解GATT操作中的角色与流程
在Android开发中,理解GATT操作的关键是明确谁发起、谁响应。以读取心率为例:手机(GATT Client)向手环(GATT Server)发送"读取心率特征"请求,手环返回心率值。如果手环需要主动推送心率数据,则手环发送通知,手机在onCharacteristicChanged回调中接收。判断操作类型的方法:读操作使用readCharacteristic(),写操作使用writeCharacteristic(),通知订阅需要先写入CCCD描述符(0x2902)使能通知。
BluetoothGattCharacteristic的UUID.fromString("00002A37-0000-1000-8000-00805F9B34FB")格式。
案例拆解:实现一个完整的BLE心率数据读取
假设我们需要开发一个Android应用,实时读取心率带的数据。硬件设备是标准BLE心率带,遵循心率服务规范。
实现步骤:
- 扫描设备,过滤包含心率服务UUID(0x180D)的广播包;
- 连接设备,发现服务;
- 获取心率服务(0x180D)下的心率测量特征(0x2A37);
- 写入CCCD描述符(0x2902)值为0x0001,使能通知;
- 在
onCharacteristicChanged回调中解析心率数据。
// 使能通知
BluetoothGattCharacteristic heartRateChar = service.getCharacteristic(
UUID.fromString("00002A37-0000-1000-8000-00805F9B34FB"));
gatt.setCharacteristicNotification(heartRateChar, true);
BluetoothGattDescriptor cccd = heartRateChar.getDescriptor(
UUID.fromString("00002902-0000-1000-8000-00805F9B34FB"));
cccd.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
gatt.writeDescriptor(cccd);
// 解析心率数据(标准格式:第1字节标志位,后续为心率值)
@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
byte[] data = characteristic.getValue();
int flags = data[0] & 0xFF;
int heartRate;
if ((flags & 0x01) == 0) {
heartRate = data[1] & 0xFF; // 8位心率值
} else {
heartRate = ((data[2] & 0xFF) << 8) | (data[1] & 0xFF); // 16位心率值
}
// 更新UI
}
排障过程:如果通知未触发,首先检查CCCD是否写入成功(在onDescriptorWrite回调中确认);其次检查特征属性是否包含PROPERTY_NOTIFY;最后确认设备端是否正确发送通知。如果读取到的心率值异常,检查标志位解析是否正确,注意字节序。
常见误区与总结复盘
- 误区一:混淆GAP和GATT的角色。GAP负责"找到并连接设备",GATT负责"连接后如何读写数据"。在Android中,扫描是GAP操作,服务发现是GATT操作。
- 误区二:忘记写入CCCD就期望收到通知。这是最常见的错误之一,必须显式使能通知。
- 误区三:在
onServicesDiscovered中直接操作特征值。此时GATT连接刚建立,部分设备可能尚未准备好,建议延迟100-200ms再操作。 - 误区四:忽略MTU大小。默认MTU为23字节,扣除ATT头后有效载荷仅20字节。如果传输大文件,必须协商更大的MTU(如247字节),否则需要分包。
复盘要点:GAP/GATT/ATT是BLE开发的核心知识框架。建议在项目开始前,先画出设备角色图和数据流图,明确谁是Client、谁是Server,哪些数据用读、哪些用通知,再动手写代码。这样能避免大量返工。
本章从经典蓝牙与BLE的对比出发,深入剖析了蓝牙协议栈的分层架构,并重点讲解了GAP/GATT/ATT在BLE通信中的核心作用。理解这些基础概念,是后续实现Android蓝牙服务的关键前提。在实际开发中,建议开发者始终带着"协议栈分层"的视角去分析问题,利用HCI日志和分层排查法快速定位故障,同时严格遵循GATT规范进行数据操作,避免因角色混淆或流程遗漏导致的通信失败。