蓝牙基础:经典蓝牙与BLE的区别、蓝牙协议栈架构、GAP/GATT/ATT核心概念

在开始Android蓝牙服务项目开发之前,必须首先厘清蓝牙技术体系中的几个关键概念。许多初学者在接触Android蓝牙API时,常常对经典蓝牙(BR/EDR)与低功耗蓝牙(BLE)的API差异感到困惑,对GAP、GATT、ATT等术语更是似懂非懂。本章将从协议栈架构出发,系统梳理经典蓝牙与BLE的本质区别,深入剖析GAP/GATT/ATT在蓝牙通信中的角色与协作关系,为后续的Android蓝牙服务开发打下坚实的理论基础。

蓝牙协议栈架构与核心概念关系图 经典蓝牙 BR/EDR 协议栈 应用层 (APP / Profiles) RFCOMM / SDP L2CAP HCI / Link Manager / Baseband 物理层:2.4GHz 79信道 1Mbps BLE 低功耗蓝牙协议栈 应用层 (APP / GATT Profiles) GAP / GATT / ATT L2CAP HCI / Link Layer 物理层:2.4GHz 40信道 1Mbps/2Mbps 核心抽象层 GAP GATT ATT 经典蓝牙 vs BLE 关键对比 通信模型 面向连接,RFCOMM串口模拟 通信模型 面向连接,GATT属性读写/通知 功耗与延迟 高功耗,连接延迟约100ms 功耗与延迟 极低功耗,连接延迟可至7.5ms 数据速率 1-3 Mbps,适合音频/文件传输 数据速率 125kbps-2Mbps,适合传感器/控制

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 APIBluetoothSocket / BluetoothServerSocketBluetoothGatt / BluetoothGattCallback

判断方法:如何为项目选择正确的蓝牙类型

在Android项目立项时,技术选型是第一道决策关卡。判断依据应围绕数据量、功耗约束、连接时延、设备形态四个维度展开。如果应用需要持续传输音频流(如蓝牙音箱)或大文件(如打印机),应选择经典蓝牙;如果应用是周期性上报传感器数据(如温度计每5秒上报一次)、需要纽扣电池供电、或需要快速广播存在性(如Beacon),则必须选择BLE。此外,还需考虑兼容性:Android 4.3(API 18)开始支持BLE,但直到Android 5.0(API 21)才提供完善的扫描API;经典蓝牙则在所有Android版本中均有良好支持。

经验提醒:在Android 12(API 31)及以上版本,扫描BLE设备需要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应用,典型的调用链路如下:

  1. 获取BluetoothManager并从中获取BluetoothAdapter;
  2. 检查蓝牙是否开启,若未开启则请求用户授权;
  3. 使用BluetoothLeScanner.startScan()扫描设备,在回调中获取ScanResult;
  4. 通过BluetoothDevice.connectGatt()建立GATT连接;
  5. 在BluetoothGattCallback中处理连接状态变化、服务发现、特征读写等事件;
  6. 操作完成后调用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回调确认协商结果,问题解决。

风险提示:在Android中,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)使能通知。

关键参数速查:常用UUID——心率服务(0x180D)、心率测量特征(0x2A37)、电池服务(0x180F)、电池电量特征(0x2A19)、设备信息服务(0x180A)、CCCD描述符(0x2902)。在Android中,使用BluetoothGattCharacteristic的UUID.fromString("00002A37-0000-1000-8000-00805F9B34FB")格式。

案例拆解:实现一个完整的BLE心率数据读取

假设我们需要开发一个Android应用,实时读取心率带的数据。硬件设备是标准BLE心率带,遵循心率服务规范。

实现步骤:

  1. 扫描设备,过滤包含心率服务UUID(0x180D)的广播包;
  2. 连接设备,发现服务;
  3. 获取心率服务(0x180D)下的心率测量特征(0x2A37);
  4. 写入CCCD描述符(0x2902)值为0x0001,使能通知;
  5. 在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规范进行数据操作,避免因角色混淆或流程遗漏导致的通信失败。