资讯动态

Android BLE官方Demo全解析:从扫描到读写特征值的避坑指南

发布时间:2026/10/21 21:52:01 来源:尧图企业网站定制
简介这是一份基于安卓官方低功耗蓝牙示例代码整理的开发资源包面向需要学习蓝牙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.0API 21之后的标准扫描入口。写法上最核心的是拿到系统蓝牙适配器再拿到扫描器实例然后发起扫描。需要注意的是从 Android 6.0API 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_LATENCYsetReportDelay0 或 0 毫秒设为 0 表示立即上报设为大于 0 表示批量上报0 延迟适合列表刷新setCallbackTypeCALLBACK_TYPE_ALL_MATCHES / FIRST_MATCH / LOST默认 ALL_MATCHES一般不用改2.3 扫描停止与回调时机为什么你经常收不到 onScanResult扫描超时这个点太容易被忽略了。官方 Demo 里会设置一个固定时间的Handler扫描时间到了就调stopScan。但有一个细节是很多人没注意的stopScan的操作要和你发起扫描的方式一一对应——如果你用startScan(ListScanFilter, 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迟迟不来。常见原因包括权限没给全、系统蓝牙缓存异常开关一次蓝牙可解、或者广播包里没有FlagsLE 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 过需重新 connect133 (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-3ATT 头字节的数据。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 位 UUID00002a19-0000-1000-8000-00805f9b34fb和短 16 位 UUID0x2A19。蓝牙 SIG 定义的标准属性都拥有对应的短 UUID其中0x2A19是电池电量Battery Level0x180F是电池服务Battery Service0x180A是设备信息服务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电池服务有些设备额外实现了2A1CTest 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 Queuebyte[] 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超时。硬件连接反复掉线原因通常有以下几种连接参数协商失败。部分设备对连接间隔和从设备延迟有严格要求Android 发起的默认连接参数与设备不匹配设备端会直接断链。解决方法是先抓底层日志确认设备期望的参数区间再尝试在手机侧用BluetoothGatt.connect相关参数调整。信号衰减或遮蔽。把设备放在金属桌面上或者机身被手完全握住都可能导致 RSSI 急剧下降。解决方式是先贴近设备做一次静置测试排除信号因素再谈别的。系统蓝牙缓存污染。Android 系统会对已连接设备的 GATT 服务做缓存如果设备端固件升级后服务表发生变化缓存不更新就会导致连接失败或服务发现异常。解决方式是清除系统蓝牙缓存Settings Bluetooth 清除配对记录代码层面可以在连接前调用BluetoothDevice的createBond/removeBond逻辑做缓存重建。设备端固件只支持单连接。低功耗设备上常见这种限制手机侧连接成功但设备端固件超时就主动断开让出连接资源。解决方式只能是等几秒再重连或升级设备固件。6.2 收不到通知现象、原因与解决现象setCharacteristicNotification(true)已调用onCharacteristicChanged就是不触发或者偶尔触发一下、很快就不再继续。具体原因要看一步排查一步没有在 CCCD0x2902里写启用值。前面代码段里强调过只调setCharacteristicNotification是不够的。先用本地变量确认writeDescriptor是否在之前已经执行成功看onDescriptorWrite回调的 status。设备端通知类型和手机端不匹配。通知有两种类型Indicate带确认和Notify不带确认。部分设备只实现了其中一种。0x2902 描述符里写入ENABLE_INDICATION_VALUE0x02才对应 Indicate写入ENABLE_NOTIFICATION_VALUE0x01对应 Notify。如果设备用 Indicate你只写 0x01 是开不了通知的。MTU 协商失败导致数据分包异常。有些设备不支持大 MTU系统默认 23 字节而一次通知的负载超过这个大小数据会被截断或丢弃。解决方式是在连接阶段先把 MTU 请求降到 64 或 100或者直接不发 MTU 请求保证使用默认值再测试通知是否正常。主服务 / 从服务的概念别搞混。这里说的主服务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_AUTHENTICATION5或GATT_INVALID_ATTRIBUTE_LENGTH22。常见原因是设备需要配对Bonding才能写某些特征值。报 5 基本都是这个情况。处理方式是在连接阶段主动触发createBond等配对完成后再写配对过程里系统会弹窗也可能自动配对需要确保设备端正确处理了Passkey或Just Works方式的配对请求。写的数据超过 MTU-3 字节。报 22 就是这个意思。比如 MTU 是 23 时最多只能写 20 字节一次写 30 字节必然失败。解决办法是先requestMtu协商大 MTU或者调用splitPacket把数据分包再逐条发。设备端对写操作有时间窗口限制。部分低功耗模块需要特征值写完立刻读回状态如果写入间隔过长比如超过几百毫秒设备会进入假死状态。这个只存在于个别模块上最直接的排查方法是用厂商自己的调试工具复现一次排除手机端代码干扰。错误码含义建议动作2GATT_READ_NOT_PERMITTED确认特征值属性是否支持读5GATT_INSUFFICIENT_AUTHENTICATION先配对再操作22GATT_INVALID_ATTRIBUTE_LENGTH拆分数据包或增大 MTU133GATT_ERROR重启蓝牙或重新连接257GATT_CONN_CANCEL之前的连接被取消需重新 connectGatt7. 从官方 Demo 到可用工程连接状态机与日志验证7.1 引入轻量状态机STATE_IDLE / CONNECTING / CONNECTED / DISCONNECTING官方 Demo 的代码是按步骤演示的扫描列表 → 点击连接 → 收通知。但真实工程里连接是会反复断开的所以你需要给连接过程建立一个清晰的状态机。状态机的好处是任何回调进来先判断当前状态是不是预期值不是就直接丢弃或报错。大量因为重复调用connectGatt导致的回调错乱都能靠状态机挡掉。public enum BleState { IDLE, SCANNING, CONNECTING, CONNECTED, DISCONNECTING }开始时是IDLE点击连接切到CONNECTINGonConnectionStateChange收到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 延伸出来的方法能帮到你少走几天弯路。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑