☰
Android BLE连接调试核心:状态机、MTU协商与服务发现避坑指南
2026/10/17 14:41:27 网站建设 项目流程

简介:本资源是基于Android官方蓝牙低功耗(BLE)开发指南实现的完整Demo工程,面向Android应用开发者及嵌入式通信初学者,聚焦BLE核心流程实践:设备扫描(scan)、连接建立与断开(connection/disconnect)、服务发现(discover service)及UUID交互等关键能力。压缩包共70个文件,涵盖26个Class类文件(含BLE状态管理与回调逻辑)、15个XML布局与配置资源、9个PNG图标素材、7个Java源码文件(主Activity与BluetoothAdapter操作封装),以及APK可运行包、JAR依赖库和项目构建配置文件(project.properties、.classpath等),整体仅1.34MB,轻量易导入。已有525人学习下载,代码结构清晰、模块职责分明,完整复现了Android 4.0+ BLE典型通信链路,附带可直接调试的APK与详细Manifest权限声明,是理解Android BLE协议栈集成与调试的优质入门参考。

1. 这不是“点开就能连”的玩具:Android BLE 官方 Demo 是一套可拆解、可嵌入、带完整状态机的连接验证骨架

你手头有个新蓝牙外设,芯片手册写了支持 GATT Server,UUID 列得清清楚楚,但 Android App 死活 scan 不到?或者 scan 到了,connect() 一调就 timeout,logcat 里只有一行D/BluetoothGatt: connect() - device: XX:XX:XX:XX:XX:XX, auto: false然后石沉大海?别急着骂硬件或改 manifest 权限——问题大概率出在你跳过了 BLE 连接生命周期里那些「看不见却必须走完」的步骤。Android 官方 BLE Demo(即BluetoothLeGatt示例项目)不是教学动画,它是一套经过 Google 内部多轮设备兼容性压测、覆盖从低功耗扫描策略、服务发现重试机制、特征值读写状态同步到连接异常自动恢复的完整工程骨架。它不教你“怎么写一个 Hello World”,而是告诉你“当手机连上心率带、电子秤、温湿度传感器时,系统底层到底在等什么、发什么、收什么、超时后该信谁”。适合正在调试自研 BLE 设备固件的嵌入式工程师、需要把第三方 BLE 模块集成进现有 App 的 Android 开发者,以及被onConnectionStateChange()回调里一堆STATE_CONNECTING/STATE_CONNECTED/STATE_DISCONNECTED状态绕晕的新手。它不能替代你读 Bluetooth Core Spec,但它能让你第一次在真机上看到onServicesDiscovered()被触发时,log 里真正打印出的是哪几个 UUID、哪个 handle 值、是否包含WRITE_NO_RESPONSE属性——这才是调试的起点。


2. 从源码结构到核心流程:为什么这个 Demo 的BluetoothLeService不是简单封装,而是一个状态驱动的通信中枢

官方 Demo 的代码组织看似平铺直叙,实则暗藏三层抽象:最底层是BluetoothAdapter和BluetoothGatt的原生回调桥接;中间层是BluetoothLeService提供的startScan()/connect()/readCharacteristic()等语义化接口;最上层是DeviceControlActivity对 UI 状态与用户操作的响应。这三层不是线性调用,而是通过Handler+MessageQueue+Binder构成的状态流管道。理解这点,才能避免把 Demo 当成“抄完就能跑”的黑匣子。

2.1BluetoothLeService的生命周期绑定与跨进程通信设计

Demo 中BluetoothLeService继承自Service,但关键不在“后台运行”,而在其onBind()返回的IBinder实现了LocalBinder。这意味着 Activity 通过bindService()获取的并非远程代理,而是 Service 实例本身——所有方法调用都是进程内直接调用,零 IPC 开销。这是性能敏感场景(如每秒上报心率数据)的合理选择,但也带来一个隐含约束:Service 必须与 Activity 处于同一进程(android:process未声明即默认)。若你在自己的项目中将 BLE Service 单独切到.ble进程,LocalBinder会失效,mBluetoothLeService将为 null,后续所有操作必然 NPE。

// BluetoothLeService.java 片段 public class BluetoothLeService extends Service { private final IBinder mBinder = new LocalBinder(); public class LocalBinder extends Binder { BluetoothLeService getService() { return BluetoothLeService.this; // 直接返回 this,非代理 } } @Override public IBinder onBind(Intent intent) { return mBinder; // 关键:返回本地 binder } }

提示:LocalBinder模式意味着 Service 的生命周期强依赖于绑定它的组件。若 Activity 被系统回收(如横竖屏重建),onUnbind()触发后 Service 可能被销毁。官方 Demo 在onDestroy()中显式调用mBluetoothGatt.close()和mBluetoothAdapter.cancelDiscovery(),这是防止资源泄漏的硬性要求,不可省略。

2.2 扫描策略:ScanSettings的SCAN_MODE_LOW_LATENCY并非“越快越好”

Demo 使用BluetoothLeScanner启动扫描,其ScanSettings配置直接影响发现成功率。很多人误以为SCAN_MODE_LOW_LATENCY(高频率扫描)一定能最快发现设备,但实际在 Android 8.0+ 上,该模式会强制启用BLE_SCAN_MODE_AGGRESSIVE,导致系统对扫描窗口(scan window)和扫描间隔(scan interval)施加更严苛的限制。若你的外设广播间隔为 1s(1000ms),而系统因省电策略将 scan interval 动态拉长到 2.5s,那么设备可能连续 2 次广播都被错过。

官方 Demo 的处理是:先以SCAN_MODE_BALANCED启动基础扫描,发现目标设备后立即切换至SCAN_MODE_LOW_LATENCY进行连接前的最后确认。这种分阶段策略平衡了功耗与可靠性。其核心逻辑在scanLeDevice()方法中:

// DeviceScanActivity.java 片段 private void scanLeDevice(final boolean enable) { if (enable) { // Step 1: 先用 BALANCED 模式扫描,降低功耗 ScanSettings settings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_BALANCED) .build(); List<ScanFilter> filters = new ArrayList<>(); // 可选:添加 MAC 地址或 Service UUID 过滤,减少回调量 if (!targetDeviceAddress.isEmpty()) { filters.add(new ScanFilter.Builder() .setDeviceAddress(targetDeviceAddress).build()); } mLEScanner.startScan(filters, settings, mScanCallback); } else { mLEScanner.stopScan(mScanCallback); } }

参数说明:ScanSettings.Builder().setScanMode()的三个选项本质是系统调度策略的 hint。SCAN_MODE_LOW_POWER(默认)适合后台长期监听;SCAN_MODE_BALANCED是折中方案,推荐作为初始扫描模式;SCAN_MODE_LOW_LATENCY仅应在已知设备即将出现(如用户点击“连接”按钮后)时启用,且需配合setReportDelay(0)确保实时回调。

2.3 连接建立:BluetoothGatt的autoConnect参数是连接鲁棒性的开关

BluetoothGatt.connect()的autoConnect参数常被误解为“是否自动重连”。实际上,它的语义是:是否启用系统级的后台连接重试机制。当autoConnect = true时,即使设备当前不可达(如关机、超出范围),BluetoothGatt也会在设备重新进入范围内时自动发起连接,无需 App 主动调用connect()。但代价是:onConnectionStateChange()的state参数首次回调可能为STATE_CONNECTING,且该状态可能持续数秒甚至数十秒,期间无法进行任何 GATT 操作。

官方 Demo 采用autoConnect = false,原因很务实:用户点击“Connect”是明确意图,App 必须对本次连接行为完全可控。此时connect()是同步发起、异步回调,onConnectionStateChange()的state会快速经历STATE_CONNECTING → STATE_CONNECTED或STATE_DISCONNECTED,便于 UI 及时反馈。更重要的是,autoConnect = false下,BluetoothGatt不会缓存连接请求,避免了因多次快速点击“Connect”导致的连接队列堆积。

// BluetoothLeService.java 片段 public boolean connect(final String address) { if (mBluetoothAdapter == null || address == null) { return false; } // 关键:autoConnect = false,确保连接行为即时、可预测 final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); mBluetoothGatt = device.connectGatt(this, false, mGattCallback); return true; }

注意:autoConnect = false并不意味“不重试”。Demo 在mGattCallback的onConnectionStateChange()中,对STATE_DISCONNECTED状态做了手动重连逻辑(见 4.2 节),这是应用层可控的、带退避策略的重试,比系统级autoConnect更灵活。


3. 服务发现与特征值交互:UUID 解析、handle 映射与readCharacteristic()的隐藏前提

发现服务(discoverServices())不是“扫一眼就完事”的动作,而是 BLE 通信中第一个真正的 GATT 协议交互。官方 Demo 将此过程拆解为三步:触发发现 → 等待回调 → 遍历解析。很多开发者卡在onServicesDiscovered()不回调,或回调后getServices()返回空列表,根源往往不在代码,而在对 BLE 协议栈状态的理解偏差。

3.1onServicesDiscovered()不触发的三大元凶:连接状态、MTU 与服务缓存

BluetoothGatt.discoverServices()的执行前提是:GATT 连接已稳定建立,且底层链路层(Link Layer)状态为CONNECTED。但onConnectionStateChange()回调STATE_CONNECTED仅表示 L2CAP 连接成功,并不保证 ATT(Attribute Protocol)层已就绪。此时若立即调用discoverServices(),系统会静默失败,onServicesDiscovered()永远不会触发。

官方 Demo 的应对是:在onConnectionStateChange()收到STATE_CONNECTED后,不立刻 discover,而是先调用requestMtu(512)。这是一个精妙的设计:

  • requestMtu()会触发一次 ATT Exchange MTU Request/Response 交互,强制 ATT 层完成初始化;
  • 该操作有明确回调onMtuChanged(),且只有 ATT 层就绪后才会回调;
  • Demo 将discoverServices()的调用严格放在onMtuChanged()之后,确保服务发现必走 ATT 通道。
// BluetoothLeService.java 片段 private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState == BluetoothProfile.STATE_CONNECTED) { // 连接成功,但 ATT 层尚未就绪,先申请 MTU gatt.requestMtu(512); // 触发 ATT 初始化 } } @Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { // ATT 层确认就绪,此时才安全调用 discoverServices() gatt.discoverServices(); } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { // 真正的服务发现完成,开始解析 List<BluetoothGattService> services = gatt.getServices(); for (BluetoothGattService service : services) { Log.d(TAG, "Found service: " + service.getUuid().toString()); // ... 解析 Characteristic } } } };

参数说明:requestMtu(512)中的 512 是请求的 ATT MTU 值(单位:字节)。BLE 默认 MTU 为 23 字节,远不足以传输复杂特征值。512 是 Android 设备普遍支持的上限值,能显著提升后续读写效率。注意:onMtuChanged()的mtu参数是协商后的实际值,可能小于请求值(如对方只支持 256),需以该值为准。

3.2 UUID 解析:字符串 vs. 128-bit,以及BluetoothGattService的getUuid()陷阱

Demo 中解析服务 UUID 的代码简洁:

BluetoothGattService service = gatt.getService(UUID.fromString("0000180F-0000-1000-8000-00805F9B34FB"));

但这里藏着两个易错点:

  1. UUID 字符串格式必须严格匹配:UUID.fromString()要求输入为标准 32 位十六进制字符串,带 4 个短横线(-),共 36 字符。若固件文档写的是180F(16-bit UUID),需补全为0000180F-0000-1000-8000-00805F9B34FB。漏掉任意-或大小写错误(如180f)都会抛IllegalArgumentException。

  2. getUuid()返回的是服务定义的 UUID,而非声明的 128-bit 值:BLE 协议中,16-bit UUID(如0x180F)是蓝牙 SIG 官方分配的短码,实际在空中传输时会被扩展为 128-bit 标准 UUID。BluetoothGattService.getUuid()返回的是扩展后的 128-bit UUID 字符串。因此,若你的固件使用自定义 128-bit UUID(如AABBCCDD-EEFF-1122-3344-556677889900),必须用完整字符串匹配,不能截取前 4 字节。

为防错,Demo 在DeviceControlActivity中提供了 UUID 校验工具:

// 工具方法:安全解析 UUID private UUID parseServiceUuid(String uuidStr) { try { return UUID.fromString(uuidStr.trim()); } catch (IllegalArgumentException e) { Log.e(TAG, "Invalid UUID string: " + uuidStr, e); return null; } } // 使用 UUID targetServiceUuid = parseServiceUuid("0000180F-0000-1000-8000-00805F9B34FB"); if (targetServiceUuid != null) { BluetoothGattService service = mBluetoothGatt.getService(targetServiceUuid); if (service != null) { // 服务存在,继续找 Characteristic } }

3.3 特征值读写:readCharacteristic()前必须setCharacteristicNotification()吗?

答案是:读(Read)不需要,写(Write)和通知(Notify/Indicate)需要。这是初学者最高频的误解。

  • readCharacteristic():向设备发送 ATT Read Request,设备回复 Read Response。这是单次请求-响应模型,无需提前配置。
  • writeCharacteristic():同理,发送 Write Request/Command,设备回复 Write Response(若为WRITE_TYPE_DEFAULT)。
  • setCharacteristicNotification():这是为设备端的 Notify/Indicate 功能“开闸”。它向设备的 Client Characteristic Configuration Descriptor (CCCD) 写入0x0001(Notify)或0x0002(Indicate),告诉设备:“请在我订阅后,主动推送数据”。此操作本身是writeDescriptor(),必须在readCharacteristic()或writeCharacteristic()之前完成,否则设备不会发送通知。

官方 Demo 在displayGattAttributes()中,对每个BluetoothGattCharacteristic检查其属性(properties),并根据属性决定是否启用通知:

// BluetoothLeService.java 片段 private void displayGattAttributes(List<BluetoothGattService> gattServices) { for (BluetoothGattService service : gattServices) { for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { int prop = characteristic.getProperties(); if ((prop & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0) { // 该特征值支持 Notify,需先启用通知 mBluetoothGatt.setCharacteristicNotification(characteristic, true); // 查找 CCCD descriptor 并写入 0x0001 BluetoothGattDescriptor descriptor = characteristic .getDescriptor(UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")); if (descriptor != null) { descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); mBluetoothGatt.writeDescriptor(descriptor); } } } } }

注意:BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE是byte[]{0x01, 0x00},ENABLE_INDICATION_VALUE是byte[]{0x02, 0x00}。务必按小端序(Little-Endian)写入,否则设备无法识别。


4. 避坑指南:五个真实发生过的 BLE 连接翻车现场与血泪修复方案

调试 BLE 连接时,90% 的问题不是代码写错,而是对 Android 系统行为、BLE 协议细节或硬件能力边界的误判。以下是基于多个真实项目(模拟项目X、某跨平台系统)总结的五大高频坑,每一条都附带现象、根因与可立即落地的修复方案。

4.1 现象:onScanResult()回调频繁,但onConnectionStateChange()永远卡在STATE_CONNECTING

原因:Android 9.0(Pie)起,系统对后台应用的 BLE 扫描和连接施加了严格限制。若 App 处于后台(Activity 不可见、Service 未前台化),connect()调用会被系统静默丢弃,onConnectionStateChange()不会触发任何回调,logcat 也无错误日志。

解决:

  • 前台化 Service:在BluetoothLeService.onCreate()中调用startForeground(NOTIFICATION_ID, notification),确保 Service 有前台优先级;
  • 检查 App 状态:在connect()前,用ActivityManager.getRunningAppProcesses()判断当前 App 是否在前台(importance == IMPORTANCE_FOREGROUND),若非前台,提示用户“请保持屏幕亮起并打开本应用”;
  • 降级策略:对 Android 9+,若检测到后台,改用PendingIntent注册BroadcastReceiver监听BluetoothDevice.ACTION_FOUND,待用户切回前台后再连接。

4.2 现象:onServicesDiscovered()回调status = GATT_FAILURE,logcat 显示D/BluetoothGatt: discoverServices() - device: XX:XX:XX:XX:XX:XX

原因:设备端 GATT Server 未正确实现Discover All Primary Services请求,或服务数量超过 Android 系统默认缓存上限(通常为 16 个服务)。部分低成本 BLE 芯片固件存在此缺陷。

解决:

  • 强制刷新缓存:在onConnectionStateChange()收到STATE_CONNECTED后,discoverServices()前,先调用mBluetoothGatt.refresh()(反射调用,需@SuppressLint("PrivateApi")):
    try { Method refresh = BluetoothGatt.class.getMethod("refresh"); refresh.invoke(mBluetoothGatt); } catch (Exception e) { Log.w(TAG, "Failed to refresh GATT", e); }
  • 分批发现:若服务过多,改用discoverServices()后,用BluetoothGattService.getType()区分PRIMARY/SECONDARY,再对关键服务单独调用service.discoverCharacteristics()。

4.3 现象:readCharacteristic()成功返回GATT_SUCCESS,但onCharacteristicRead()的value字节数组为空或长度异常

原因:特征值(Characteristic)的PROPERTY_READ属性虽被声明,但设备端未在 ATT Read Request 到来时,正确填充value字段。常见于固件未处理ATT_OP_READ_REQ或value缓冲区未初始化。

解决:

  • 增加读取重试与超时:在onCharacteristicRead()中,若characteristic.getValue().length == 0,记录日志并启动重试(最多 3 次,间隔 200ms);
  • 验证特征值长度:在displayGattAttributes()中,对每个BluetoothGattCharacteristic调用characteristic.getValue(),若为 null,说明设备未提供初始值,需在onCharacteristicRead()中等待首次有效读取;
  • 固件侧检查:确认固件中该特征值的attr_read_callback函数是否返回了有效数据指针及长度。

4.4 现象:setCharacteristicNotification(true)后,onCharacteristicChanged()从不触发,但writeDescriptor()返回GATT_SUCCESS

原因:writeDescriptor()成功仅表示 CCCD 写入成功,不代表设备已启用 Notify。设备端需在收到 CCCD 写入后,主动调用ble_gatts_notification()或类似 API 启动推送。若固件未实现此逻辑,通知永远不发。

解决:

  • 双向确认:在writeDescriptor()后,立即readCharacteristic()该特征值,若返回值包含预期数据,证明 Notify 已生效;否则,需固件升级;
  • 添加握手特征值:在设备端定义一个专用“握手”特征值,App 写入0x01后,设备回复0x02,以此确认 Notify 通道已通。

4.5 现象:App 连接一台设备后,再 scan 其他设备,新设备onScanResult()不回调

原因:BluetoothLeScanner的startScan()在已有BluetoothGatt连接时,会与连接共用同一扫描资源。若连接设备广播间隔长(如 2s),系统可能将扫描窗口全部分配给该连接,导致新设备广播被忽略。

解决:

  • 扫描与连接分离:使用独立的BluetoothLeScanner实例进行扫描,连接时用BluetoothDevice.connectGatt()创建新BluetoothGatt,避免资源争抢;
  • 动态调整扫描参数:在onConnectionStateChange()收到STATE_CONNECTED后,将扫描模式从SCAN_MODE_BALANCED切换为SCAN_MODE_LOW_POWER,降低对连接的影响;
  • 硬件辅助:若设备支持 Bluetooth 5.0+,启用ScanResult#isConnectable()过滤,只扫描可连接设备,减少无效回调。

5. 进阶技巧:用BluetoothGatt的getConnectedDevices()实现多设备连接状态监控与优雅降级

官方 Demo 默认只管理单设备连接,但在工业传感器网关、智能家居中控等场景,App 需同时连接多个 BLE 设备(如 5 个温湿度节点、2 个门磁)。此时,BluetoothLeService的单例模式会成为瓶颈。一个成熟的做法是:放弃全局单例 Service,转而为每个设备创建独立的BluetoothGatt实例,并用BluetoothAdapter.getConnectedDevices()实时监控连接健康度。这不是 Demo 的原始设计,但它是从 Demo 骨架上自然生长出的、经实战验证的扩展方案。

5.1 多实例BluetoothGatt管理:避免close()误伤其他连接

核心原则:每个设备对应一个BluetoothGatt实例,且close()操作必须精确作用于该实例。官方 Demo 的mBluetoothGatt.close()是全局变量,多设备下会关闭所有连接。改造方案是将BluetoothGatt封装为BleDevice类:

// BleDevice.java public class BleDevice { private final String mAddress; private BluetoothGatt mGatt; private final BluetoothAdapter mAdapter; public BleDevice(String address, BluetoothAdapter adapter) { this.mAddress = address; this.mAdapter = adapter; } public void connect(Context context) { BluetoothDevice device = mAdapter.getRemoteDevice(mAddress); // 关键:每个设备独立的 GattCallback mGatt = device.connectGatt(context, false, new BleGattCallback(this)); } public void close() { if (mGatt != null) { mGatt.close(); // 只关闭本设备 mGatt = null; } } // getter/setter... }

BleGattCallback内部持有BleDevice引用,确保所有回调(onConnectionStateChange,onServicesDiscovered)都能精准路由到对应设备实例。

5.2 连接状态监控:getConnectedDevices()的实时性与权限边界

BluetoothAdapter.getConnectedDevices(BluetoothProfile.GATT)是获取当前所有已连接 GATT 设备的唯一可靠 API。但它有两大限制:

  • 仅返回STATE_CONNECTED设备:STATE_CONNECTING设备不会出现在列表中,需自行维护连接中设备列表;
  • 需要BLUETOOTH_ADMIN权限:Android 12+ 要求BLUETOOTH_CONNECT,且必须在AndroidManifest.xml中声明。

监控逻辑如下:

// ConnectionMonitor.java private void monitorConnections() { // 1. 获取系统认为已连接的设备 List<BluetoothDevice> connected = mAdapter.getConnectedDevices(BluetoothProfile.GATT); // 2. 对比本地管理的设备列表 for (BleDevice device : mManagedDevices) { boolean systemConnected = connected.stream() .anyMatch(d -> d.getAddress().equals(device.getAddress())); if (device.isConnected() && !systemConnected) { // 系统已断开,但本地状态仍为 connected → 强制 close 并触发重连 device.onSystemDisconnect(); } else if (!device.isConnected() && systemConnected) { // 系统已连上,但本地状态未更新 → 同步状态并触发服务发现 device.onSystemConnect(); } } }

提示:getConnectedDevices()调用开销极小,可每 5 秒轮询一次。但不要在主线程调用,应置于HandlerThread或ExecutorService中。

5.3 优雅降级:当getConnectedDevices()返回空时,自动触发全量重连

网络抖动、蓝牙模块临时故障会导致getConnectedDevices()突然返回空列表,此时不应立即报错,而应启动降级策略:

降级级别触发条件行为恢复条件
Level 1(轻度)getConnectedDevices()为空,但mManagedDevices中有connecting状态设备暂停新连接请求,等待 3 秒后重试getConnectedDevices()连续 2 次非空
Level 2(中度)Level 1 失败,且mManagedDevices中有connected状态设备对每个connected设备,调用device.getGatt().discoverServices()强制刷新onServicesDiscovered()成功
Level 3(重度)Level 2 失败,或mManagedDevices全为disconnected清空本地状态,重启BluetoothAdapter(disable()/enable()),然后对所有设备发起connect()所有设备onConnectionStateChange()返回STATE_CONNECTED

此策略让 App 在蓝牙模块异常时,从“崩溃”变为“自愈”,极大提升用户体验。

从那以后我每次设计 BLE 多设备管理模块,都强制走一遍getConnectedDevices()的轮询 + 三级降级验证。它不能解决所有问题,但能把 70% 的“连接莫名消失”归类为可预测、可恢复的系统事件,而不是玄学 Bug。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询