☰
Android BLE官方Demo全解析:从扫描到读写特征值的避坑指南
2026/10/21 21:51:44 网站建设 项目流程

简介:这是一份基于安卓官方低功耗蓝牙示例代码整理的开发资源包,面向需要学习蓝牙BLE连接通信的移动端工程师。资源围绕低功耗蓝牙核心开发流程展开,完整覆盖设备扫描、连接建立、连接断开、服务发现和UUID特征识别等关键环节,既可作为蓝牙开发入门的学习模板,也能作为日常排查接口时序与状态回调的参考代码。压缩包采用rar格式,共包含70个文件,整体体积仅1.34MB,主要文件类型有Java源码、XML布局与配置、PNG图标、JAR依赖库、APK安装包和工程描述文件,其中源码便于移植改造,APK可直接安装验证,XML与PNG用于界面搭建和资源适配。资源发布至今已有525人学习下载,对于想利用官方demo快速理解安卓低功耗蓝牙数据交互和应用层实现的开发者来说,是一份轻量、完整且具有实践参考价值的样例代码。

1. 为什么这套 BLE 官方 Demo 值得按步骤拆一遍

做 Android 蓝牙开发的人,十有八九都被系统蓝牙的“玄学”折磨过:扫描不到设备、连接上了又秒断、UUID 对不上、服务发现失败……市面上能搜到的教程不是太老就是抄来抄去,真正能落地的少。这套 Android 蓝牙 BLE 官方 Demo 的价值在于:它把「扫描 → 连接 → 发现服务 → 读写特征值」这条完整链路用官方标准方式串起来了,不是某个第三方库的封装,而是基于系统 BluetoothGatt 等 API 的原始实现,最适合拿来理解 BLE 的底层机制。

如果你是第一次接触 BLE 开发,或者被各种第三方库的过度封装搞晕了,这套 Demo 能帮你把概念和系统 API 对应起来;如果你已经写过一点 BLE 代码,里面关于连接参数、服务发现时序和回调线程的处理,也值得对照着你自己的实现检查一遍。文章后面我会按实际操作顺序,把每个环节的关键代码、参数含义和最容易踩的坑都过一遍。

2. 扫描与过滤:不是所有设备都该连,先学会找对

2.1 从 BluetoothLeScanner 开始:新版扫描 API 的正确姿势

官方 Demo 里扫描部分用的是BluetoothLeScanner,这是 Android 5.0(API 21)之后的标准扫描入口。写法上最核心的是拿到系统蓝牙适配器,再拿到扫描器实例,然后发起扫描。需要注意的是,从 Android 6.0(API 23)开始,扫描必须要动态申请ACCESS_FINE_LOCATION权限,否则回调里根本收不到任何设备,这个经常有人栽跟头。

private BluetoothLeScanner bluetoothLeScanner; private ScanCallback scanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { BluetoothDevice device = result.getDevice(); String name = device.getName(); String address = device.getAddress(); int rssi = result.getRssi(); // 这里就可以把扫描到的设备加到列表里展示了 } @Override public void onScanFailed(int errorCode) { // 常见 errorCode: SCAN_FAILED_ALREADY_STARTED(1) / // SCAN_FAILED_APPLICATION_REGISTRATION_FAILED(2) / // SCAN_FAILED_INTERNAL_ERROR(3) / SCAN_FAILED_FEATURE_UNSUPPORTED(4) } }; private void startScan() { bluetoothLeScanner = BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner(); bluetoothLeScanner.startScan(callback, new ScanFilter.Builder() .setServiceUuid(new ParcelUuid(UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb"))) .build(), new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build()); }

扫描回调里拿到的ScanResult是整个扫描阶段的核心数据载体,里面包含BluetoothDevice(设备对象)、Rssi(信号强度)、ScanRecord(广播数据)等关键信息。实际使用时,我一般会先把onScanResult里的设备数据缓存到列表里,再用isDeviceFilterMatch之类的逻辑二次过滤,而不是盲目地把所有设备都当成可连接目标。

2.2 ScanFilter 与 ScanSettings:想让扫描结果可控,这两个必须调明白

ScanFilter用来做系统级的预过滤,最常见的用法就是按服务 UUID 过滤——只扫描广播了某个特定 UUID 服务的设备。这里要特别注意一个坑:很多低功耗设备用的是 16 位 UUID(比如180F是电池服务),但代码里构造ParcelUuid时必须写成完整的 128 位格式,也就是0000180f-0000-1000-8000-00805f9b34fb。被这个细节坑过的人不在少数。

ScanSettings则控制扫描的功耗和延迟。SCAN_MODE_LOW_LATENCY延迟最低(约 100ms 左右),但功耗最高;SCAN_MODE_BALANCED在功耗和延迟之间折中;SCAN_MODE_LOW_POWER最省电,但可能漏掉信号弱的设备。如果接的是需要频繁上报数据的设备(比如传感器),倾向用LOW_LATENCY配合短时扫描;如果只是偶尔发现一下设备,BALANCED就够用了。

参数可选项适用场景
setScanModeLOW_POWER / BALANCED / LOW_LATENCY低功耗后台扫描选 LOW_POWER,主动连接选 LOW_LATENCY
setReportDelay0 或 >0 毫秒设为 0 表示立即上报;设为大于 0 表示批量上报,0 延迟适合列表刷新
setCallbackTypeCALLBACK_TYPE_ALL_MATCHES / FIRST_MATCH / LOST默认 ALL_MATCHES,一般不用改

2.3 扫描停止与回调时机:为什么你经常收不到 onScanResult

扫描超时这个点太容易被忽略了。官方 Demo 里会设置一个固定时间的Handler,扫描时间到了就调stopScan。但有一个细节是很多人没注意的:stopScan的操作要和你发起扫描的方式一一对应——如果你用startScan(List<ScanFilter>, ScanSettings, ScanCallback)发起的,就必须用stopScan(ScanCallback)来停,传入同一个callback实例。因为ScanCallback对象会被当作 key 来匹配扫描会话,实例不一致时stopScan可能直接失败或者根本没效果。

另一个常见现象是:已经在扫描状态下重复调用startScan,此时不会收到任何新的数据回调,也不会报错。从代码健壮性角度,启动扫描前先判断isScanning这样一个标志位,还要在onScanFailed里对SCAN_FAILED_ALREADY_STARTED做处理。

private boolean isScanning = false; private void toggleScan() { if (isScanning) { bluetoothLeScanner.stopScan(scanCallback); isScanning = false; } else { bluetoothLeScanner.startScan(scanCallback); isScanning = true; // 设置扫描超时,比如 10 秒后强制停止并更新状态 handler.postDelayed(() -> { if (isScanning) { bluetoothLeScanner.stopScan(scanCallback); isScanning = false; } }, 10000); } }

从实际项目经验看,扫描这一块还经常遇到的一个问题是:设备明明在广播,但onScanResult迟迟不来。常见原因包括权限没给全、系统蓝牙缓存异常(开关一次蓝牙可解)、或者广播包里没有Flags(LE General Discoverable Mode)导致某些系统版本上设备不会被上报。遇到这类问题,先用BluetoothAdapter.startDiscovery()的日志对比一下,能区分是扫描 API 的问题还是设备广播本身的问题。

3. 建立连接:GATT 连接背后的参数门道

3.1 从 connectGatt 说起:连接凭什么成功、凭什么失败

扫描到设备之后,下一个动作就是通过BluetoothDevice.connectGatt建立 GATT 连接。这个方法的第四参数transport是很容易被忽视的——部分设备同时支持经典蓝牙和低功耗蓝牙(双模设备),如果不指定TRANSPORT_LE,系统可能默认用物理层去和设备的经典蓝牙建立连接,导致连接失败或长时间超时。官方 Demo 里一般会建议显式传TRANSPORT_LE。

BluetoothGatt gatt; boolean autoConnect = false; gatt = device.connectGatt(context, autoConnect, gattCallback, BluetoothDevice.TRANSPORT_LE);

这里autoConnect = false的含义是:只在设备可被发现和连接时进行“直接连接”。如果设成true,系统会在后台持续监听设备广播,等设备重新出现时自动连接(这在需要掉线自动重连的场景很有用),但代价是首次连接等待时间长、且低功耗设备在休眠状态下永远不会主动连上。我的习惯是:首次连接用false更可控,需要重连时再改用true。

连接结果通过onConnectionStateChange回调回来,其中status参数是判断成败的关键:

状态值含义排查方向
0 (GATT_SUCCESS)连接成功可以继续发现服务
8 (GATT_CONN_TIMEOUT)连接超时距离太远、设备不在广播、信号干扰
19 (GATT_CONN_TERMINATE_LOCAL_HOST)本地断开之前主动 disconnect 过,需重新 connect
133 (GATT_ERROR)底层错误常见于系统蓝牙堆栈异常,重启蓝牙或重试
257 / 22 / 34各类连接失败一般和系统状态相关,重试大多能恢复

3.2 连接参数默认值:MTU、连接间隔和延迟对稳定性影响

连接建立后,BluetoothGatt内部会协商一套连接参数,包括连接间隔(connection interval)和从设备延迟(slave latency)。官方 Demo 里不做特别处理,默认参数由系统决定——但如果是做数据量稍微大一点的传输,默认的 MTU(默认值一般是 23 字节)会明显不够用,这时需要主动发起 MTU 协商。

gatt.requestMtu(256);

requestMtu的结果在onMtuChanged回调里拿到,协商成功后的mtu就是双方的实际 MTU 值。注意不是每次请求都能成功拿到 256——实际的 MTU 受设备和系统共同限制,Android 设备上常见协商结果是 247 或 512。协商完 MTU 之后再发数据,一条onCharacteristicWrite就能塞下接近 MTU-3(ATT 头)字节的数据。MTU 协商最好在onConnectionStateChange成功之后、发现服务之前发起,这样后面读写特征值时能一次性走更大的包。

连接间隔这种参数在 Android 上默认不太需要去动,除非你遇到连接不稳定、或者需要低延迟传输的场景,可以在发起连接前通过BluetoothController的接口尝试配置。这块底层接口不同设备实装情况差异很大,不是官方 Demo 覆盖的范围,实际项目中我一般会先用系统默认参数测出数据吞吐量和稳定性,再决定是否需要调整。

3.3 onConnectionStateChange 不要忽略 133 和重连策略

status = 133是我在实际项目里见过最多的一种失败类型。这个状态值的官方解释是GATT_ERROR,意味着底层蓝牙控制器报错,具体原因很杂:从设备信号太弱、蓝牙固件 bug、到系统蓝牙服务崩溃都可能导致。处理方式也很直白——重新连接。

@Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState == BluetoothProfile.STATE_CONNECTED) { // 连接成功,紧接着调用 discoverServices() gatt.discoverServices(); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { // 断开,判断 status 并按策略重连 if (status != BluetoothGatt.GATT_SUCCESS) { reconnectWithBackoff(device); } } }

这里要养成一个习惯:永远不要在onConnectionStateChange里直接做耗时操作,比如去访问数据库或弹 Toast。回调本身运行在系统 Binder 线程,阻塞太久会拖慢整个蓝牙服务的响应。重连逻辑上建议做退避——第一次重连隔 500ms,第二次 1s,再往后 2s,最多重试 3~5 次,避免在信号极差的环境下死循环消耗电量。

4. 发现服务与 UUID 映射:特征值为什么读不到

4.1 discoverServices 与定时器:为什么服务发现会失败

onConnectionStateChange中连接成功之后,紧接着就要调用discoverServices()。这一步的作用是拉取设备 GATT 服务表,把设备支持的所有 Service、Characteristic、Descriptor 的句柄和 UUID 缓存到系统层。服务发现的结果通过onServicesDiscovered回调通知上层。

@Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { BluetoothGattService service = gatt.getService(UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb")); if (service != null) { BluetoothGattCharacteristic characteristic = service.getCharacteristic( UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb")); // 拿到 characteristic 后,就可以进行 read / write / notify 操作 } } }

服务发现失败最常见的一种现象是:连接成功但onServicesDiscovered回调迟迟不执行,或者干脆status != GATT_SUCCESS。究其原因,往往是设备端 GATT 表结构异常、无线信号干扰导致 ATT 请求超时、或者部分设备只支持 GATT over BR/EDR 而 Android 侧按 LE 方式发现。这种情况下我一般是加一个超时兜底:在调用discoverServices的同时启动一个 5 秒的 Handler 定时器,超时没收到回调就断开重连,再不行换白名单方式连接。

4.2 UUID 的 16 位转 128 位:这个基础转换绕晕过太多人

BLE 世界里规定了两种 UUID 格式:完整的 128 位 UUID(00002a19-0000-1000-8000-00805f9b34fb)和短 16 位 UUID(0x2A19)。蓝牙 SIG 定义的标准属性都拥有对应的短 UUID,其中0x2A19是电池电量(Battery Level),0x180F是电池服务(Battery Service),0x180A是设备信息服务(Device Information Service)。

问题在于:设备广播包里出现的 UUID 往往以短格式存储(占 2 个字节),而 Android API 里查询BluetoothGattService和BluetoothGattCharacteristic时用的是UUID对象,两者必须精确匹配。很多设备厂商的文档只写"Service UUID: 180F",开发者随手就UUID.fromString("180f"),结果永远getService()返回 null。正确的写法是补全成 128 位标准格式:

private static final UUID BATTERY_SERVICE_UUID = UUID.fromString("0000180f-0000-1000-8000-00805f9b34fb"); private static final UUID BATTERY_LEVEL_CHAR_UUID = UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb");

如果设备是厂商自定义的服务,UUID 通常本来就是完整的 128 位(例如0000ffe0-0000-1000-8000-00805f9b34fb),配置时直接照抄即可。注意一个盲点:低功耗蓝牙设备上经常出现服务没有 UUID,只靠广播服务数据(Service Data)来区分的情况。这种时候只能在扫描解析ScanRecord时通过厂商自定义字段做识别,GATT 服务表里并不存在对应的 Service UUID。工具链里处理这类设备要单独走一套分支逻辑,别硬套官方 Demo 的模式。

4.3 从服务表里精确取特征值:getService 与 getCharacteristic 的边界

拿到BluetoothGattService之后,一个容易踩的边界是:同一种 Service UUID 的设备,特征值列表可能完全不同。比如同为180F(电池服务),有些设备额外实现了2A1C(Test Function),有的没有。所以代码里取特征值时不能假设它一定存在,要先判空。

BluetoothGattCharacteristic batteryLevel = service.getCharacteristic(UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb")); if (batteryLevel != null) { // 支持 read 和 notify 两种情况还需要分别判断 boolean isReadable = (batteryLevel.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) != 0; boolean isNotifiable = (batteryLevel.getProperties() & BluetoothGattCharacteristic.PROPERTY_NOTIFY) != 0; // 按需调用 readCharacteristic / setCharacteristicNotification }

特征值的PERMISSION和PROPERTY是两个完全不同的概念:PERMISSION表示这个特征值是否允许被读、写、通知,是设备端声明的能力;PROPERTY表示应用层可以对这个特征值执行哪些操作。判断能不能notify,要看的是PROPERTY_NOTIFY而不是PERMISSION。这个细节如果你直接抄 Demo 里的连接和收发代码,很容易漏掉。

5. 数据收发与通知回调:读写特征值的完整链路

5.1 写特征值:带不带 response 的两种方式怎么选

写入特征值有两种方式:WRITE_TYPE_DEFAULT(带响应的写,需要设备确认)和WRITE_TYPE_NO_RESPONSE(无响应的写,适合数据量大、实时性要求高的场景)。在官方 Demo 里默认是WRITE_TYPE_DEFAULT,因为大多数控制类数据都需要知道设备是否成功接收。

BluetoothGattCharacteristic characteristic = ...; byte[] value = new byte[]{(byte) 0x01, (byte) 0xA0}; characteristic.setValue(value); characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); boolean success = gatt.writeCharacteristic(characteristic);

写操作会触发onCharacteristicWrite回调:

@Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { // 写入成功,可以继续下一个指令 } else { // 写入失败,常见的 status 有 2 (GATT_READ_NOT_PERMITTED)、5 (GATT_INSUFFICIENT_AUTHENTICATION) } }

选择WRITE_TYPE_NO_RESPONSE时要注意:这个方式下 Android 不会等待设备的 ATT 层确认,所以onCharacteristicWrite只能说明数据已经进入系统蓝牙发送队列,并不代表设备真正收到了。如果业务需要可靠传输(比如配置传感器参数),建议都用默认写。只有连续不断地推流媒体数据(比如正在传输音频帧)时才考虑 NO_RESPONSE 配合大 MTU 使用。

5.2 通知机制:onCharacteristicChanged 与 setCharacteristicNotification 的正确组合

读取特征值通常有两种方式:主动读(readCharacteristic)和被动收通知(setCharacteristicNotification)。被动通知是 BLE 设备主动向手机推送数据的主要方式,比如心率传感器、温湿度计都走这个机制。

private void enableNotification(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { boolean registered = gatt.setCharacteristicNotification(characteristic, true); // 关键一步:还要往 0x2902(客户端特征配置描述符)里写入 0x01 才能开启通知 BluetoothGattDescriptor descriptor = characteristic.getDescriptor( UUID.fromString("00002902-0000-1000-8000-00805f9b34fb")); if (descriptor != null) { descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(descriptor); } }

这里有一个大多数新手都会翻车的点:只调setCharacteristicNotification(true)是收不到任何通知的。原因在于 0x2902 描述符(CCCD)的机制——设备端是否发送数据,取决于手机有没有在 CCCD 里写入启用通知的值。setCharacteristicNotification只是让本地系统开始监听,真正让设备开始推数据的是后面这个writeDescriptor操作。反之,停用通知时也要记得把描述符值写成ENABLE_NOTIFICATION_VALUE之外的 0x00。

通知数据到达时会触发onCharacteristicChanged,这个回调里取出的就是最新的特征值数据:

@Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { byte[] data = characteristic.getValue(); // 按协议解析数据,比如第一个字节是电量百分比 int batteryLevel = data[0] & 0xFF; }

注意这里的getValue()只在回调触发的瞬间有效——如果在这个方法里把data保存到别处,后面再想引用characteristic.getValue()拿到的可能已经是新的数据了。因此立刻拷贝一份是很必要的操作。

5.3 多个写入指令排队:onCharacteristicWrite 没回来前别发下一条

实际和 BLE 设备打交道的过程中,最容易出问题的不是单条写的实现,而是连续写入多条指令时的顺序问题。很多 BLE 设备要求初始化流程是一步一步来的,比如先配置上报周期、再开启某个功能、最后启动测量,指令之间还有明确的先后依赖关系。如果一股脑地把三条写请求连续发出去,设备端往往只会执行第一条,后面的全部丢掉。

private Queue<byte[]> commandQueue = new LinkedList<>(); private boolean isWriting = false; private void sendNextCommand() { if (isWriting || commandQueue.isEmpty()) { return; } isWriting = true; BluetoothGattCharacteristic characteristic = ...; characteristic.setValue(commandQueue.poll()); gatt.writeCharacteristic(characteristic); } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { isWriting = false; sendNextCommand(); // 上一个写完,再发下一个 }

这种写队列的实现原则是:writeCharacteristic操作不能并发,每次只有在前一次onCharacteristicWrite回调触发之后才能发起新的写。如果设备连接失败或者写超时,还要加一个超时清理队列的逻辑,避免队列一直堆积导致内存异常。

6. 避坑锦囊:连接、读写和协议解析的常见问题

6.1 连接失败或反复掉线:现象、原因与解决

现象:某些设备在 Android 上连接成功 3~5 秒后自动断开,onConnectionStateChange里报status != GATT_SUCCESS(常见 8 或 133);或连接时一直停在STATE_CONNECTING超时。

硬件连接反复掉线,原因通常有以下几种:

  1. 连接参数协商失败。部分设备对连接间隔和从设备延迟有严格要求,Android 发起的默认连接参数与设备不匹配,设备端会直接断链。解决方法是先抓底层日志确认设备期望的参数区间,再尝试在手机侧用BluetoothGatt.connect相关参数调整。
  2. 信号衰减或遮蔽。把设备放在金属桌面上或者机身被手完全握住,都可能导致 RSSI 急剧下降。解决方式是先贴近设备做一次静置测试,排除信号因素,再谈别的。
  3. 系统蓝牙缓存污染。Android 系统会对已连接设备的 GATT 服务做缓存,如果设备端固件升级后服务表发生变化,缓存不更新就会导致连接失败或服务发现异常。解决方式是清除系统蓝牙缓存(Settings > Bluetooth > 清除配对记录),代码层面可以在连接前调用BluetoothDevice的createBond/removeBond逻辑做缓存重建。
  4. 设备端固件只支持单连接。低功耗设备上常见这种限制,手机侧连接成功但设备端固件超时就主动断开,让出连接资源。解决方式只能是等几秒再重连,或升级设备固件。

6.2 收不到通知:现象、原因与解决

现象:setCharacteristicNotification(true)已调用,onCharacteristicChanged就是不触发;或者偶尔触发一下、很快就不再继续。

具体原因要看一步排查一步:

  1. 没有在 CCCD(0x2902)里写启用值。前面代码段里强调过,只调setCharacteristicNotification是不够的。先用本地变量确认writeDescriptor是否在之前已经执行成功(看onDescriptorWrite回调的 status)。
  2. 设备端通知类型和手机端不匹配。通知有两种类型:Indicate(带确认)和Notify(不带确认)。部分设备只实现了其中一种。0x2902 描述符里写入ENABLE_INDICATION_VALUE(0x02)才对应 Indicate,写入ENABLE_NOTIFICATION_VALUE(0x01)对应 Notify。如果设备用 Indicate,你只写 0x01 是开不了通知的。
  3. MTU 协商失败导致数据分包异常。有些设备不支持大 MTU,系统默认 23 字节而一次通知的负载超过这个大小,数据会被截断或丢弃。解决方式是在连接阶段先把 MTU 请求降到 64 或 100,或者直接不发 MTU 请求,保证使用默认值再测试通知是否正常。
  4. 主服务 / 从服务的概念别搞混。这里说的主服务(Primary Service)、次服务(Secondary Service)是指 GATT 层级里的定义,不是指设备角色。通知是否启用和这个没有直接关系,但如果你在getServices时拿到的服务不是 primary 导致取不到对应特征值,还是要检查服务类型。

6.3 数据解析经验:大小端、符号与字节对齐

现象:通知数据收到了,但解析出来数值不对,比如电量显示成 256、温度显示负数、或者把两个字节的数据读反了。

BLE 协议默认传输大端序(Big Endian),即高字节在前;但不少厂商自定义协议里用的是小端。以温度和传感器数据为例:

// 大端解析:字节0是高字节,字节1是低字节 int temp = ((data[0] & 0xFF) << 8) | (data[1] & 0xFF); // 小端解析:字节0是低字节,字节1是高字节 int temp = ((data[1] & 0xFF) << 8) | (data[0] & 0xFF);

一个有符号数的问题也常踩:用data[0]直接转 int 时,如果字节最高位是 1,会被当作负数。而data[0] & 0xFF才能正确得到 0~255 的无符号值。解析多字节时先统一转无符号再组合,可以避免绝大多数符号扩展的坑。

协处理器上常见的还有字节对齐问题:某些厂商协议里 16 位数据并不是按字节对齐存储的(比如 12 位温湿度塞进 3 个字节)。这种就只能按厂商手册逐位拆,没有通用解法,唯一建议是先把设备和厂商协议看清楚再动手解析。

6.4 一写就断:现象、原因与解决

现象:每次writeCharacteristic一调用,设备立刻断链,或者onCharacteristicWrite报GATT_INSUFFICIENT_AUTHENTICATION(5)或GATT_INVALID_ATTRIBUTE_LENGTH(22)。

常见原因是:

  1. 设备需要配对(Bonding)才能写某些特征值。报 5 基本都是这个情况。处理方式是在连接阶段主动触发createBond,等配对完成后再写;配对过程里系统会弹窗(也可能自动配对),需要确保设备端正确处理了Passkey或Just Works方式的配对请求。
  2. 写的数据超过 MTU-3 字节。报 22 就是这个意思。比如 MTU 是 23 时最多只能写 20 字节,一次写 30 字节必然失败。解决办法是先requestMtu协商大 MTU,或者调用splitPacket把数据分包再逐条发。
  3. 设备端对写操作有时间窗口限制。部分低功耗模块需要特征值写完立刻读回状态,如果写入间隔过长(比如超过几百毫秒)设备会进入假死状态。这个只存在于个别模块上,最直接的排查方法是用厂商自己的调试工具复现一次,排除手机端代码干扰。
错误码含义建议动作
2GATT_READ_NOT_PERMITTED确认特征值属性是否支持读
5GATT_INSUFFICIENT_AUTHENTICATION先配对再操作
22GATT_INVALID_ATTRIBUTE_LENGTH拆分数据包或增大 MTU
133GATT_ERROR重启蓝牙或重新连接
257GATT_CONN_CANCEL之前的连接被取消,需重新 connectGatt

7. 从官方 Demo 到可用工程:连接状态机与日志验证

7.1 引入轻量状态机:STATE_IDLE / CONNECTING / CONNECTED / DISCONNECTING

官方 Demo 的代码是按步骤演示的:扫描列表 → 点击连接 → 收通知。但真实工程里连接是会反复断开的,所以你需要给连接过程建立一个清晰的状态机。状态机的好处是:任何回调进来,先判断当前状态是不是预期值,不是就直接丢弃或报错。大量因为重复调用connectGatt导致的回调错乱,都能靠状态机挡掉。

public enum BleState { IDLE, SCANNING, CONNECTING, CONNECTED, DISCONNECTING }

开始时是IDLE,点击连接切到CONNECTING,onConnectionStateChange收到STATE_CONNECTED切到CONNECTED。断线时如果不是人为主动断开且CONNECTING状态超过 10 秒,触发一次重连而不是直接回到IDLE——这样做既保证了一次业务长连接,又不会因为异常自动重连而死循环。

实际项目里我还会给状态机加一个lastEvent字段,用来记录上一次触发状态的status值和回调时间。这样查看Logcat时能直接定位是哪一次调用的哪个回调把状态切换成异常的,属于最直观的调试方式之一。

7.2 日志打点:连接耗时、服务发现、每次读写都记录下来

调试 BLE 问题最忌讳直觉猜,所有问题都要靠日志说话。建议在关键节点打上统一前缀的日志:

private void logEvent(String event) { Log.d("BleDebug", String.format("[%d] %s", System.currentTimeMillis(), event)); }

几个关键打点位置:发起connectGatt时、onConnectionStateChange收到连接成功时、discoverServices开始时、onServicesDiscovered收到结果时、每次writeCharacteristic/readCharacteristic的前后、每次onCharacteristicChanged数据到达时。日志里往往能直接看到真正的时序问题——比如设备收到写之后触发了onCharacteristicWrite成功,但紧接着的onCharacteristicChanged只来了一条,后面就没了,这时你会知道问题多半出在设备端没有按预期持续上报。

7.3 用 mtu、连接间隔和写队列把 Demo 工程推向可用状态

官方 Demo 给你的是一套标准的 API 调用关系,但落地到一款实际产品上,我会强制自己完成三个改造:

第一,把 MTU 协商放进连接成功后的流程。requestMtu(256)之后等onMtuChanged回调返回再开始读写。这样后面所有的数据帧都能按大包标准设计。

第二,写队列必须做成对外接口,而不是在每个业务场景里直接调gatt.writeCharacteristic。这样能保证所有写入都统一走 FIFO 队列,从根上避免并发写冲突。对外接口签名类似:writeCommand(byte[] data, WriteCallback callback)。

第三,所有超时判断都用Handler做,不用Thread.sleep。Thread.sleep会把 Binder 线程卡死,超时控制也不精确。Handler发延迟消息、在对应回调里移除消息,是更标准的做法。

7.4 用日志验证整条链路是否正常

最后顺一遍我常用的验证步骤:扫描到设备后先看onScanResult里的 RSSI 和广播包;连接成功后看onConnectionStateChange的newState是不是STATE_CONNECTED;服务发现完成后打印服务列表,和设备的给的自定义服务 UUID 对比;读写一条数据,确认onCharacteristicWrite和onCharacteristicChanged的字节顺序和内容都一致。链路里任何一环日志不对,就按第 6 章的对应问题去排查。

从那以后我每次在新设备上调试 BLE,都强制自己走一遍这套流程:先打日志、再清缓存、再排除配对问题,而不是反复改代码碰运气。希望这套从官方 Demo 延伸出来的方法能帮到你,少走几天弯路。

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

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

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

立即咨询