ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android BLE主控端开发:状态机驱动的连接与GATT通信实战

Android BLE主控端开发:状态机驱动的连接与GATT通信实战 简介本资源是一个面向Android开发者特别是物联网与智能硬件交互方向初学者的蓝牙低功耗BLE开发实践项目聚焦Android 4.3系统下蓝牙4.0协议栈的完整调用流程。项目涵盖设备扫描、配对、GATT连接、服务发现、特征值读写及通知订阅等核心环节代码结构清晰适合作为BLE通信原理学习与工程落地的入门参考。压缩包共45个文件包含19个XML布局与配置文件、6个Java核心逻辑类如BluetoothGattCallback实现、DeviceScanner等、5张UI图标PNG以及Gradle构建脚本、README说明文档和Git相关配置文件整体仅116KB轻量易导入。目前已有467人学习下载代码组织规范模块职责分明——如app模块封装连接管理CardioChek示例模拟真实健康设备交互便于开发者快速理解GATT通信时序与异常处理机制。1. Android BLE 主控端开发不是配对而是状态机驱动的连接与数据流控制很多刚接触 Android 蓝牙 4.0BLE开发的人第一反应是“怎么配对”——但 BLE 在 Android 上根本不需要传统蓝牙那种 PIN 码配对。真正卡住开发进度的是BluetoothAdapter、BluetoothDevice、BluetoothGatt三者之间严格的生命周期约束以及onConnectionStateChange()、onServicesDiscovered()、onCharacteristicRead()这些回调触发的隐式状态跃迁。一个典型失败场景是设备已扫描到、connectGatt()返回非 null GATT 实例但后续所有readCharacteristic()都无响应——问题往往出在autoConnect false时未等待STATE_CONNECTED就发读请求或discoverServices()未完成就调用getCharacteristic()。本文聚焦Android-ble-master.zip所代表的典型主控Central端工程结构拆解从初始化到稳定通信的完整链路不依赖第三方 SDK只用 Android 原生android.bluetooth.le和android.bluetooth包覆盖 Android 6.0API 23到 Android 14API 34的权限适配、后台扫描限制、GATT 操作队列阻塞等真实坑点。适合正在调试心率手环、温湿度传感器、电子标签等 BLE 外设的 Android 开发者尤其当你发现 Logcat 里反复出现D/BluetoothGatt: onClientConnectionState() - status133 clientIf7 deviceXX:XX:XX:XX:XX:XX却不知所措时这篇就是为你写的。2. 从 BluetoothAdapter 初始化到扫描启动权限、兼容性与扫描参数的硬性约束2.1 权限声明与运行时校验必须分两步走缺一不可Android 12API 31起BLE 扫描被划入BLUETOOTH_SCAN特权权限且需在AndroidManifest.xml中显式声明android:usesPermissionFlagsneverForLocation。但仅声明不够必须在代码中动态申请!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.BLUETOOTH / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION / uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT /提示ACCESS_FINE_LOCATION是 Android 10API 29及以下版本扫描必需项Android 11 可选ACCESS_COARSE_LOCATION但为兼容性建议保留FINE。BLUETOOTH_CONNECT在 Android 12 必须声明否则connectGatt()直接抛SecurityException。运行时校验逻辑需严格按 API 分层// Java private void checkAndRequestPermissions() { if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { // Android 12 使用新权限组 if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) ! PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT}, REQUEST_CODE_PERMISSIONS); } else { startScan(); } } else { // Android 11 及以下 if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE_PERMISSIONS); } else { startScan(); } } }2.2 扫描过滤器ScanFilter和参数ScanSettings决定能否发现目标设备Android-ble-master.zip中常见错误是直接new ScanCallback()后调用startScan(null, null, callback)——这会扫描所有广播包但大量低功耗设备如 iBeacon、Eddystone使用自定义 AD 结构必须用ScanFilter精确匹配。例如若目标设备广播名固定为TempSensor-001则// 构建精确匹配的 ScanFilter ScanFilter.Builder filterBuilder new ScanFilter.Builder(); filterBuilder.setDeviceName(TempSensor-001); // 按设备名过滤 // 或按服务 UUID 过滤更可靠 // filterBuilder.setServiceUuid(ParcelUuid.fromString(00001809-0000-1000-8000-00805f9b34fb)); // Battery Service ListScanFilter filters Arrays.asList(filterBuilder.build()); // 设置扫描参数平衡功耗与发现速度 ScanSettings settings new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 高频扫描适合调试 .setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE) // 加速匹配 .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) .build(); // 启动扫描 mBluetoothLeScanner.startScan(filters, settings, scanCallback);2.2.1 SCAN_MODE 的实际影响与选型依据Scan Mode扫描间隔功耗等级适用场景SCAN_MODE_LOW_POWER~10s 一次★☆☆☆☆后台长期监听如信标定位SCAN_MODE_BALANCED~2s 一次★★☆☆☆App 前台常规扫描SCAN_MODE_LOW_LATENCY~100ms 一次★★★★☆调试阶段快速发现设备注意SCAN_MODE_LOW_LATENCY在 Android 12 受SCAN_ALWAYS_AVAILABLE限制需在AndroidManifest.xml中声明uses-feature android:nameandroid.hardware.bluetooth_le android:requiredtrue/否则部分 OEM 设备如华为、小米会静默降级为BALANCED模式。2.3 ScanCallback 回调中的状态校验与设备去重原始ScanResult仅含 RSSI、广告数据ScanResult.getScanRecord().getBytes()但Android-ble-master.zip常见 bug 是直接将result.getDevice()存入列表导致同一设备因多次广播被重复添加。正确做法是用device.getAddress()作为唯一键private final ScanCallback scanCallback new ScanCallback() { Override public void onScanResult(int callbackType, ScanResult result) { BluetoothDevice device result.getDevice(); String address device.getAddress(); // MAC 地址是唯一标识 if (!scannedDevices.containsKey(address)) { scannedDevices.put(address, device); // 解析广告数据提取设备名、服务 UUID 等 byte[] advData result.getScanRecord() ! null ? result.getScanRecord().getBytes() : new byte[0]; parseAdvertisementData(advData); } } Override public void onBatchScanResults(ListScanResult results) { // 批量处理减少主线程压力 for (ScanResult result : results) { onScanResult(ScanCallback.CALLBACK_TYPE_FIRST_MATCH, result); } } Override public void onScanFailed(int errorCode) { Log.e(BLE, Scan failed with code: errorCode); // errorCode2 表示硬件忙errorCode3 表示参数非法需重试或提示用户重启蓝牙 } };2.3.1 广告数据AD Structure解析的关键字段提取BLE 广播包由多个 AD 结构Advertising Data Structure拼接而成每个结构含Length1字节、AD Type1字节、AD Data变长。常用类型AD Type (Hex)含义提取方式0x08Shortened Local NameparseAdStructure(data, 0x08)0x09Complete Local NameparseAdStructure(data, 0x09)0x02Flagsdata[2] 0x04判断是否支持 LE General Discoverable0x16Service Data (16-bit UUID)UUID.fromString(String.format(%04x, bytesToShort(data, 2)) -0000-1000-8000-00805f9b34fb)private String parseAdStructure(byte[] data, int type) { int pos 0; while (pos data.length) { int length data[pos] 0xFF; if (length 0) break; int adType data[pos 1] 0xFF; if (adType type length 2) { byte[] value new byte[length - 1]; System.arraycopy(data, pos 2, value, 0, length - 1); return new String(value, StandardCharsets.UTF_8); } pos length 1; } return null; }3. GATT 连接与服务发现状态机驱动的异步操作队列管理3.1 connectGatt() 的 autoConnect 参数决定连接行为本质BluetoothDevice.connectGatt(Context context, boolean autoConnect, BluetoothGattCallback callback)中autoConnect是核心开关autoConnect false主动连接立即发起连接请求适用于已知设备需快速交互的场景如点击列表项后连接。此时onConnectionStateChange()的state参数为BluetoothProfile.STATE_CONNECTED时才可进行下一步。autoConnect true后台连接系统在设备进入范围时自动连接适用于需要持续监听的设备如智能门锁。但 Android 7.0 对后台连接有严格限制onConnectionStateChange()可能延迟数秒甚至失败。// 主动连接示例 mBluetoothGatt device.connectGatt(this, false, gattCallback); // 此时 mBluetoothGatt 不为空但尚未连接成功不能调用任何 GATT 操作提示connectGatt()返回null仅表示蓝牙适配器不可用或设备地址非法返回非 null 但后续无回调大概率是权限未授予或autoConnecttrue时设备未在范围内。3.2 BluetoothGattCallback 的四大核心回调及其触发条件GATT 操作完全异步所有结果通过BluetoothGattCallback回调。Android-ble-master.zip中最易忽略的是回调触发顺序的强约束回调方法触发条件关键约束onConnectionStateChange()连接建立/断开必须在此回调中state STATE_CONNECTED后才能调用discoverServices()onServicesDiscovered()服务发现完成必须在此回调中调用gatt.getService(uuid)否则返回nullonCharacteristicRead()特征值读取完成必须在readCharacteristic()调用后触发且characteristic.getValue()才是有效数据onCharacteristicWrite()特征值写入完成写入成功后外设才会执行动作如LED亮起需等待此回调确认private final BluetoothGattCallback gattCallback new BluetoothGattCallback() { Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState BluetoothProfile.STATE_CONNECTED) { Log.i(BLE, Connected to gatt.getDevice().getName()); // ✅ 必须在此处发起服务发现 gatt.discoverServices(); } else if (newState BluetoothProfile.STATE_DISCONNECTED) { Log.i(BLE, Disconnected from gatt.getDevice().getName()); } } Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status BluetoothGatt.GATT_SUCCESS) { Log.i(BLE, Services discovered); // ✅ 必须在此处获取服务和特征值 BluetoothGattService service gatt.getService(UUID.fromString(00001809-0000-1000-8000-00805f9b34fb)); if (service ! null) { BluetoothGattCharacteristic characteristic service.getCharacteristic( UUID.fromString(00002a19-0000-1000-8000-00805f9b34fb)); // Battery Level if (characteristic ! null) { // ✅ 此时才能安全读取 gatt.readCharacteristic(characteristic); } } } } Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status BluetoothGatt.GATT_SUCCESS) { byte[] value characteristic.getValue(); int batteryLevel value[0] 0xFF; // 单字节电池电量 Log.i(BLE, Battery level: batteryLevel %); } } };3.3 GATT 操作队列阻塞为什么连续 read/write 会失败Android 系统对单个BluetoothGatt实例的 GATT 操作实行串行队列管理。若在onCharacteristicRead()中立即调用writeCharacteristic()而前一个操作如readCharacteristic()尚未完成新操作会被丢弃并返回GATT_FAILURE。Android-ble-master.zip的典型修复方案是引入操作队列private final QueueRunnable gattOperationQueue new ConcurrentLinkedQueue(); private boolean isOperationPending false; private void enqueueGattOperation(Runnable operation) { gattOperationQueue.offer(operation); if (!isOperationPending) { executeNextOperation(); } } private void executeNextOperation() { if (gattOperationQueue.isEmpty()) { isOperationPending false; return; } isOperationPending true; Runnable op gattOperationQueue.poll(); if (op ! null) { op.run(); } } // 在 onCharacteristicRead() 中 Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status BluetoothGatt.GATT_SUCCESS) { // 处理读取数据... // ✅ 排队写入操作避免队列阻塞 enqueueGattOperation(() - { characteristic.setValue(new byte[]{0x01}); gatt.writeCharacteristic(characteristic); }); } }4. 特征值读写与通知启用Descriptor 操作是开启通知的必要步骤4.1 读写特征值前必须确认属性Properties与权限Permissions并非所有特征值都可读写。BluetoothGattCharacteristic的getProperties()返回位掩码需校验PROPERTY_READ支持读取 → 可调用readCharacteristic()PROPERTY_WRITE支持写入 → 可调用writeCharacteristic()PROPERTY_NOTIFY支持通知 → 需先启用CLIENT_CHARACTERISTIC_CONFIGDescriptor// 获取特征值后检查属性 if ((characteristic.getProperties() BluetoothGattCharacteristic.PROPERTY_READ) ! 0) { gatt.readCharacteristic(characteristic); } if ((characteristic.getProperties() BluetoothGattCharacteristic.PROPERTY_WRITE) ! 0) { characteristic.setValue(CMD.getBytes()); gatt.writeCharacteristic(characteristic); } if ((characteristic.getProperties() BluetoothGattCharacteristic.PROPERTY_NOTIFY) ! 0) { // ✅ 必须先设置通知再调用 setCharacteristicNotification enableNotification(gatt, characteristic); }4.2 启用通知的三步法Descriptor 写入是关键启用通知不是简单调用setCharacteristicNotification()而是必须向00002902-0000-1000-8000-00805f9b34fbClient Characteristic ConfigurationDescriptor 写入特定值Descriptor Value含义0x0000禁用通知/指示0x0001启用通知Notify0x0002启用指示Indicateprivate void enableNotification(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { // Step 1: 启用本地通知 gatt.setCharacteristicNotification(characteristic, true); // Step 2: 获取 CCCD Descriptor BluetoothGattDescriptor descriptor characteristic.getDescriptor( UUID.fromString(00002902-0000-1000-8000-00805f9b34fb)); // Step 3: 写入 0x0001 启用 Notify if (descriptor ! null) { descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(descriptor); } } // 对应的 Descriptor 写入回调 Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { if (status BluetoothGatt.GATT_SUCCESS) { Log.i(BLE, Notification enabled for descriptor.getCharacteristic().getUuid()); } }4.3 通知数据接收onCharacteristicChanged() 的线程与数据解析当外设发送通知时onCharacteristicChanged()在Bluetooth Handler 线程非主线程触发需注意 UI 更新必须切回主线程Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { byte[] value characteristic.getValue(); // 解析数据例如温度传感器返回 2 字节整数大端序 if (value.length 2) { int tempRaw (value[0] 0xFF) 8 | (value[1] 0xFF); float temperature tempRaw / 100.0f; // 假设单位为 0.01°C runOnUiThread(() - { temperatureTextView.setText(String.format(%.2f°C, temperature)); }); } }5. Android 10 后台扫描与连接限制的绕过策略与合规实践5.1 后台位置权限变更导致的扫描失效Foreground Service 是唯一合规解Android 10API 29起后台应用无法获取ACCESS_FINE_LOCATION导致startScan()无设备返回。官方要求必须将扫描逻辑置于前台服务Foreground Service中并展示持续通知。// 启动前台服务 Intent serviceIntent new Intent(this, BleScanService.class); if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { startForegroundService(serviceIntent); } else { startService(serviceIntent); } // BleScanService.java 中 Override public int onStartCommand(Intent intent, int flags, int startId) { // 创建 Notification ChannelAndroid 8.0 if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { NotificationChannel channel new NotificationChannel( ble_scan_channel, BLE Scan Service, NotificationManager.IMPORTANCE_LOW); NotificationManager manager getSystemService(NotificationManager.class); manager.createNotificationChannel(channel); } // 显示前台通知 Notification notification new NotificationCompat.Builder(this, ble_scan_channel) .setContentTitle(BLE Scanner Running) .setContentText(Scanning for devices...) .setSmallIcon(R.drawable.ic_bluetooth) .build(); startForeground(1, notification); // 启动扫描 startBleScan(); return START_STICKY; }5.2 Android 12 的蓝牙连接限制BLUETOOTH_CONNECT 权限与后台豁免Android 12 引入BLUETOOTH_CONNECT权限且默认禁止后台应用调用connectGatt()。若需后台连接如车载系统监听胎压传感器必须申请FOREGROUND_SERVICE_SPECIAL_USE!-- AndroidManifest.xml -- uses-permission android:nameandroid.permission.FOREGROUND_SERVICE_SPECIAL_USE /并在代码中声明特殊用途// Android 12 申请特殊前台服务 if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { if (ActivityCompat.checkSelfPermission(this, Manifest.permission.FOREGROUND_SERVICE_SPECIAL_USE) ! PackageManager.PERMISSION_GRANTED) { // 请求权限 } // 启动服务时指定类型 startForegroundService(intent, new Bundle(), bluetooth-connect); }5.3 连接稳定性优化重连机制与超时控制Android-ble-master.zip缺少健壮的重连逻辑。实际项目中需实现带退避的重连private static final int MAX_RETRY_COUNT 3; private static final long[] BACKOFF_DELAY_MS {1000, 3000, 5000}; private void reconnectWithBackoff(int attempt) { if (attempt MAX_RETRY_COUNT) { Log.e(BLE, Max retry attempts reached); return; } // 延迟重连 new Handler(Looper.getMainLooper()).postDelayed(() - { if (mBluetoothGatt null || !mBluetoothGatt.connect()) { reconnectWithBackoff(attempt 1); } }, BACKOFF_DELAY_MS[attempt]); }6. BLE 连接过程深度验证Logcat 过滤与关键状态码解读6.1 精准过滤 BLE 相关日志的 ADB 命令避免被海量日志淹没用tag精确捕获 BLE 核心流程# 过滤所有 BLE 相关 tagAndroid 10 adb logcat -s BluetoothAdapter:W BluetoothDevice:W BluetoothGatt:W BluetoothLeScanner:W # 或聚焦 GATT 操作 adb logcat -s BluetoothGatt:D # 查看连接状态变化status133 是经典超时错误 adb logcat | grep -i onconnectionstatechange\|status133\|status8\|status1296.2 关键 GATT status 码含义与应对措施Status Code含义常见原因解决方案0x80(128)GATT_REQ_NOT_SUPPORTED外设不支持该操作检查特征值属性确认是否支持读/写/通知0x81(129)GATT_INVALID_HANDLE特征值句柄无效重新discoverServices()确认服务 UUID 和特征值 UUID0x85(133)GATT_CONNECTION_TIMEOUT连接超时检查设备是否在范围内、电量是否充足、是否被其他设备占用0x08(8)GATT_BUSYGATT 通道忙实现操作队列避免并发调用0x0e(14)GATT_INSUF_AUTHORIZATION权限不足检查BLUETOOTH_CONNECT是否授予外设是否需配对6.3 使用 nRF Connect 验证外设行为的实操技巧nRF Connect 是验证 BLE 外设行为的黄金标准工具。关键验证步骤连接后立即查看 Services 列表确认目标服务如00001809-...是否存在展开服务查看 Characteristics检查目标特征值的 Properties 是否含Notify长按特征值 → Enable Notifications观察是否成功写入 CCCD DescriptorLogcat 应出现onDescriptorWrite success手动 Write Value输入十六进制值如0100验证外设是否响应对比 Android 日志若 nRF Connect 能正常 Notify 而 App 不能问题必在 App 的 Descriptor 写入逻辑。提示nRF Connect 的Log标签页会显示完整的 GATT 交互帧包括Write Request和Handle Value Notification可直接比对 App 发送的 Descriptor 值是否为01 00小端序。当adb logcat | grep onDescriptorWrite显示status0且 nRF Connect 能成功启用通知而你的 App 仍收不到onCharacteristicChanged()请立即检查setCharacteristicNotification()是否在writeDescriptor()之前调用——这是Android-ble-master.zip中复现率最高的逻辑错误。本文还有配套的精品资源点击获取
返回列表