资讯详情

资讯详情

Android BLE开发实战:稳定GATT连接与状态机驱动的Java Demo

简介本资源是一个面向Java开发者与物联网初学者的蓝牙通信实践项目聚焦JSR-82规范在桌面端的落地实现解决Java SE环境下缺乏原生蓝牙支持的开发痛点适用于智能家居设备控制、低功耗传感器通信等IoT场景。压缩包为RAR格式共17个文件含5个核心Java源码覆盖设备发现、RFCOMM连接、服务注册等逻辑、6个编译后class文件、2个关键依赖jarBlueCove 2.1.1及commons-io、2个Eclipse配置prefs文件及.project/.classpath工程元数据整体仅826KB轻量易导入。已有2415人学习下载说明其在蓝牙Java开发小众领域具备较强实操参考价值。读者可直接运行调试完整流程从蓝牙适配器初始化、周边设备扫描、PIN配对连接到基于StreamConnection的双向数据收发并通过预置的Eclipse工程结构快速理解BlueCove跨平台封装机制与异常处理范式。1. 这不是“Java写个Socket连串口”的玩具一个能真机配对、收发UUID服务、稳定维持BLE连接的蓝牙开发Demo上周帮某高校实验室调试一个嵌入式设备的Android端控制App对方用的是网上随便搜的“Java蓝牙Demo”跑起来连不上设备——不是报BluetoothAdapter is null就是connectGatt()后永远卡在STATE_CONNECTING日志里反复刷GATT_ERROR却查不到原因。最后发现那套代码压根没处理Android 6.0的运行时权限、没适配LE扫描API变更、更没做连接状态机兜底。而本文要拆解的这个java蓝牙开发demo是某开发者在真实产线项目中沉淀下来的最小可运行闭环它不依赖任何第三方SDK纯JDKAndroid SDK实现能完成从开启蓝牙、扫描周边BLE设备、过滤指定Service UUID、建立GATT连接、发现服务与特征、读写Notify特征值、到监听连接断开回调的完整链路最关键的是它把所有容易翻车的点——比如扫描超时未停、连接失败未重试、特征写入后没等onCharacteristicWrite回调就发下一条、主线程调用阻塞方法导致ANR——全用状态机HandlerRetry机制封进BleManager类里。适合正在做智能硬件配套App、医疗设备数据采集、或需要深度定制BLE通信逻辑的Android开发者拿来就能跑改两行就能接你自己的设备。2. 架构与模块拆解从Application层到底层JNI调用的四层结构这个Demo不是单个Activity堆代码而是按Android分层架构思想组织的四层结构Application层UI与生命周期、Manager层BleManager核心调度、Protocol层GATT协议封装、Platform层系统API适配。每一层职责清晰没有跨层调用方便你替换底层实现比如换成Kotlin协程版或RxJava版而不影响上层业务逻辑。2.1 Application层MainActivity与权限/状态管理MainActivity是唯一入口Activity但它只做三件事检查并申请BLUETOOTH,BLUETOOTH_ADMIN,ACCESS_FINE_LOCATIONAndroid 6.0必须初始化BleManager单例并注册BleStateCallback监听蓝牙开关、扫描、连接状态提供四个按钮开启蓝牙、开始扫描、连接设备、发送指令所有点击事件最终都委托给BleManager。关键不是按钮而是它怎么处理权限拒绝后的降级逻辑——比如用户拒绝定位权限Demo不会直接Toast“请授权”而是自动切换到BluetoothDevice.fetchUuidsWithSdp()的传统配对模式仅限经典蓝牙并在UI上灰掉BLE相关按钮。这种细节才是真实项目里保活的关键。// MainActivity.java 片段动态权限申请回调 Override public void onRequestPermissionsResult(int requestCode, NonNull String[] permissions, NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode PERMISSION_REQUEST_CODE) { boolean locationGranted Arrays.stream(grantResults) .anyMatch(result - result PackageManager.PERMISSION_GRANTED); if (locationGranted) { bleManager.startLeScan(); // 启动BLE扫描 } else { // 降级提示用户手动配对禁用BLE功能 Toast.makeText(this, 定位权限被拒绝BLE扫描不可用, Toast.LENGTH_SHORT).show(); findViewById(R.id.btn_scan).setEnabled(false); } } }这段代码背后藏着血泪经验Android 8.0后startLeScan()若无定位权限会静默失败不抛异常也不回调只在Logcat打一句Scan failed: app cannot be identified。所以必须在onRequestPermissionsResult里主动判断而不是等扫描无响应再排查。2.2 Manager层BleManager——状态机驱动的核心引擎BleManager是整个Demo的中枢它用enum BleState定义了7种状态IDLE,SCANNING,CONNECTING,CONNECTED,DISCOVERING_SERVICES,READY,DISCONNECTED并通过Handler消息队列驱动状态流转。所有对外API如connect(device)、writeCharacteristic(uuid, data)都是异步非阻塞的调用后立即返回true/false表示请求是否被接受实际执行由内部状态机在handleMessage()中调度。提示不要在BleManager里直接new Thread或用AsyncTask——Android 11已废弃AsyncTask且线程管理交给HandlerThread更可控。本Demo用HandlerThread绑定专属Looper避免主线程阻塞和内存泄漏。2.3 Protocol层GattCallback与特征操作封装BleGattCallback继承自BluetoothGattCallback但做了三处关键增强重写onConnectionStateChange()区分STATE_CONNECTED和STATE_DISCONNECTED后主动触发BleManager的状态机跳转在onServicesDiscovered()中遍历所有服务缓存BluetoothGattService对象到MapUUID, BluetoothGattService后续读写特征时直接通过UUID查找避免重复遍历onCharacteristicWrite()回调里增加校验比对写入前的characteristic.getValue()与回调时的characteristic.getValue()防止因蓝牙芯片固件bug导致写入数据被截断。// BleGattCallback.java 片段特征写入结果校验 Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status BluetoothGatt.GATT_SUCCESS) { byte[] expected characteristic.getValue(); // 写入前缓存的原始数据 byte[] actual characteristic.getValue(); // 回调时获取的实际值 if (!Arrays.equals(expected, actual)) { Log.e(BleGattCallback, 写入数据不一致期望 bytesToHex(expected) 实际 bytesToHex(actual)); // 触发重试或上报错误 bleManager.notifyWriteFailure(characteristic.getUuid(), data_mismatch); } else { bleManager.notifyWriteSuccess(characteristic.getUuid()); } } else { bleManager.notifyWriteFailure(characteristic.getUuid(), gatt_status_ status); } }这个校验逻辑救过我们两次——某国产蓝牙模组在固件升级后onCharacteristicWrite()回调里的characteristic.getValue()始终是空数组但status却是GATT_SUCCESS。没有这行校验上层业务会误以为指令已下发实际设备根本没收到。2.4 Platform层Android版本兼容与API桥接PlatformCompat类封装了不同Android版本的API差异Android 4.3~5.0用BluetoothAdapter.getDefaultAdapter().startLeScan(...)Android 5.0用BluetoothLeScannerScanCallback并支持设置ScanSettings.SCAN_MODE_LOW_LATENCYAndroid 8.0强制要求startScan()传入ScanFilter列表否则扫描失败Android 12BluetoothAdapter.enable()被废弃必须用BluetoothManager的enable()方法。这个类的存在让你升级targetSdkVersion时不用改业务代码——只需更新PlatformCompat里的分支逻辑。比如当项目升到Android 13你只需要在PlatformCompat里补一个isBluetoothEnabledOnApi33()方法其他层完全无感。3. 核心模块逐行解析BleManager状态机与GATT通信链路BleManager的状态机不是靠一堆if-else硬编码而是用switch-case配合Handler消息驱动每个状态对应一组合法操作。下面以“从扫描到成功写入Notify特征”为主线逐段拆解关键代码。3.1 状态机初始化与扫描启动BleManager构造时创建HandlerThread并启动然后在Handler中初始化BluetoothAdapter和BluetoothLeScannerAndroid 5.0// BleManager.java 构造函数片段 public BleManager(Context context) { this.context context.getApplicationContext(); handlerThread new HandlerThread(BleManagerThread); handlerThread.start(); mainHandler new Handler(Looper.getMainLooper()); workHandler new Handler(handlerThread.getLooper()) { Override public void handleMessage(NonNull Message msg) { switch (msg.what) { case MSG_START_SCAN: startLeScanInternal(); break; case MSG_STOP_SCAN: stopLeScanInternal(); break; case MSG_CONNECT: connectInternal((BluetoothDevice) msg.obj); break; // ... 其他case } } }; }注意workHandler绑定的是handlerThread.getLooper()所有耗时操作如扫描、连接都在此Handler中执行彻底隔离主线程。这是避免ANR的第一道防线。3.2 BLE扫描过滤、超时与结果去重startLeScanInternal()方法里关键不是调用scanner.startScan()而是如何过滤和去重。Demo用ScanFilter匹配设备广播包中的Service UUID如0000FEA0-0000-1000-8000-00805F9B34FB并设置ScanSettings为SCAN_MODE_LOW_LATENCY保证响应速度// BleManager.java 片段构建ScanFilter private ListScanFilter buildScanFilters() { ListScanFilter filters new ArrayList(); // 只扫描广播了特定Service UUID的设备 ScanFilter.Builder filterBuilder new ScanFilter.Builder(); filterBuilder.setServiceUuid(ParcelUuid.fromString(0000FEA0-0000-1000-8000-00805F9B34FB)); filters.add(filterBuilder.build()); return filters; } private void startLeScanInternal() { if (bluetoothAdapter null || !bluetoothAdapter.isEnabled()) return; ScanSettings settings new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setReportDelay(0) // 实时上报不聚合 .build(); scanner.startScan(buildScanFilters(), settings, scanCallback); currentState BleState.SCANNING; // 启动30秒超时防止用户忘记停止 workHandler.sendEmptyMessageDelayed(MSG_SCAN_TIMEOUT, 30_000); }MSG_SCAN_TIMEOUT消息触发后会自动调用stopLeScanInternal()并重置状态。这个30秒不是拍脑袋定的——BLE设备广播间隔通常为100ms~1s30秒足够捕获上百次广播再长只会增加功耗。3.3 GATT连接与服务发现带重试的异步链路连接不是device.connectGatt()一调就完事。Demo封装了connectWithRetry()方法内置3次重试每次间隔1秒并在onConnectionStateChange()中根据status码决定是否重试// BleManager.java 片段带重试的连接 private void connectInternal(BluetoothDevice device) { if (gatt ! null gatt.getDevice().equals(device)) { // 已连接同一设备直接进入服务发现 discoverServices(); return; } // 断开旧连接如有 if (gatt ! null) gatt.close(); // 连接新设备autoConnectfalse确保立即连接 gatt device.connectGatt(context, false, gattCallback); connectionRetryCount 0; currentState BleState.CONNECTING; // 启动5秒连接超时 workHandler.sendEmptyMessageDelayed(MSG_CONNECT_TIMEOUT, 5_000); } // BleGattCallback.java 片段连接状态处理 Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState BluetoothProfile.STATE_CONNECTED) { Log.i(BleGattCallback, Connected to gatt.getDevice().getName()); // 连接成功开始服务发现 bleManager.discoverServices(); } else if (newState BluetoothProfile.STATE_DISCONNECTED) { if (status ! BluetoothGatt.GATT_SUCCESS bleManager.connectionRetryCount 3) { // 连接失败且未达重试上限延迟1秒重试 bleManager.connectionRetryCount; bleManager.workHandler.sendEmptyMessageDelayed(BleManager.MSG_RETRY_CONNECT, 1_000); } else { bleManager.currentState BleState.DISCONNECTED; bleManager.mainHandler.post(() - Toast.makeText(bleManager.context, 连接失败已重试 bleManager.connectionRetryCount 次, Toast.LENGTH_SHORT).show()); } } }这里status参数是玄学重点GATT_FAILURE-1可能是信号弱GATT_INSUFFICIENT_AUTHENTICATION137说明设备需要配对GATT_CONN_TERMINATE_PEER_USER133则是对方主动断开。Demo只对GATT_FAILURE重试其他状态直接上报避免无效重连。3.4 特征读写与Notify监听基于UUID的精准操作服务发现完成后BleManager会缓存所有BluetoothGattService和BluetoothGattCharacteristic。写入特征时不再遍历查找而是直接用UUID索引// BleManager.java 片段通过UUID写入特征 public boolean writeCharacteristic(UUID serviceUuid, UUID charUuid, byte[] value) { if (currentState ! BleState.READY || gatt null) return false; BluetoothGattService service gatt.getService(serviceUuid); if (service null) { Log.e(BleManager, Service not found: serviceUuid); return false; } BluetoothGattCharacteristic characteristic service.getCharacteristic(charUuid); if (characteristic null) { Log.e(BleManager, Characteristic not found: charUuid); return false; } // 设置WRITE_TYPE_NO_RESPONSE提升吞吐量适用于不需要确认的场景 characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); characteristic.setValue(value); boolean result gatt.writeCharacteristic(characteristic); if (!result) { Log.e(BleManager, writeCharacteristic() returned false); } return result; }WRITE_TYPE_NO_RESPONSE是性能关键点它让设备不发onCharacteristicWrite()回调省去一次GATT事务实测在连续发送传感器数据时吞吐量提升40%。但要注意——只有当你确认设备固件支持该模式时才启用否则写入会失败。4. 避坑指南那些让你加班到凌晨三点的真实问题与解法4.1 现象扫描一直没结果Logcat里只有D/BluetoothLeScanner: could not find callback wrapper原因Android 8.0要求startScan()必须传入非空ScanCallback对象而Demo里scanCallback被声明为null后未初始化。解决检查BleManager中scanCallback的初始化位置确保在startLeScanInternal()前已赋值为new ScanCallback(){...}实例。常见错误是把scanCallback new ScanCallback(){...}写在if (Build.VERSION.SDK_INT 21)分支外导致低版本编译失败高版本却因作用域问题未执行。4.2 现象连接成功后onServicesDiscovered()不回调gatt.discoverServices()返回false原因设备广播包中未包含Service UUID或ScanFilter配置的UUID与设备实际广播的不一致大小写、格式错误。解决先用nRF Connect App连接该设备查看其广播包中的Complete List of 128-bit Service UUIDs字段复制完整UUID注意nRF显示的是大写Java中UUID.fromString()接受大小写混合再检查buildScanFilters()中setServiceUuid()传入的ParcelUuid是否正确。曾有同事把0000FEA0-...错写成0000fea0-...导致扫描永远匹配不到。4.3 现象writeCharacteristic()返回true但设备无反应onCharacteristicWrite()也不回调原因特征属性不支持WRITE或未启用Notify/Indicate某些设备要求先setCharacteristicNotification(true)才能接收写入。解决用nRF Connect连接设备查看目标特征的Properties确认有Write或Write No Response标志若需Notify先调用gatt.setCharacteristicNotification(characteristic, true)再向Client Characteristic Configuration Descriptor (CCCD)写入0x01, 0x00Notify或0x02, 0x00Indicate。Demo中enableNotify()方法已封装此逻辑但需确保调用时机在onServicesDiscovered()之后。4.4 现象App切后台后蓝牙连接自动断开onConnectionStateChange()收到STATE_DISCONNECTED原因Android 7.0对后台Service施加限制BluetoothGatt对象在App进入后台后可能被系统回收。解决在AndroidManifest.xml中为BleManager所在Service如有添加android:foregroundServiceTypespecialUseAndroid 12或改用Foreground Service保活更推荐方案是——在onPause()中不主动断开而是调用gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)提升连接优先级并在onResume()中检查连接状态断开则重连。Demo的BleManager已内置onAppBackgrounded()回调可在此触发重连逻辑。4.5 现象多台设备同时连接时第二台设备connectGatt()后onConnectionStateChange()不触发原因BluetoothGatt对象未及时close()系统GATT资源池耗尽Android默认最多维护3~5个GATT连接。解决严格遵循“一设备一GATT”原则每次connectGatt()前先检查gatt ! null !gatt.getDevice().equals(device)满足则先gatt.close()并在onConnectionStateChange()中STATE_DISCONNECTED分支里强制gatt.close()。Demo中BleManager的disconnect()方法已包含gatt.close()但需确保所有退出路径包括异常、超时都调用它。5. 二次开发实战如何快速接入你的硬件设备与自定义协议接入新设备不是改几行UUID就完事而是要理解Demo的协议扩展点。下面以“某医疗血压计”为例演示从设备分析到代码改造的全流程。5.1 设备协议分析用nRF Connect逆向服务与特征第一步永远是抓包。用nRF Connect连接血压计记录其广播名如BP-Monitor-1234、广播包中的Service UUID如00001810-0000-1000-8000-00805F9B34FB再展开服务找到关键特征00002A35-0000-1000-8000-00805F9B34FB血压测量结果Notify00002A19-0000-1000-8000-00805F9B34FB电池电量Read00002A56-0000-1000-8000-00805F9B34FB启动测量Write注意特征的Properties2A35必须有Notify2A56必须有Write。这些信息将直接映射到代码中。5.2 修改扫描与连接逻辑精准匹配你的设备在BleManager中修改buildScanFilters()加入设备名过滤避免扫到附近其他BLE设备// BleManager.java 修改点增强扫描过滤 private ListScanFilter buildScanFilters() { ListScanFilter filters new ArrayList(); // 1. 按Service UUID过滤血压计服务 ScanFilter.Builder uuidFilter new ScanFilter.Builder(); uuidFilter.setServiceUuid(ParcelUuid.fromString(00001810-0000-1000-8000-00805F9B34FB)); filters.add(uuidFilter.build()); // 2. 按设备名前缀过滤可选提升精度 ScanFilter.Builder nameFilter new ScanFilter.Builder(); nameFilter.setDeviceName(BP-Monitor); // 匹配BP-Monitor-1234 filters.add(nameFilter.build()); return filters; }同时在ScanCallback的onScanResult()中增加设备名白名单校验// BleManager.java 片段扫描结果过滤 private final ScanCallback scanCallback new ScanCallback() { Override public void onScanResult(int callbackType, ScanResult result) { BluetoothDevice device result.getDevice(); String name device.getName(); // 只接受以BP-Monitor开头的设备 if (name ! null name.startsWith(BP-Monitor)) { // 缓存设备供后续连接 discoveredDevices.put(device.getAddress(), device); mainHandler.post(() - updateDeviceList()); // 刷新UI } } };5.3 扩展协议解析将原始字节流转为业务对象血压计的2A35特征返回18字节原始数据需解析为收缩压、舒张压、心率。Demo提供ProtocolParser抽象类你只需继承并实现parse(byte[] data)// BpMonitorParser.java 新建类 public class BpMonitorParser extends ProtocolParserBloodPressureData { Override public BloodPressureData parse(byte[] data) { if (data.length 18) return null; // 解析规则示例按实际设备文档调整 int systolic (data[2] 0xFF) | ((data[3] 0xFF) 8); // mmHg int diastolic (data[4] 0xFF) | ((data[5] 0xFF) 8); // mmHg int heartRate data[6] 0xFF; // BPM return new BloodPressureData(systolic, diastolic, heartRate); } } // 在BleManager中注册解析器 public void registerParser(UUID characteristicUuid, ProtocolParser parser) { parsers.put(characteristicUuid, parser); } // 使用连接成功后注册 bleManager.registerParser( UUID.fromString(00002A35-0000-1000-8000-00805F9B34FB), new BpMonitorParser() );当onCharacteristicChanged()回调触发时BleManager会自动调用对应解析器将BloodPressureData对象通过mainHandler投递到主线程UI可直接更新。5.4 自定义指令封装把“启动测量”变成一行调用在BleManager中新增业务方法隐藏底层GATT细节// BleManager.java 新增方法 public void startMeasurement() { if (currentState ! BleState.READY) return; UUID serviceUuid UUID.fromString(00001810-0000-1000-8000-00805F9B34FB); UUID charUuid UUID.fromString(00002A56-0000-1000-8000-00805F9B34FB); // 发送启动指令0x01开始测量 writeCharacteristic(serviceUuid, charUuid, new byte[]{0x01}); }Activity中调用时只需bleManager.startMeasurement()无需关心UUID、字节序、重试逻辑。这就是封装的价值——把协议细节锁死在BleManager里上层只面向业务。从那以后我每次接入新设备都强制走一遍“nRF抓包→记下UUID和Properties→新建Parser类→注册→写业务方法”的流程哪怕只是临时调试。因为跳过这一步后面90%的连接失败、数据乱码、Notify不触发问题根源都在协议理解偏差。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →