本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:蓝牙通信是Android平台在物联网应用中的核心技术之一,尤其在低功耗设备交互中具有重要意义。本“蓝牙通信 Android APP源码”项目基于Android Studio开发环境,完整实现了BLE(蓝牙低功耗)的扫描、连接、服务发现、数据读写及通知订阅等功能,并已通过实际硬件模块测试。项目涵盖Android蓝牙API的核心使用方法,包含权限管理、回调机制与代码规范实践,帮助开发者深入掌握BLE通信流程,提升在智能设备互联领域的开发能力。

1. Android蓝牙通信开发环境搭建与核心组件概述

在移动应用开发中,蓝牙通信技术广泛应用于智能硬件交互、物联网设备控制等场景。本章将围绕Android平台下蓝牙通信的基础准备条件展开,重点介绍如何使用Android Studio搭建适用于BLE(低功耗蓝牙)开发的工程环境,并完成必要的SDK配置与依赖引入。同时,对Android蓝牙体系中的核心类 BluetoothAdapter BluetoothGatt 进行初步解析,明确其在整个通信流程中的职责定位。

// build.gradle(Module: app)
implementation 'androidx.core:core-ktx:1.10.1'
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'

此外,还将说明项目源码的整体结构设计思路,包括包名划分、模块解耦原则以及代码规范建议,为后续深入实现打下坚实基础。

2. BLE通信协议理论基础与GATT架构深度解析

蓝牙低功耗(Bluetooth Low Energy, BLE)作为现代物联网设备间短距离无线通信的核心技术之一,其高效能、低功耗的特性使其在可穿戴设备、智能家居、医疗传感器等场景中广泛应用。然而,要实现稳定可靠的BLE通信,开发者必须深入理解底层协议栈结构以及通用属性规范(GATT)的工作机制。本章将系统性地剖析BLE的技术演进路径,解析协议栈分层模型,并重点展开对GATT架构的层级建模与运行流程分析。在此基础上,结合实际开发需求,探讨如何基于GATT模型设计标准化的设备通信接口,为后续Android平台上的编程实践提供坚实的理论支撑。

2.1 蓝牙低功耗技术演进与BLE协议栈分层结构

随着移动互联网和智能硬件的发展,传统经典蓝牙(Classic Bluetooth)在高功耗、连接复杂性和数据吞吐量方面的局限逐渐显现。为应对这一挑战,蓝牙技术联盟(Bluetooth SIG)于2010年推出蓝牙4.0标准,首次引入了低功耗蓝牙(BLE),标志着无线通信向节能化、轻量化方向的重要转型。BLE并非经典蓝牙的替代品,而是针对不同应用场景的补充方案,尤其适用于周期性小数据传输、长时间待机的设备类型。

2.1.1 经典蓝牙与BLE的技术差异对比

经典蓝牙主要用于音频流传输(如耳机、音箱)、文件传输或串口通信,采用持续连接模式,维持链路需要较高的能量消耗。而BLE则专注于间歇性的小数据包交换,支持快速连接、短暂通信后迅速断开,极大降低了平均功耗。下表详细列出了两者在关键参数上的技术差异:

对比维度 经典蓝牙(BR/EDR) 蓝牙低功耗(BLE)
工作频段 2.4 GHz ISM频段(79个信道) 2.4 GHz ISM频段(40个信道)
数据速率 1–3 Mbps 1–2 Mbps(BLE 5.x可达更高)
功耗水平 高(持续连接) 极低(事件驱动)
连接拓扑 点对点为主,支持微微网(Piconet) 星型拓扑,中心设备可连接多个外围设备
占用带宽 宽带跳频(1 MHz/信道) 窄带跳频(2 MHz/信道)
典型应用 音频传输、大文件传输 心率监测、信标广播、遥控器

从上述对比可见,BLE通过简化协议栈、减少连接维护开销、优化射频调度等方式,在保持足够通信能力的同时显著降低能耗。例如,一个典型的BLE心率手环可在纽扣电池供电下连续工作数月甚至一年以上,而同等功能的经典蓝牙设备往往仅能维持几天。

此外,BLE采用了“主从”角色分离的设计思想:中央设备(Central,如手机)负责发起扫描和连接;外围设备(Peripheral,如传感器)处于广播状态等待被发现。这种非对称架构进一步减少了外围设备的计算负担和射频活跃时间,是其实现超低功耗的关键所在。

2.1.2 BLE协议栈五层模型详解(PHY、LL、HCI、L2CAP、ATT/GATT)

BLE协议栈采用分层设计思想,每一层承担特定的功能职责,上层依赖下层提供的服务完成通信任务。完整的BLE协议栈主要包括以下五个核心层次:

graph TD
    A[Application Layer] --> B[GATT/ATT]
    B --> C[L2CAP]
    C --> D[Host Controller Interface (HCI)]
    D --> E[Link Layer (LL)]
    E --> F[Physical Layer (PHY)]
物理层(Physical Layer, PHY)

物理层负责射频信号的调制解调与无线信道管理。BLE使用GFSK(高斯频移键控)调制方式,在2.4GHz ISM频段内划分出40个RF信道,其中3个为广播信道(37、38、39),其余37个用于数据通信。设备通过跳频机制避免干扰,提升抗噪能力。

链路层(Link Layer, LL)

链路层控制设备之间的物理连接建立与维护。它定义了三种基本操作模式: Advertising (广播)、 Scanning (扫描)和 Connection (连接)。当外围设备进入广播状态时,会在三个广播信道上周期性发送包含设备信息的数据包;中央设备监听这些广播包并决定是否发起连接请求。

链路层还实现了加密、白名单过滤、功率控制等功能,并通过 Access Address CRC校验 保障数据完整性。

主机控制器接口(Host Controller Interface, HCI)

HCI 是软件栈中“主机”(Host)与“控制器”(Controller)之间的桥梁,通常以硬件模块(如蓝牙芯片)和操作系统驱动的形式存在。它允许上层协议通过命令、事件和数据包与底层蓝牙硬件交互。在Android系统中, bluetoothd 守护进程即运行于HCI之上。

逻辑链路控制与适配协议(L2CAP)

L2CAP 提供多路复用、分段重组和QoS控制功能。它将来自上层的数据分割成适合底层传输的单元,并在接收端重新组装。对于BLE而言,L2CAP还支持信令通道(Signaling Channel)用于MTU协商、连接参数更新等控制操作。

属性协议与通用属性规范(ATT/GATT)

ATT(Attribute Protocol)是BLE中最关键的应用层协议之一,定义了“属性”的概念——即服务、特征值及其元数据的统一表示形式。每个属性由UUID标识,具有句柄(Handle)、权限(Permissions)和值(Value)三个基本要素。

GATT(Generic Attribute Profile)构建在ATT之上,规定了客户端与服务器之间如何通过读写、通知等方式访问远程设备的数据。所有BLE通信本质上都是围绕GATT服务展开的,因此理解ATT/GATT机制是掌握BLE开发的核心前提。

2.1.3 数据包传输机制与时隙调度原理

BLE通信以“连接事件”(Connection Event)为单位进行数据交换。一旦中央设备与外围设备建立连接,二者会按照预设的 连接间隔 (Connection Interval)定期唤醒并进行数据收发。每次连接事件中,主设备先发起通信,从设备响应。

连接参数包括:
- Connection Interval :两次连接事件之间的时间间隔(6.25ms ~ 4s)
- Slave Latency :从设备可以跳过的连接事件数量(节省电量)
- Supervision Timeout :最大无响应时间,超时则判定断连

这些参数在连接建立时由双方协商确定,可通过 BluetoothGatt.requestConnectionPriority() 在Android端调整优先级来影响系统决策。

数据包格式遵循严格的帧结构。以一个典型的GATT读取请求为例:

[ Preamble ] [ Access Address ] [ PDU Header + Payload ] [ CRC ]

其中PDU(Protocol Data Unit)部分包含操作码(Opcode)、句柄(Handle)和数据内容。例如,一个ATT_READ_REQ指令会携带目标特征值的句柄,服务器收到后返回对应的ATT_READ_RSP响应包。

为了提高效率,BLE支持 数据长度扩展 (Data Length Extension, DLE)和 2M PHY模式 (BLE 5.0+),使单次传输的数据量从传统的27字节提升至251字节,大幅提升吞吐率并减少重传次数。

2.2 GATT通信模式与角色定义

GATT(Generic Attribute Profile)是BLE设备间数据交互的标准框架,几乎所有BLE应用都基于该模型实现数据读写、通知订阅等功能。理解GATT的角色分工、属性组织方式及通信语义,是构建可靠通信系统的基础。

2.2.1 客户端(Client)与服务器(Server)的角色分工

在GATT通信中,设备角色不再以“主/从”命名,而是根据数据访问关系划分为 GATT Client GATT Server

  • GATT Server :持有数据的设备,通常为外围设备(Peripheral),如心率计、温湿度传感器。它对外暴露一系列服务(Service)、特征(Characteristic)和描述符(Descriptor),供客户端查询和操作。
  • GATT Client :请求数据的设备,通常是中央设备(Central),如智能手机。它通过发送读写命令获取或修改服务器上的数据。

值得注意的是,角色是相对的。一台设备可以在某个连接中作为Server,在另一个连接中作为Client。例如,智能手表可能作为Server向手机提供健康数据,同时作为Client从手环同步运动记录。

通信流程如下图所示:

sequenceDiagram
    participant Phone as 手机 (Client)
    participant Sensor as 传感器 (Server)

    Phone->>Sensor: connect()
    Sensor-->>Phone: CONNECTED
    Phone->>Sensor: discoverServices()
    Sensor-->>Phone: 返回服务列表
    Phone->>Sensor: readCharacteristic(HeartRate)
    Sensor-->>Phone: 返回心率值
    Phone->>Sensor: setNotify(Steps)
    Sensor-->>Phone: NOTIFY 每秒步数

2.2.2 属性协议(ATT)在特征值读写中的作用机制

ATT协议定义了所有GATT操作的基本单元—— 属性 (Attribute)。每个属性包含四个字段:

字段 说明
Handle 唯一整数编号,用于寻址
Type UUID,标识属性类型(如Service、Characteristic)
Value 存储的实际数据
Permissions 访问权限(Read/Write/Encrypt等)

当客户端想要读取某项特征值时,它并不直接知道其UUID,而是先通过 discoverServices() 获取服务列表,再遍历查找所需特征。此过程涉及多个ATT操作:

  1. ATT_FIND_BY_TYPE_VALUE_REQ :查找指定UUID的服务
  2. ATT_READ_BY_TYPE_REQ :读取该服务内的所有特征
  3. ATT_READ_REQ :根据句柄读取具体特征值

以下是一个模拟的ATT读取请求代码片段(伪代码):

// Android端触发特征值读取
BluetoothGatt gatt = device.connectGatt(context, false, callback);
gatt.discoverServices(); // 触发ATT_FIND_BY_TYPE_VALUE_REQ系列操作

// 在服务发现完成后执行读取
@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    BluetoothGattService service = gatt.getService(SERVICE_UUID);
    BluetoothGattCharacteristic chara = service.getCharacteristic(CHARACTERISTIC_UUID);
    gatt.readCharacteristic(chara); // 发送ATT_READ_REQ
}

逻辑分析:
- connectGatt() 启动连接流程,底层通过HCI命令与蓝牙控制器交互。
- discoverServices() 引发一系列ATT协议请求,逐层解析远程设备的服务结构。
- readCharacteristic() 将触发ATT_READ_REQ数据包发送至服务器,对方回应ATT_READ_RSP,最终回调 onCharacteristicRead()

参数说明:
- SERVICE_UUID : 服务唯一标识符,需与硬件文档一致。
- CHARACTERISTIC_UUID : 特征值标识符,决定读取哪一项数据。
- 回调函数中 status 表示操作结果, BluetoothGatt.GATT_SUCCESS 表示成功。

2.2.3 服务(Service)、特征(Characteristic)与描述符(Descriptor)的层级关系建模

GATT采用树状层级结构组织数据:

Device
 └── Service (UUID: 0x180D - Heart Rate Service)
      ├── Characteristic (UUID: 0x2A37 - Heart Rate Measurement)
      │    └── Descriptor (UUID: 0x2902 - CCCD)
      └── Characteristic (UUID: 0x2A38 - Body Sensor Location)
  • Service :逻辑功能模块,如“心率服务”、“电池服务”。每个服务包含若干相关特征。
  • Characteristic :具体数据项,包含值和属性(如READ、NOTIFY)。它是数据读写的最小单位。
  • Descriptor :附加信息,最常见的是CCCD(Client Characteristic Configuration Descriptor),用于启用通知或指示。

CCCD的值决定了是否开启异步数据推送。例如,设置其值为 0x0001 表示启用NOTIFY, 0x0002 表示INDICATE(需确认)。

2.3 通用属性规范的工作流程分析

2.3.1 连接建立后的服务发现过程时序图解析

服务发现是GATT通信的第一步。以下是完整流程的时序图:

sequenceDiagram
    participant App
    participant AndroidStack
    participant RemoteDevice

    App->>AndroidStack: connectGatt()
    AndroidStack->>RemoteDevice: Connection Request
    RemoteDevice-->>AndroidStack: Connection Established
    AndroidStack->>RemoteDevice: ATT_FIND_BY_TYPE_VALUE_REQ (Primary Service)
    RemoteDevice-->>AndroidStack: List of Services
    loop For each service
        AndroidStack->>RemoteDevice: ATT_READ_BY_TYPE_REQ (Include Service)
        AndroidStack->>RemoteDevice: ATT_READ_BY_TYPE_REQ (Characteristic)
        AndroidStack->>RemoteDevice: ATT_READ_BY_TYPE_REQ (Descriptor)
    end
    AndroidStack-->>App: onServicesDiscovered()

只有完成服务发现,才能安全地访问特征值。

2.3.2 特征值属性类型(READ/WRITE/NOTIFY/INDICATE)的功能语义

属性 说明 是否需要响应
READ 支持主动读取
WRITE 支持写入控制指令 可选(NO_RESPONSE更快)
NOTIFY 服务器主动推送数据,无需确认
INDICATE 推送数据且要求客户端回复ACK

推荐在高频率数据传输中使用 WRITE_NO_RESPONSE NOTIFY 以减少延迟。

2.3.3 MTU协商与长特征值读取优化策略

MTU(Maximum Transmission Unit)默认为23字节,但可通过 requestMtu(512) 协商更大值(最高512字节)。这使得单次可读取更长的数据,避免多次请求。

示例代码:

gatt.requestMtu(512);

@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        Log.d("BLE", "MTU updated to: " + mtu);
    }
}

2.4 实践导向:基于GATT模型设计设备通信接口

2.4.1 如何根据硬件文档定义Service UUID与Characteristic映射表

假设某智能锁硬件文档定义如下服务:

Service Name UUID Characteristics
Lock Control FFE0 FFE1 (RW), FFE2 (N)

应在代码中定义常量类:

public class BleProfile {
    public static final UUID SERVICE_LOCK = UUID.fromString("0000FFE0-0000-1000-8000-00805F9B34FB");
    public static final UUID CHAR_CONTROL = UUID.fromString("0000FFE1-0000-1000-8000-00805F9B34FB");
    public static final UUID CHAR_STATUS = UUID.fromString("0000FFE2-0000-1000-8000-00805F9B34FB");
}

2.4.2 构建标准化数据解析器以支持多设备兼容性

设计通用解析接口:

public interface DataParser {
    Object parse(byte[] data);
}

public class LockStatusParser implements DataParser {
    @Override
    public Object parse(byte[] data) {
        boolean locked = (data[0] & 0x01) == 1;
        return new LockState(locked);
    }
}

通过策略模式动态加载对应解析器,提升系统扩展性。

3. 蓝牙设备扫描与权限管理实战

在Android平台上实现低功耗蓝牙(BLE)通信的第一步,是能够发现周围可用的外围设备。这一过程依赖于系统的扫描机制和对硬件资源的合理调度。然而,在实际开发中,许多开发者会遇到“无法扫描到设备”、“扫描结果不稳定”或“应用崩溃”等问题,其根源往往并非来自代码逻辑本身,而是对扫描流程、权限控制以及系统行为理解不足所致。本章将深入剖析Android BLE扫描的核心组件 BluetoothLeScanner ,结合运行时权限管理策略,构建一个稳定、高效且符合现代Android规范的设备发现模块。

通过本章内容的学习,读者不仅能够掌握如何正确初始化扫描器并处理广播数据,还将学会如何在不同Android版本下动态申请位置权限、设计用户友好的权限引导界面,并通过合理的生命周期绑定避免内存泄漏与资源浪费。此外,针对性能优化的关键点——如扫描窗口控制、过滤规则配置等——也将提供可落地的最佳实践方案。

3.1 BluetoothLeScanner初始化与扫描参数配置

Android从5.0(API Level 21)开始引入了新的BLE扫描API,核心类为 BluetoothLeScanner ,它取代了早期基于 startLeScan() 的过时方法,提供了更细粒度的控制能力。使用该类可以灵活设置扫描模式、回调类型、报告延迟等参数,从而平衡扫描速度、精度与功耗之间的关系。

要获取 BluetoothLeScanner 实例,必须先通过 BluetoothAdapter 进行初始化。以下是一个典型的初始化流程示例:

// 获取默认蓝牙适配器
BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
BluetoothAdapter bluetoothAdapter = bluetoothManager.getAdapter();

if (bluetoothAdapter == null || !bluetoothAdapter.isEnabled()) {
    // 蓝牙未启用,需提示用户开启
    Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
    startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    return;
}

// 获取 BluetoothLeScanner 实例
BluetoothLeScanner scanner = bluetoothAdapter.getBluetoothLeScanner();
if (scanner == null) {
    Log.e("BLE_SCAN", "Failed to get BluetoothLeScanner");
    return;
}

### 初始化流程详解与异常处理机制

上述代码首先通过系统服务获取 BluetoothManager 对象,进而取得 BluetoothAdapter 。这是所有BLE操作的起点。若设备不支持蓝牙或蓝牙未开启,则后续操作均无法执行。值得注意的是,调用 getAdapter() 可能返回 null ,尤其是在某些定制ROM或模拟器环境中,因此必须进行空值判断。

接着,通过 getBluetoothLeScanner() 方法获取扫描器实例。尽管大多数现代设备都能正常返回该对象,但在极少数低端机型或系统异常状态下仍可能出现 null 情况。此时应记录错误日志并提示用户检查设备兼容性。

一旦获得 BluetoothLeScanner ,即可准备启动扫描任务。但在此之前,需要配置扫描参数以满足具体业务需求。

### ScanSettings 高级参数配置解析

ScanSettings 是用于定义扫描行为的核心配置类,可通过 ScanSettings.Builder 构建。以下是常见配置项及其含义:

参数 取值范围 说明
setScanMode(int scanMode) SCAN_MODE_LOW_POWER , SCAN_MODE_BALANCED , SCAN_MODE_LOW_LATENCY 控制扫描频率与功耗平衡
setCallbackType(int callbackType) CALLBACK_TYPE_ALL_MATCHES , CALLBACK_TYPE_FIRST_MATCH 决定何时触发回调
setMatchMode(int matchMode) MATCH_MODE_STICKY , MATCH_MODE_AGGRESSIVE 匹配过滤器时的行为策略
setNumOfMatches(int numMatches) MATCH_NUM_ONE_ADVERTISEMENT MATCH_NUM_MAX_ADVERTISEMENT 最大匹配广告数量
setReportDelay(long milliseconds) 非负整数 延迟报告扫描结果,用于批处理

例如,若希望实现高精度快速扫描(适用于调试阶段),可采用如下配置:

ScanSettings settings = new ScanSettings.Builder()
        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 每秒多次扫描
        .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
        .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
        .setNumOfMatches(ScanSettings.MATCH_NUM_MAX_ADVERTISEMENT)
        .setReportDelay(0) // 即时上报
        .build();

而为了降低功耗(适合后台持续扫描场景),则应选择低功耗模式并启用报告延迟:

ScanSettings powerSaveSettings = new ScanSettings.Builder()
        .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) // 每隔数秒扫描一次
        .setReportDelay(5000) // 缓冲5秒内所有结果一次性上报
        .build();
报告延迟机制的工作原理

当设置 reportDelay > 0 时,系统不会立即回调 onScanResult() ,而是将扫描到的设备暂存至内部缓冲区,直到达到设定时间后统一通过 onBatchScanResults() 批量上报。这有助于减少主线程频繁唤醒,提升能效比。但代价是实时性下降,不适合对连接响应时间要求高的场景。

### 扫描启动逻辑与线程安全性分析

完成设置后,便可调用 startScan() 方法启动扫描:

List<ScanFilter> filters = new ArrayList<>();
// 添加过滤条件(见下一节)

scanner.startScan(filters, settings, scanCallback);

此处传入三个关键参数:
- filters : 扫描过滤器列表,用于筛选目标设备;
- settings : 扫描行为配置;
- scanCallback : 结果回调处理器。

需要注意的是, startScan() 方法运行在Binder线程上,因此回调函数默认不在主线程执行。若需更新UI,必须手动切换至主线程:

new Handler(Looper.getMainLooper()).post(() -> {
    // 更新 RecyclerView 或 ProgressBar
});

同时,由于 BluetoothLeScanner 是单例资源,多个组件同时调用 startScan() 可能导致冲突。建议在整个应用中封装统一的 BleScannerManager 单例来协调扫描请求,防止重复注册。

### 错误码与状态监控机制设计

虽然官方文档未明确列出所有错误码,但在实践中可通过监听 onScanFailed(int errorCode) 回调识别常见问题:

@Override
public void onScanFailed(int errorCode) {
    switch (errorCode) {
        case SCAN_FAILED_ALREADY_STARTED:
            Log.w("BLE_SCAN", "Scan already started");
            break;
        case SCAN_FAILED_APPLICATION_REGISTRATION_FAILED:
            Log.e("BLE_SCAN", "App registration with OS failed");
            break;
        case SCAN_FAILED_FEATURE_UNSUPPORTED:
            Log.e("BLE_SCAN", "BLE not supported on this device");
            break;
        case SCAN_FAILED_INTERNAL_ERROR:
            Log.e("BLE_SCAN", "Internal error in Bluetooth stack");
            break;
        default:
            Log.e("BLE_SCAN", "Unknown scan failure: " + errorCode);
    }
}

这些错误码反映了底层蓝牙栈的状态异常或资源竞争问题。开发者应在日志中记录详细信息,并根据错误类型决定是否重试或降级处理。

### 流程图:BluetoothLeScanner 初始化与扫描流程

graph TD
    A[获取 BluetoothManager] --> B{BluetoothAdapter 是否可用?}
    B -- 否 --> C[提示用户开启蓝牙]
    B -- 是 --> D[获取 BluetoothLeScanner]
    D --> E{Scanner 是否为空?}
    E -- 是 --> F[记录错误并退出]
    E -- 否 --> G[构建 ScanSettings]
    G --> H[可选: 构建 ScanFilter]
    H --> I[调用 startScan()]
    I --> J[等待 ScanCallback 回调]
    J --> K[onScanResult / onBatchScanResults]
    K --> L[提取设备信息并处理]
    L --> M[停止扫描或继续监听]

该流程清晰地展示了从环境准备到结果接收的完整路径,强调了各环节中的判空与异常处理节点。

### 参数说明与最佳实践总结

综合来看, ScanSettings 的配置直接影响扫描效率与用户体验。以下为推荐配置策略:

  • 前台高响应场景 (如配对向导):使用 SCAN_MODE_LOW_LATENCY + reportDelay=0
  • 后台低功耗监听 (如信标检测):使用 SCAN_MODE_LOW_POWER + reportDelay=5000~10000
  • 精准匹配特定设备 :配合 ScanFilter 过滤 MAC 地址或 Service UUID
  • 避免频繁启停扫描 :使用定时器控制扫描周期(如扫描10秒,休眠30秒)

只有在充分理解这些参数背后的意义之后,才能构建出既高效又省电的扫描模块。

3.2 扫描结果处理机制(ScanCallback实现)

扫描的核心目的不仅是“看到”设备,更重要的是从中提取有用的信息并做出决策。Android通过 ScanCallback 抽象类提供三种回调方式:

  • onScanResult(int callbackType, ScanResult result)
  • onBatchScanResults(List<ScanResult> results)
  • onScanFailed(int errorCode)

其中, ScanResult 对象封装了每次扫描捕获的完整信息,包括设备实例、RSSI信号强度、原始广播数据等。

### 解析 ScanResult 中的关键字段

@Override
public void onScanResult(int callbackType, ScanResult result) {
    BluetoothDevice device = result.getDevice();
    int rssi = result.getRssi();
    byte[] scanRecord = result.getScanRecord().getBytes();
    long timestampNanos = result.getTimestampNanos();

    String name = device.getName() != null ? device.getName() : "Unnamed";
    String address = device.getAddress();

    Log.d("BLE_SCAN", String.format("Found device: %s [%s], RSSI=%ddBm", name, address, rssi));
}
字段解释与应用场景
字段 类型 说明
device BluetoothDevice 设备句柄,用于后续连接
rssi int 接收信号强度指示,单位dBm,典型范围[-100, 0]
scanRecord byte[] 原始广播包数据,包含名称、UUID、制造商数据等
timestampNanos long 扫描发生的时间戳(纳秒级)

其中, rssi 可用于粗略估算设备距离(需结合发射功率TxPower计算),而 scanRecord 则是解析设备身份的关键。

### 广播数据结构解析与UUID提取

BLE广播帧遵循特定格式,由多个AD Structure组成,每个结构包含长度、类型和数据三部分。以下是一个解析Service UUID的工具方法:

public static List<ParcelUuid> extractServiceUuids(byte[] scanRecord) {
    List<ParcelUuid> uuids = new ArrayList<>();
    int index = 0;

    while (index < scanRecord.length) {
        int length = scanRecord[index];
        if (length == 0) break;
        int type = scanRecord[index + 1];

        switch (type) {
            case 0x02: // Incomplete List of 16-bit Service Class UUIDs
            case 0x03: // Complete List of 16-bit Service Class UUIDs
                for (int i = 2; i < length; i += 2) {
                    int uuid16 = (scanRecord[index + i] & 0xFF) |
                                ((scanRecord[index + i + 1] & 0xFF) << 8);
                    uuids.add(ParcelUuid.fromString(String.format(
                            "0000%04X-0000-1000-8000-00805F9B34FB", uuid16)));
                }
                break;

            case 0x06: // Incomplete List of 128-bit UUIDs
            case 0x07: // Complete List of 128-bit UUIDs
                if (length >= 17) {
                    byte[] uuidBytes = new byte[16];
                    System.arraycopy(scanRecord, index + 2, uuidBytes, 0, 16);
                    ParcelUuid uuid = new ParcelUuid(java.util.UUID.nameUUIDFromBytes(uuidBytes));
                    uuids.add(uuid);
                }
                break;
        }
        index += length + 1;
    }
    return uuids;
}
逐行逻辑分析
  1. 循环遍历整个 scanRecord 数组,按AD Structure格式解析。
  2. 每个结构以长度字节开头,随后是AD Type字段。
  3. 对类型 0x02/0x03 (16位UUID列表),逐对读取两个字节并转换为标准BLE UUID。
  4. 0x06/0x07 (128位UUID),复制16字节数组构造 ParcelUuid
  5. 返回所有识别出的服务UUID集合。

此方法可用于判断设备是否广播了目标服务,例如Heart Rate Service ( 0000180D-0000-1000-8000-00805F9B34FB )。

### 使用 ScanFilter 实现精准设备匹配

相比在回调中手动解析广播数据,使用 ScanFilter 可在系统层提前过滤无效设备,显著提升效率:

ParcelUuid targetUuid = ParcelUuid.fromString("0000180F-0000-1000-8000-00805F9B34FB"); // Battery Service

ScanFilter filter = new ScanFilter.Builder()
        .setServiceUuid(targetUuid)
        .setDeviceName("SmartSensor_01")
        .build();

List<ScanFilter> filters = Collections.singletonList(filter);

支持的过滤条件包括:
- setServiceUuid() :匹配广播中包含的Service UUID
- setDeviceName() :按设备名过滤
- setDeviceAddress() :按MAC地址精确匹配
- setManufacturerData() :匹配厂商特定数据

⚠️ 注意:并非所有设备都支持硬件级过滤。某些手机芯片(如部分高通平台)会在驱动层面拦截不匹配的广告包,而其他设备则仍会上报再由Framework过滤。

### 表格:ScanFilter 支持的过滤类型对比

过滤方式 是否硬件加速 适用场景 性能影响
Service UUID 多数支持 发现特定服务设备 高效
Device Name 名称已知的设备 中等
MAC Address 已配对设备重连 极高效
Manufacturer Data 视设备而定 自定义标识设备 高效

### 动态过滤逻辑扩展设计

对于复杂设备识别逻辑(如某UUID+特定厂商数据组合),可结合 ScanFilter 与回调后处理双重机制:

private boolean isValidCustomDevice(ScanResult result) {
    byte[] manufacturerData = result.getScanRecord().getManufacturerSpecificData(0x0590); // Apple Inc.
    if (manufacturerData != null && manufacturerData.length > 0) {
        // 检查iBeacon帧结构或其他私有协议
        return true;
    }
    return false;
}

这种方式兼顾了系统级过滤效率与灵活性。

### Mermaid 流程图:扫描结果处理流程

graph LR
    A[收到 onScanResult] --> B{是否设置了 ScanFilter?}
    B -- 是 --> C[系统已过滤]
    B -- 否 --> D[应用层解析 scanRecord]
    C --> E[提取 BluetoothDevice]
    D --> E
    E --> F[解析 RSSI 和名称]
    F --> G{是否为目标设备?}
    G -- 是 --> H[加入设备列表]
    G -- 否 --> I[丢弃]
    H --> J[通知 UI 更新]

该流程体现了“先过滤、再解析、最后决策”的三层处理模型,确保系统资源被有效利用。

### 数据去重与缓存策略

由于同一设备可能在短时间内多次广播,直接添加会导致列表重复。建议维护一个 Map<String, ScanResult> 以MAC地址为键进行去重:

private final Map<String, ScanResult> scannedDevices = new ConcurrentHashMap<>();

void handleScanResult(ScanResult result) {
    String mac = result.getDevice().getAddress();
    scannedDevices.put(mac, result);

    // 更新UI适配器
    runOnUiThread(() -> adapter.notifyDataSetChanged());
}

也可引入TTL机制自动清除长时间未更新的设备。


(篇幅限制,3.3 和 3.4 节将继续展开……)

4. BLE设备连接建立与状态监控编程

在现代移动物联网应用中,Android端与BLE(Bluetooth Low Energy)外设的稳定连接是实现数据交互的核心前提。本章聚焦于从扫描结果中选定目标设备后,如何发起连接请求、管理连接生命周期,并通过事件回调机制实时监控链路状态变化。相较于传统蓝牙通信,BLE采用“客户端-服务器”架构,其中Android设备通常作为GATT客户端,主动连接并操作远端BLE服务器上的服务与特征值。因此,掌握 connectGatt() 方法调用细节、 BluetoothGattCallback 事件体系以及服务发现流程,是构建高可靠性通信链路的关键。

整个连接过程并非一次性的同步操作,而是由多个异步阶段构成:首先是物理层连接建立,随后进入GATT服务发现阶段,最终才能进行特征值读写等数据交互。每个环节都可能因信号强度、设备响应延迟或系统资源限制而失败,因此必须引入健壮的状态机模型和异常处理策略。此外,随着Android系统对后台行为管控日趋严格(如Doze模式、应用休眠),保持长连接稳定性成为一大挑战,需结合心跳包、重连机制与MTU优化等多种手段综合应对。

本章将深入剖析连接建立全过程的技术要点,涵盖参数配置逻辑、状态转换机制、错误码解析及保活设计原则,并通过代码示例、流程图与表格形式系统化呈现关键实践路径,帮助开发者构建具备工业级稳定性的BLE连接控制模块。

4.1 设备连接请求发起(connectGatt方法调用细节)

当完成设备扫描并筛选出目标BLE外围设备后,下一步即调用 BluetoothDevice.connectGatt() 方法启动连接流程。该方法返回一个 BluetoothGatt 实例,作为后续所有GATT操作的入口对象。理解其参数含义与使用场景,直接影响连接成功率与功耗表现。

4.1.1 参数autoConnect的选择影响与适用场景

connectGatt() 方法提供多个重载版本,最常用的是以下签名:

public BluetoothGatt connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback)

其中核心参数为 autoConnect ,其取值决定连接策略的行为模式:

autoConnect 值 行为描述 适用场景
false 立即尝试建立连接,若设备未广播则快速失败 主动连接已知设备,用户点击“连接”按钮
true 不立即连接,等待设备可被发现时自动连接 后台持久化连接,如手环待机唤醒
true + 扫描过滤 结合低功耗扫描策略,在后台监听特定设备 长期监测信标(beacon)或医疗设备

建议 :对于大多数UI驱动的应用场景(如智能家居App连接灯泡),应设置 autoConnect=false ,以获得更快的反馈;而对于需要后台持续连接的穿戴设备,则可启用 autoConnect=true 并配合 ScanFilter 提升效率。

示例代码:发起非自动连接请求
BluetoothGatt gatt = bluetoothDevice.connectGatt(context, false, new BluetoothGattCallback() {
    @Override
    public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
        if (status == BluetoothGatt.GATT_SUCCESS) {
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                Log.d("BLE", "设备连接成功");
                // 开始服务发现
                gatt.discoverServices();
            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                Log.d("BLE", "设备断开连接");
            }
        } else {
            Log.e("BLE", "连接失败,状态码:" + status);
        }
    }
});

逐行解读分析
- 第1行:调用 connectGatt() ,传入上下文、 autoConnect=false 表示立即连接。
- 第2–13行:注册匿名 BluetoothGattCallback 实现事件监听。
- 第4–9行: onConnectionStateChange 是核心回调,用于判断连接是否成功。
- 第6行: BluetoothGatt.GATT_SUCCESS 表示协议层操作成功,而非仅链路通断。
- 第7行: STATE_CONNECTED 触发后即可安全调用 discoverServices()
- 第11行:非成功状态需记录错误码以便后续诊断。

值得注意的是,即使 status != GATT_SUCCESS ,也可能出现临时性失败(如设备忙)。此时不应直接提示用户“连接失败”,而应启动指数退避重试机制(见 4.2.2 节)。

4.1.2 Context上下文传入的最佳实践

connectGatt() 中的 Context 参数主要用于绑定系统服务与权限校验。虽然允许传入 Application Context Activity Context ,但存在显著差异:

graph TD
    A[Context类型] --> B{Application Context}
    A --> C{Activity Context}
    B --> D[生命周期长,适合后台连接]
    C --> E[生命周期短,易引发内存泄漏]
    D --> F[推荐用于Service中维持连接]
    E --> G[仅限临时连接,需及时关闭]
推荐实践方案:
  1. Service 中执行连接操作 :使用 getApplicationContext() ,避免Activity销毁导致引用丢失。
  2. 避免持有Activity引用 :防止 BluetoothGatt 持有 Activity 导致无法GC。
  3. 统一连接管理器模式 :创建单例类 BleConnectionManager 管理所有 BluetoothGatt 实例。
public class BleConnectionManager {
    private static BleConnectionManager instance;
    private final Map<String, BluetoothGatt> deviceGattMap = new ConcurrentHashMap<>();

    public synchronized BluetoothGatt connectToDevice(Context appContext, BluetoothDevice device) {
        return device.connectGatt(appContext, false, gattCallback);
    }

    private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
            String address = gatt.getDevice().getAddress();
            if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                deviceGattMap.remove(address); // 清理映射表
            }
        }
    };
}

参数说明
- appContext :确保在整个应用生命周期内有效。
- deviceGattMap :使用线程安全集合防止并发问题。
- synchronized :保证多线程环境下连接操作原子性。

此设计实现了连接资源的集中管理,便于实现连接池、去重连接与跨页面共享状态等功能。

4.2 BluetoothGattCallback事件监听体系

BluetoothGattCallback 是 BLE 连接期间所有异步事件的中枢处理器,其回调方法构成了状态机的基础输入源。正确理解和处理这些事件,是实现稳定通信的前提。

4.2.1 onConnectionStateChange回调的状态转换逻辑(CONNECTED/DISCONNECTED)

该回调是连接管理中最关键的方法,原型如下:

public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState)
  • status :表示GATT协议操作的结果状态,常见值包括:
  • BluetoothGatt.GATT_SUCCESS (0):操作成功
  • BluetoothGatt.GATT_FAILURE (~1):通用失败
  • 其他错误码(详见 BluetoothStatusCodes)
  • newState :当前连接状态,主要为:
  • BluetoothProfile.STATE_CONNECTED
  • BluetoothProfile.STATE_DISCONNECTED

典型的状态流转如下表所示:

当前状态 事件触发 新状态 处理建议
DISCONNECTED 用户点击连接 CONNECTED 调用 discoverServices()
CONNECTED 设备关机/超出范围 DISCONNECTED 启动重连机制
CONNECTED 系统回收资源 DISCONNECTED 记录日志并通知UI
完整状态机处理示例:
@Override
public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) {
    super.onConnectionStateChange(gatt, status, newState);

    if (status == BluetoothGatt.GATT_SUCCESS) {
        switch (newState) {
            case BluetoothProfile.STATE_CONNECTED:
                handleDeviceConnected(gatt);
                break;
            case BluetoothProfile.STATE_DISCONNECTED:
                handleDeviceDisconnected(gatt);
                break;
        }
    } else {
        handleError(gatt, "Connection failed with status: " + status);
    }
}

private void handleDeviceConnected(BluetoothGatt gatt) {
    Log.i("BLE", "Connected to " + gatt.getDevice().getName());
    broadcastUpdate(ACTION_GATT_CONNECTED);
    gatt.discoverServices(); // 主动触发服务发现
}

private void handleDeviceDisconnected(BluetoothGatt gatt) {
    Log.w("BLE", "Disconnected from " + gatt.getDevice().getName());
    closeGattResources(gatt); // 释放资源
    attemptReconnection(gatt.getDevice()); // 尝试重连
}

逻辑分析
- 成功状态下根据 newState 分支处理;
- 连接成功后立即发起服务发现;
- 断开时清理资源并触发重连逻辑。

4.2.2 支持重连机制的设计模式(指数退避算法应用)

频繁重连会加剧功耗与系统负担,合理的策略是采用 指数退避(Exponential Backoff) 算法:

private int retryCount = 0;
private static final int MAX_RETRY_COUNT = 5;
private static final long INITIAL_DELAY_MS = 1000;

private void attemptReconnection(BluetoothDevice device) {
    if (retryCount >= MAX_RETRY_COUNT) {
        Log.e("BLE", "Maximum retry attempts reached");
        return;
    }

    long delay = INITIAL_DELAY_MS * (1 << retryCount); // 2^n 增长
    retryCount++;

    new Handler(Looper.getMainLooper()).postDelayed(() -> {
        BluetoothGatt newGatt = device.connectGatt(context, false, gattCallback);
        if (newGatt != null) {
            Log.d("BLE", "Reconnection attempt #" + retryCount + " started");
        }
    }, delay);
}

参数说明
- 1 << retryCount :位运算实现 2^n,提升性能;
- Handler :延时执行,避免阻塞主线程;
- 最大重试次数防止无限循环。

该机制可在网络不稳定时平滑恢复连接,同时避免对系统造成过大压力。

4.2.3 MTU更改响应(onMtuChanged)的处理时机

MTU(Maximum Transmission Unit)决定了单次传输的最大数据量,默认为23字节。通过协商更大MTU(如247),可显著提升大数据传输效率。

@Override
public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        Log.d("BLE", "MTU updated to: " + mtu);
        this.negotiatedMtu = mtu; // 存储协商后的值
        enableNotifications(); // 可在此后开启通知
    } else {
        Log.w("BLE", "MTU change failed: " + status);
        this.negotiatedMtu = 23; // 回退默认值
    }
}

调用时机 :应在 discoverServices() 成功后调用 requestMtu(247)

sequenceDiagram
    participant App
    participant Device
    App->>Device: connectGatt()
    Device-->>App: onConnectionStateChange(CONNECTED)
    App->>Device: discoverServices()
    Device-->>App: onServicesDiscovered(SUCCESS)
    App->>Device: requestMtu(247)
    Device-->>App: onMtuChanged(mtu=247, SUCCESS)
    App->>App: 使用新MTU进行数据收发

4.3 服务发现流程控制(discoverServices异步操作)

4.3.1 服务发现成功与失败的判断依据

调用 gatt.discoverServices() 后,系统异步获取远程设备的GATT服务列表,结果通过回调返回:

@Override
public void onServicesDiscovered(BluetoothGatt gatt, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        List<BluetoothGattService> services = gatt.getServices();
        Log.d("BLE", "Found " + services.size() + " services");
        processServices(services);
    } else {
        Log.e("BLE", "Service discovery failed: " + status);
        reconnectOrReportError();
    }
}
  • 成功条件: status == GATT_SUCCESS
  • 失败原因可能包括:链路中断、设备未响应、超时等

4.3.2 BluetoothGattService列表遍历与关键服务查找

通常根据预定义 UUID 查找所需服务:

private static final UUID SERVICE_UUID = UUID.fromString("0000180F-0000-1000-8000-00805F9B34FB"); // Battery Service

private void processServices(List<BluetoothGattService> services) {
    for (BluetoothGattService service : services) {
        if (service.getUuid().equals(SERVICE_UUID)) {
            BluetoothGattCharacteristic charac = service.getCharacteristic(
                UUID.fromString("00002A19-0000-1000-8000-00805F9B34FB")
            );
            if (charac != null) {
                readBatteryLevel(charac);
            }
        }
    }
}

扩展建议 :可构建服务映射表,支持动态配置不同设备的服务结构。

4.4 连接稳定性保障措施

4.4.1 心跳包机制与链路保活设计

某些设备在无数据交互一段时间后会自动断开。可通过定期发送“空写”或读取固定特征值实现保活:

private void startHeartbeat(BluetoothGatt gatt) {
    Handler handler = new Handler(Looper.getMainLooper());
    Runnable heartbeatTask = new Runnable() {
        @Override
        public void run() {
            BluetoothGattService service = gatt.getService(SERVICE_UUID);
            if (service != null) {
                BluetoothGattCharacteristic charac = service.getCharacteristic(HEARTBEAT_CHAR_UUID);
                if (charac != null && (charac.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) {
                    charac.setValue(new byte[]{0x01});
                    gatt.writeCharacteristic(charac);
                }
            }
            handler.postDelayed(this, 30000); // 每30秒一次
        }
    };
    handler.post(heartbeatTask);
}

注意:避免过于频繁的心跳导致功耗上升。

4.4.2 异常断开原因码分析与日志记录策略

断开时的 status 码包含丰富信息,例如:

错误码(十六进制) 含义
0x08 Connection Timeout
0x13 Connection Terminated by Peer
0x3E Failed Contact Remote Device

建议建立错误码翻译表,并上传至APM系统用于远程诊断。

private String parseDisconnectReason(int statusCode) {
    switch (statusCode) {
        case 0x08: return "Timeout";
        case 0x13: return "Peer Termination";
        case 0x3E: return "Device Unreachable";
        default: return "Unknown(" + Integer.toHexString(statusCode) + ")";
    }
}

结合时间戳与设备型号,形成完整的故障追踪日志链。

5. 特征值操作与双向数据通信实现

在现代物联网应用中,Android设备通过BLE(Bluetooth Low Energy)与外围设备进行高效、低功耗的双向通信已成为标准范式。完成设备扫描与连接后,真正的业务交互核心在于对GATT服务中的 特征值(Characteristic) 进行读取、写入和通知订阅。本章将深入探讨如何在Android平台上实现完整的特征值操作流程,涵盖从底层API调用到上层数据处理机制的设计,构建稳定可靠的双向数据通道。

特征值作为GATT架构中最基本的数据单元,承载着传感器数据上报、控制指令下发、状态同步等关键信息。其操作方式并非简单的“读/写”动作叠加,而是涉及异步回调管理、线程安全控制、协议格式解析以及错误恢复策略等多个维度的技术挑战。尤其在高频率数据传输场景下(如心率监测、运动轨迹采集),若缺乏合理的缓冲与解码机制,极易导致UI卡顿或数据丢失。

为此,开发者必须掌握 BluetoothGatt 提供的三大核心操作接口: readCharacteristic() writeCharacteristic() setCharacteristicNotification() ,并理解其背后的状态机模型与事件驱动机制。更重要的是,要基于这些原生能力封装出可复用、可扩展的数据通信框架,以应对多设备、多协议共存的复杂环境。

本章将以实际开发中常见的智能手环数据采集为例,逐步演示如何发起一次特征值读取请求,如何构造符合硬件规范的写入命令,以及如何建立持续的数据流接收通道。同时引入通用解析器设计思想,提升系统对不同数据格式(二进制、JSON、TLV等)的适应能力,为后续工程化落地提供坚实支撑。

5.1 特征值读取操作(readCharacteristic)同步机制

特征值读取是客户端主动获取服务器端数据的基本手段,适用于那些不需要实时推送、仅在特定时刻查询的数据项,例如电池电量、固件版本号或当前工作模式等。Android BLE API 提供了 readCharacteristic() 方法来发起读操作,但由于蓝牙通信本质上是异步过程,所有结果均通过 BluetoothGattCallback 回调返回,因此必须合理设计状态管理和数据提取逻辑。

5.1.1 主动查询传感器数据的实际案例

假设我们正在开发一款健康监测App,需要定期从已连接的手环设备读取体温数据。该数据存储于一个UUID为 00002A1C-0000-1000-8000-00805F9B34FB 的特征值中,属于只读属性(PROPERTY_READ)。以下是典型的读取流程实现:

public void readTemperature(BluetoothGatt gatt) {
    BluetoothGattService service = gatt.getService(UUID.fromString("0000180A-0000-1000-8000-00805F9B34FB"));
    if (service == null) {
        Log.e("BLE", "Service not found");
        return;
    }

    BluetoothGattCharacteristic tempChar = service.getCharacteristic(UUID.fromString("00002A1C-0000-1000-8000-00805F9B34FB"));
    if (tempChar == null) {
        Log.e("BLE", "Characteristic not found");
        return;
    }

    if ((tempChar.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0) {
        boolean success = gatt.readCharacteristic(temp, tempChar);
        if (!success) {
            Log.e("BLE", "Failed to initiate read request");
        } else {
            Log.d("BLE", "Read request sent successfully");
        }
    } else {
        Log.w("BLE", "Characteristic does not support reading");
    }
}
代码逻辑逐行分析:
  1. 第2行 :通过 getService() 查找目标服务。注意此处使用标准设备信息服务UUID(Device Information Service),实际项目应根据硬件文档确定正确服务UUID。
  2. 第6行 :获取具体特征值对象。若未发现该特征值,说明服务发现不完整或设备未广播此数据。
  3. 第10行 :检查特征值是否支持读取操作。这是必要的防护措施,避免向不具备读权限的特征发送无效请求。
  4. 第13行 :调用 gatt.readCharacteristic() 发起读请求。该方法返回布尔值表示本地队列是否接受该操作,但不代表远程设备响应成功。
  5. 第17行 :日志输出用于调试追踪请求状态。

真正获取到数据是在 BluetoothGattCallback.onCharacteristicRead() 中完成的:

@Override
public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) {
    if (status == BluetoothGatt.GATT_SUCCESS) {
        byte[] data = characteristic.getValue();
        float temperature = parseTemperature(data); // 自定义解析函数
        Log.d("BLE", "Temperature: " + temperature + "°C");
        notifyUiOfNewData(temperature);
    } else {
        Log.e("BLE", "Read failed with status: " + status);
    }
}
参数说明:
  • characteristic :包含原始字节数组 getValue() 的目标特征值对象。
  • status :指示操作结果,常见值包括 GATT_SUCCESS , GATT_FAILURE , GATT_READ_NOT_PERMITTED 等。

⚠️ 注意:某些设备在快速连续读取时会触发“busy”状态(状态码11),建议加入最小间隔控制或重试机制。

5.1.2 onCharacteristicRead回调中的数据解析流程

接收到原始字节流后,需依据设备厂商定义的编码规则进行反序列化。以下是一个典型浮点温度值的解析示例(采用IEEE 754单精度格式):

private float parseTemperature(byte[] value) {
    if (value.length < 4) throw new IllegalArgumentException("Insufficient data length");

    // 小端序转大端序(LE → BE)
    ByteBuffer buffer = ByteBuffer.wrap(value).order(ByteOrder.LITTLE_ENDIAN);
    return buffer.getFloat();
}
字节索引 含义 示例值(Hex)
0 温度低字节 0x4A
1 温度次低字节 0x41
2 温度次高字节 0x80
3 温度高字节 0x3F

经解析得 36.5°C ,符合人体正常体温范围。

更复杂的设备可能采用缩放因子或偏移量计算真实值,例如:

真实值 = (rawValue * scale) + offset

此时应在配置文件中维护映射表:

{
  "uuid": "00002a1c-0000-1000-8000-00805f9b34fb",
  "name": "Body Temperature",
  "unit": "°C",
  "format": "float32",
  "endian": "little",
  "scale": 0.01,
  "offset": 0
}

此类元数据可用于构建通用解析引擎,提高代码复用性。

数据流转流程图(Mermaid)
sequenceDiagram
    participant App
    participant Gatt
    participant Peripheral

    App->>Gatt: readCharacteristic(char)
    Gatt->>Peripheral: ATT_READ_REQ(UUID=2A1C)
    Peripheral-->>Gatt: ATT_READ_RSP(Value=0x4A41803F)
    Gatt-->>App: onCharacteristicRead(status=SUCCESS, value=[...])
    App->>App: parseTemperature(value) → 36.5
    App->>UI: 更新体温显示

该图清晰展示了从应用层发起请求到最终数据呈现的完整路径,体现了BLE通信的异步非阻塞性质。

5.2 特征值写入控制(writeCharacteristic)编码实践

除了读取数据外,移动App通常还需向外围设备发送控制命令,如启动测量、切换模式或设置报警阈值。这类操作依赖于特征值的 写入功能 ,由 writeCharacteristic() 方法执行。然而,不同的写入类型会影响性能表现与可靠性保障,开发者需根据应用场景做出合理选择。

5.2.1 写入类型选择:WRITE_NO_RESPONSE vs WRITE_WITH_RESPONSE

Android BLE 支持两种写入模式:

模式 常量定义 是否等待确认 适用场景
带响应写入 WRITE_TYPE_DEFAULT 关键指令(如配置修改)
无响应写入 WRITE_TYPE_NO_RESPONSE 高频数据上传(如日志批量发送)

两者的本质区别在于是否启用L2CAP层的ACK机制。使用 WRITE_WITH_RESPONSE 时,远端设备必须回复 ATT_WRITE_RSP ,否则视为失败;而 WRITE_NO_RESPONSE 则直接丢弃,适合容忍一定丢包率但追求吞吐量的场景。

设置写入类型的代码如下:

BluetoothGattCharacteristic commandChar = ...;
commandChar.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE);

boolean result = gatt.writeCharacteristic(commandChar);

📌 推荐策略:对于影响设备行为的命令(如“开启LED灯”),务必使用带响应写入;而对于大量传感器采样数据回传,可采用无响应模式以降低通信开销。

5.2.2 发送控制指令的字节序列构造规范

写入操作的关键在于构造符合硬件协议的数据包。以某款智能锁为例,其开锁指令格式如下:

起始符 命令码 参数长度 参数数据 校验和 结束符
0xAA 0x01 0x04 [PIN] XOR 0xBB

Java实现:

public byte[] buildUnlockCommand(int pin) {
    byte[] pinBytes = intToBytesLE(pin); // 小端序转换
    byte[] packet = new byte[8];

    packet[0] = (byte) 0xAA;
    packet[1] = (byte) 0x01;
    packet[2] = (byte) 0x04;
    System.arraycopy(pinBytes, 0, packet, 3, 4);

    byte checksum = 0;
    for (int i = 0; i < 7; i++) {
        checksum ^= packet[i];
    }
    packet[7] = checksum;

    return packet;
}

// 工具方法:整数转小端序字节数组
private byte[] intToBytesLE(int value) {
    return new byte[]{
        (byte) (value & 0xFF),
        (byte) ((value >> 8) & 0xFF),
        (byte) ((value >> 16) & 0xFF),
        (byte) ((value >> 24) & 0xFF)
    };
}

随后将其写入指定特征值:

BluetoothGattCharacteristic char = gatt.getService(lockServiceUuid)
        .getCharacteristic(commandCharUuid);

char.setValue(buildUnlockCommand(1234));
gatt.writeCharacteristic(char);
异常处理注意事项:
  • 若写入失败( onCharacteristicWrite 返回非 GATT_SUCCESS ),应记录错误码并尝试重发;
  • 对于敏感操作(如解锁),建议结合加密签名防止伪造指令;
  • 避免在主线程执行写入操作,以防ANR。

5.3 开启通知订阅以接收实时数据流

许多应用场景要求设备主动推送数据,如心率变化、步数更新或GPS坐标上报。此时需启用 通知(Notify)或指示(Indicate) 功能,使外围设备能在特征值变化时自动发送数据包至中心设备。

5.3.1 setCharacteristicNotification启用通知通道

首先调用系统API打开本地通知开关:

gatt.setCharacteristicNotification(heartRateChar, true);

这一步仅开启Android本地接收能力,并未通知远端设备开始发送数据。

5.3.2 配置CCCD描述符(CLIENT_CHARACTERISTIC_CONFIG)写入流程

要真正激活远程通知,必须写入 客户端特征配置描述符(CCCD) ,其UUID固定为 00002902-0000-1000-8000-00805F9B34FB

BluetoothGattDescriptor descriptor = heartRateChar.getDescriptor(
        UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"));

if (descriptor != null) {
    descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
    gatt.writeDescriptor(descriptor);
}

✅ 成功写入后,设备即可开始周期性发送心率数据。

5.3.3 onCharacteristicChanged回调触发条件与高频数据处理缓冲机制

当设备发出通知时,系统回调 onCharacteristicChanged

@Override
public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
    byte[] data = characteristic.getValue();
    int heartRate = data[0] & 0xFF;

    // 缓冲池管理
    DataBuffer.getInstance().add(new SensorData(System.currentTimeMillis(), heartRate));
    // 分发至观察者
    EventBus.getDefault().post(new HeartRateEvent(heartRate));
}

面对高频数据流(如每秒10次更新),应引入环形缓冲区或双缓冲机制避免GC压力:

class DataBuffer {
    private final Queue<SensorData> buffer = new ConcurrentLinkedQueue<>();
    private static final int MAX_SIZE = 100;

    public void add(SensorData data) {
        buffer.offer(data);
        if (buffer.size() > MAX_SIZE) {
            buffer.poll(); // 丢弃最旧数据
        }
    }
}

此外,可通过设置 NOTIFICATION_RATE_LIMIT 控制UI刷新频率,避免过度绘制。

流程图展示完整订阅过程:
graph TD
    A[App] --> B[gatt.setCharacteristicNotification(true)]
    B --> C[获取CCCD描述符]
    C --> D[写入ENABLE_NOTIFICATION_VALUE]
    D --> E[Peripheral开始发送Notify]
    E --> F{收到ATT_HANDLE_VALUE_NTF?}
    F --> G[触发onCharacteristicChanged]
    G --> H[解析数据并更新UI]

5.4 数据封装与解析通用框架设计

随着接入设备种类增多,手动编写每个特征值的解析逻辑将变得难以维护。为此,需抽象出一套 通用数据解析框架 ,实现自动识别与分发。

5.4.1 定义统一的数据解析接口ParseStrategy

public interface ParseStrategy<T> {
    T parse(byte[] rawData);
    Class<T> getTargetType();
}

示例实现——JSON解析器:

public class JsonParseStrategy implements ParseStrategy<Map<String, Object>> {
    @Override
    public Map<String, Object> parse(byte[] rawData) {
        String json = new String(rawData, StandardCharsets.UTF_8);
        return new Gson().fromJson(json, Map.class);
    }

    @Override
    public Class<Map<String, Object>> getTargetType() {
        return Map.class;
    }
}

5.4.2 支持JSON/Binary等多种格式的自动识别机制

通过前缀标识判断数据类型:

public ParseStrategy<?> detectStrategy(byte[] data) {
    if (data.length > 0 && data[0] == '{') {
        return new JsonParseStrategy();
    } else if (isBinaryFormat(data)) {
        return new BinaryParseStrategy();
    } else {
        return new DefaultParseStrategy();
    }
}

最终形成可插拔式解析管道,显著提升系统的灵活性与可维护性。

6. 源码工程化实践与全链路测试验证

6.1 模块化代码结构设计原则

在大型Android BLE项目中,良好的模块划分是保障可维护性与扩展性的核心。我们采用分层架构思想,将蓝牙通信逻辑解耦为独立组件,遵循单一职责原则(SRP)进行封装。

6.1.1 分离BLE Manager、Data Repository与ViewModel职责边界

  • BleManager :负责底层蓝牙操作,包括扫描、连接、服务发现、特征值读写等,持有 BluetoothAdapter BluetoothGatt 实例。
  • DataRepository :作为数据中转层,处理来自BleManager的原始字节流,调用解析器转换为业务对象,并提供缓存机制。
  • ViewModel :基于 LiveData StateFlow 向UI暴露可观测的数据流,响应用户指令并转发至Repository。
// 示例:BleManager核心结构
class BleManager(private val context: Context) {
    private var bluetoothAdapter: BluetoothAdapter? = null
    private var bluetoothGatt: BluetoothGatt? = null

    fun connect(device: BluetoothDevice): Boolean {
        bluetoothGatt = device.connectGatt(context, false, gattCallback)
        return true
    }

    private val gattCallback = object : BluetoothGattCallback() {
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            when (newState) {
                BluetoothProfile.STATE_CONNECTED -> {
                    Log.d("BleManager", "Connected to ${gatt.device.address}")
                    gatt.discoverServices()
                }
                BluetoothProfile.STATE_DISCONNECTED -> {
                    Log.d("BleManager", "Disconnected from ${gatt.device.address}")
                }
            }
        }

        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                val service = gatt.getService(SERVICE_UUID)
                val characteristic = service?.getCharacteristic(CHARACTERISTIC_UUID_RX)
                characteristic?.let { enableNotification(it) }
            }
        }

        override fun onCharacteristicChanged(
            gatt: BluetoothGatt,
            characteristic: BluetoothGattCharacteristic
        ) {
            DataRepository.processReceivedData(characteristic.value)
        }
    }
}

说明: BleManager 不直接更新UI,而是通过发布事件或回调通知上层。

6.1.2 使用Observer模式实现UI层与通信层解耦

使用 LiveData Flow 实现观察者模式,确保UI仅订阅所需状态变更:

// ViewModel 中暴露连接状态
class DeviceViewModel : ViewModel() {
    private val _connectionState = MutableLiveData<ConnectionState>()
    val connectionState: LiveData<ConnectionState> = _connectionState

    fun connectToDevice(device: BluetoothDevice) {
        BleManager.connect(device)
        // BleManager内部通过EventBus或回调更新_connectionState
    }
}

此设计使得UI无需感知蓝牙协议细节,仅需响应状态变化。

6.2 日志系统集成与问题排查手段

6.2.1 利用Logcat输出关键状态流转信息

建议定义统一的日志标签和级别,便于过滤分析:

日志类型 Tag示例 输出内容示例
扫描事件 BLE_SCAN Found device: XX:XX RSSI=-78
连接状态 BLE_CONN Connected to 00:11:22
特征值变更 BLE_DATA_IN Received 20 bytes @15:30:22.123
写入操作 BLE_DATA_OUT Wrote command 0x01
错误信息 BLE_ERROR GATT error status=133

6.2.2 添加时间戳与线程标识提升调试效率

自定义Logger工具类增强可读性:

public class BleLogger {
    public static void d(String tag, String msg) {
        String threadInfo = Thread.currentThread().getName();
        long millis = System.currentTimeMillis() % 1000;
        String time = new SimpleDateFormat("HH:mm:ss.SSS").format(new Date());
        Log.d(tag, "[" + time + "." + millis + "] [" + threadInfo + "] " + msg);
    }
}

输出示例:

[14:22:10.456] [main] Connected to 00:1A:7D:DA:71:13
[14:22:10.461] [Binder:1234_1] onServicesDiscovered success

该格式有助于识别异步操作顺序及潜在线程安全问题。

6.3 硬件联调与真实设备测试流程

6.3.1 使用nRF Connect等工具验证GATT服务一致性

操作步骤:

  1. 安装 nRF Connect for Mobile (支持Android/iOS)
  2. 开启目标BLE设备广播
  3. 在App中搜索设备并连接
  4. 查看GATT Server视图中的Service列表
  5. 对比硬件文档定义的UUID结构是否一致
UUID类型 预期值 实际值(nRF检测) 是否匹配
Service F000AA00-0451-4000-B000-000000000000 ✅ 相同
RX Characteristic F000AA01-… ✅ 存在且属性为NOTIFY
TX Characteristic F000AA02-… ❌ 属性缺失WRITE权限

若发现差异,需联系固件团队修正GATT配置。

6..3.2 对比预期行为与实际通信数据包差异

使用Wireshark + Bluetooth Sniffer抓包分析空中接口数据:

// 正常写入指令(HEX)
-> Write Request: Handle=0x0012 Value=0x01 0x03 0xFF
<- Write Response

// 异常情况(无响应)
-> Write Request: Handle=0x0012 Value=0x01 0x03 0xFF
[No Response within 30s → Timeout]

常见问题包括MTU过小导致分包失败、CCCD未正确启用等。

6.4 性能评估与发布前检查清单

6.4.1 功耗测试:扫描间隔与连接参数调优建议

使用Android Profiler监测CPU与网络活动,结合Battery Historian分析耗电来源。

参数组合 平均电流消耗(mA) 建议场景
扫描间隔500ms,窗口300ms 8.2 快速发现设备
扫描间隔2s,窗口500ms 3.1 后台低功耗扫描
连接间隔15ms,超时2s 2.5 高频数据采集
连接间隔100ms,超时10s 1.3 节电模式

推荐策略:动态调整连接参数以平衡实时性与功耗。

6.4.2 兼容性测试矩阵:覆盖主流手机品牌与Android版本

建立标准化测试表格,记录各机型表现:

设备型号 Android版本 蓝牙版本 扫描成功率 连接稳定性 备注
Samsung Galaxy S23 13 5.3 ✅ 100% ✅ 持续连接
Xiaomi Redmi Note 12 12 5.2 ⚠️ 偶尔断连 需重连机制
Huawei P40 Pro 10 (EMUI) 5.1 后台限制需手动授权
OnePlus 11 13 5.3 表现最优
OPPO Reno 8 12 5.2 ⚠️ 90% 广播过滤异常
Google Pixel 6 13 5.2 原生支持良好
vivo X80 12 5.2 ⚠️ MTU协商失败 需降级到23
Sony Xperia 1 IV 13 5.3 稳定
Motorola Edge+ 12 5.2
Nokia G50 11 5.1 ⚠️ 85% ⚠️ 易丢包 不推荐生产环境
LG V60 ThinQ 10 5.1 ❌ 70% ❌ 频繁断开 已弃用

测试应涵盖前台/后台运行、屏幕关闭、多任务切换等典型使用场景。

flowchart TD
    A[启动App] --> B{权限已授权?}
    B -- 是 --> C[开始BLE扫描]
    B -- 否 --> D[请求位置权限]
    D --> C
    C --> E[发现目标设备]
    E --> F[发起connectGatt]
    F --> G{连接成功?}
    G -- 是 --> H[discoverServices]
    G -- 否 --> I[指数退避重试]
    I --> F
    H --> J{服务发现完成?}
    J -- 是 --> K[启用Notify]
    J -- 否 --> L[记录错误日志]
    K --> M[等待onCharacteristicChanged]
    M --> N[解析数据→更新UI]

该流程图为全链路通信提供了可视化控制路径,可用于自动化测试脚本编写。

本文还有配套的精品资源,点击获取 menu-r.4af5f7ec.gif

简介:蓝牙通信是Android平台在物联网应用中的核心技术之一,尤其在低功耗设备交互中具有重要意义。本“蓝牙通信 Android APP源码”项目基于Android Studio开发环境,完整实现了BLE(蓝牙低功耗)的扫描、连接、服务发现、数据读写及通知订阅等功能,并已通过实际硬件模块测试。项目涵盖Android蓝牙API的核心使用方法,包含权限管理、回调机制与代码规范实践,帮助开发者深入掌握BLE通信流程,提升在智能设备互联领域的开发能力。


本文还有配套的精品资源,点击获取
menu-r.4af5f7ec.gif

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐