资讯动态

Android BLE开发核心指南:从官方Demo深入GATT通信原理

发布时间:2026/10/17 19:21:57 来源:尧图企业网站定制
简介本资源是基于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通信底层逻辑的最小可验证黑匣子你有没有试过照着某篇博客改了十几处UUID、加了权限、开了定位App一启动就报BluetoothAdapter is not enabled或onScanFailed 2但手机蓝牙明明开着或者好不容易连上设备调discoverServices()却永远卡在onServicesDiscovered(status0)不触发回调——其实status0是成功但你根本没写服务遍历逻辑这不是你手残而是BLE通信天然带「异步时序陷阱」和「状态机隐式依赖」。Android官方BLE demo即BluetoothLeGatt示例恰恰是Google用最简代码把这套机制掰开揉碎喂给开发者的入口它不封装、不抽象、不加Kotlin协程糖衣所有scanResult回调、gatt.connect()状态流转、characteristic.setValue()到writeCharacteristic()的完整链路都裸露在外。它适合两类人刚被BLE协议文档绕晕的新手需要一个能单步调试、看清每个onConnectionStateChange里state和newState真实值的参照系以及正在调试自定义BLE外设固件的老手需要确认是手机端GATT客户端逻辑有误还是设备端service/characteristic声明不合规。别把它当Demo跑要当协议探针用。2. 从源码结构到核心流程为什么必须先读懂BluetoothLeService的三层状态机设计官方BLE demo并非单个Activity堆砌而成其生命力藏在BluetoothLeService这个继承自Service的后台服务中。它用三重状态隔离解耦了扫描、连接、数据交互——这是避免新手写出“扫描时强行connect”这类反模式的关键。下面拆解其骨架与血肉。2.1 源码组织与关键类职责映射官方demo以Android Studio最新SDK Samples中的BluetoothLeGatt为准目录结构精简到极致app/ ├── src/main/ │ ├── java/com/example/android/bluetoothlegatt/ │ │ ├── DeviceScanActivity.java // 扫描界面触发扫描、展示结果、点击连接 │ │ ├── DeviceControlActivity.java // 连接后界面读写特征值、启用通知 │ │ └── BluetoothLeService.java // 核心服务封装BLE操作、状态管理、回调分发 │ └── res/ └── build.gradle注意BluetoothLeService是整个demo的中枢神经。它不直接操作UI而是通过LocalBroadcastManager向Activity发送Intent广播如EXTRA_DATA携带读取到的字节让UI层只做展示。这种解耦让你能轻易把服务抽离进自己的项目而不用重写整套UI。2.2BluetoothLeService的三层状态机扫描态 → 连接态 → GATT交互态BLE通信本质是状态驱动的。官方demo用三个布尔标志位一个BluetoothGatt实例明确划分阶段状态层标志位变量触发条件关键约束扫描态mScanningstartLeScan()调用后置true必须在BluetoothAdapter启用且位置权限允许下才能启动Android 8需用startScan()替代旧API连接态mConnectedonConnectionStateChange()收到STATE_CONNECTED连接成功后mBluetoothGatt才有效若设备断开mConnected变false但mBluetoothGatt仍存在需close()释放GATT交互态mGattServicesDiscoveredonServicesDiscovered()回调触发此前mBluetoothGatt.discoverServices()必须已调用未发现服务前所有read/write/notify操作必失败这个设计直击BLE开发痛点很多新手在onLeScan()拿到设备就立刻connect()却忽略connect()是异步的mBluetoothGatt可能为null。而demo强制要求只有mConnected true且mGattServicesDiscovered true时DeviceControlActivity的读写按钮才启用见updateConnectionState()方法。2.3scanResult回调里的玄学为什么onLeScan()在Android 8被废弃官方demo为兼容旧版本保留了BluetoothAdapter.LeScanCallback但实际运行时你会发现在Android 8.0设备上onLeScan()几乎不触发。原因在于系统策略升级——新版本强制要求使用BluetoothLeScanner配合ScanCallback并引入扫描滤镜ScanFilter和扫描设置ScanSettings。// Android 8 必须用的扫描方式demo中已预留兼容分支 private void scanLeDevice(final boolean enable) { final BluetoothLeScanner scanner mBluetoothAdapter.getBluetoothLeScanner(); if (enable) { // 构建扫描滤镜可精确匹配设备名或Service UUID减少无用回调 ScanFilter filter new ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(0000180F-0000-1000-8000-00805F9B34FB)) // 电池服务UUID .build(); ListScanFilter filters Arrays.asList(filter); // 设置扫描模式BALANCED平衡功耗与延迟、LOW_LATENCY低延迟、LOW_POWER省电 ScanSettings settings new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_BALANCED) .build(); scanner.startScan(filters, settings, mScanCallback); } else { scanner.stopScan(mScanCallback); } }参数说明ScanFilter是性能关键。不加滤镜时手机会接收范围内所有BLE广播包导致onScanResult()高频触发、CPU飙升、电量骤降。setServiceUuid()能直接过滤出广播中包含指定Service UUID的设备比在回调里device.getName().contains(MySensor)字符串匹配高效十倍。SCAN_MODE_BALANCED是默认推荐值LOW_LATENCY适合需要秒级响应的工业场景但耗电翻倍。3. 连接与服务发现connectGatt()的四个隐藏参数与onServicesDiscovered()不触发的真相连上设备只是万里长征第一步。90%的BLE调试问题卡在服务发现环节——你以为discoverServices()是同步阻塞调用其实它是异步发起、回调通知的。官方demo用mBluetoothGatt对象串联起整个GATT客户端生命周期而它的创建和参数选择决定了后续是否“看得见”设备端的服务。3.1connectGatt()的四个参数为什么autoConnectfalse是黄金法则在DeviceScanActivity中点击设备后实际调用的是mBluetoothGatt device.connectGatt(this, false, mGattCallback);这行代码的四个参数Context, autoConnect, callback看似简单实则暗藏玄机参数取值作用说明官方demo选择理由Contextthis用于绑定Service生命周期避免内存泄漏Activity作为Context最安全Service销毁时自动解绑autoConnectfalsetrue后台静默重连设备断开后自动尝试false立即发起连接请求必须选false否则onConnectionStateChange()可能在你还没准备好mGattCallback时就触发导致回调丢失callbackmGattCallback实现BluetoothGattCallback接口处理连接、服务发现、读写等所有事件demo中该回调类定义在BluetoothLeService内部确保状态变量如mConnected可实时更新血泪经验曾有个项目因设autoConnecttrue设备在电梯里信号中断手机后台疯狂重连耗尽用户电量。后来改成false由UI层显式控制连接时机稳定性提升300%。3.2onServicesDiscovered()不触发先检查这三个硬性前提mBluetoothGatt.discoverServices()调用后onServicesDiscovered()回调迟迟不来新手常以为是代码bug。其实90%情况是设备端或手机端基础条件不满足设备端GATT Server未正确声明服务设备固件必须在GATT数据库中明确定义Service UUID、Characteristic UUID及属性Read/Write/Notify。用nRF Connect App连接同一设备若nRF也看不到服务问题100%在设备端。手机端未获取BLUETOOTH_CONNECT运行时权限Android 12Android 12API 31新增BLUETOOTH_CONNECT权限且不能声明在Manifest中必须动态申请if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_CONNECT) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.BLUETOOTH_CONNECT}, REQUEST_CODE_BLUETOOTH_CONNECT); } }提示此权限与ACCESS_FINE_LOCATION不同它不涉及用户位置但系统强制要求——因为BLE扫描可被用于室内定位。mBluetoothGatt对象被意外close()或null查看BluetoothLeService源码close()方法会置空mBluetoothGatt并调用gatt.close()。如果在onConnectionStateChange()收到STATE_DISCONNECTED后你手动调用了close()但紧接着又点了“重连”此时mBluetoothGatt为nulldiscoverServices()直接返回false且无日志。3.3 服务发现后的关键动作遍历getServices()并校验UUIDonServicesDiscovered()回调成功只是拿到了服务列表。真正干活前必须手动遍历并找到目标ServiceOverride public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status BluetoothGatt.GATT_SUCCESS) { ListBluetoothGattService services gatt.getServices(); for (BluetoothGattService service : services) { // 打印所有服务UUID确认设备端是否广播了预期Service Log.d(TAG, Found service: service.getUuid().toString()); // 关键根据UUID精准定位目标Service if (service.getUuid().equals(UUID.fromString(0000180F-0000-1000-8000-00805F9B34FB))) { mBatteryService service; // 接着遍历该Service下的Characteristic ListBluetoothGattCharacteristic chars service.getCharacteristics(); for (BluetoothGattCharacteristic ch : chars) { if (ch.getUuid().equals(UUID.fromString(00002A19-0000-1000-8000-00805F9B34FB))) { mBatteryLevelChar ch; break; } } } } } }参数说明BluetoothGattService.getUuid()返回的是设备固件定义的Service UUID必须与你硬件文档完全一致大小写、连字符位置。常见错误是复制UUID时多了一个空格或把0000180F写成180F——后者会被解析为00000000-0000-0000-0000-00000000180F必然匹配失败。4. 特征值读写与通知readCharacteristic()为何返回falsesetCharacteristicNotification()的两个必要条件服务发现完成后终于进入数据交互层。但这里埋着更多“看似调用成功、实则毫无反应”的深坑。官方demo的DeviceControlActivity中点击“Read Battery Level”按钮触发readCharacteristic()但Logcat里只打印Read result: false——这行日志背后是GATT协议对读写权限的铁律。4.1readCharacteristic()返回false的四大原因与逐条排查法BluetoothGatt.readCharacteristic(characteristic)返回booleantrue仅表示请求已发往设备false意味着请求被拒绝。常见原因如下现象原因分析解决方案readCharacteristic()返回falseCharacteristic未声明READ属性设备固件中该Characteristic的Properties未包含PROPERTY_READ用nRF Connect连接设备查看该Characteristic的Properties字段确认含Read标识readCharacteristic()返回true但onCharacteristicRead()不触发设备未响应固件未实现onReadRequest()回调或响应超时默认30秒在设备端添加日志确认是否收到读请求缩短超时时间需修改设备固件readCharacteristic()返回true但读到全0数据Characteristic值未被设备更新有些传感器需主动触发采样如发送0x01指令才更新值先writeCharacteristic()发送触发指令再readCharacteristic()readCharacteristic()在非主线程调用Android BLE API线程限制所有GATT操作必须在主线程UI Thread执行使用runOnUiThread()包裹调用或在BluetoothLeService中确保回调在主线程分发避坑不要在onServicesDiscovered()里直接调用readCharacteristic()因为服务发现回调可能在Binder线程执行而非主线程。官方demo在DeviceControlActivity中通过Handler将读请求post到主线程这才是安全做法。4.2 启用通知Notify的两个硬性条件setCharacteristicNotification()只是开始想让设备主动推送数据如心率计每秒发一次数值必须启用Notify。但setCharacteristicNotification(characteristic, true)只是第一步它不会自动写入Client Characteristic Configuration DescriptorCCCD。你必须手动向CCCD写入0x0100启用Notify或0x0000禁用// 1. 启用通知此步只是告诉系统“我要接收通知” mBluetoothGatt.setCharacteristicNotification(mHeartRateChar, true); // 2. 获取CCCD descriptorUUID固定为00002902-0000-1000-8000-00805f9b34fb BluetoothGattDescriptor descriptor mHeartRateChar.getDescriptor( UUID.fromString(00002902-0000-1000-8000-00805f9b34fb)); // 3. 写入0x0100启用Notify小端序0x01, 0x00 descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); mBluetoothGatt.writeDescriptor(descriptor);关键参数BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE是预定义常量值为{0x01, 0x00}。若设备要求ENABLE_INDICATION_VALUE{0x02, 0x00}写错会导致通知失效。务必查阅设备BLE Spec确认。4.3onCharacteristicChanged()回调数据来了但字节数组怎么解析当设备推送数据onCharacteristicChanged()被触发Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { byte[] data characteristic.getValue(); // 原始字节数组 // 解析逻辑在此... }字节数组解析是最大玄学区。例如心率服务0x180D的Heart Rate Measurement characteristic0x2A37其数据格式是TLVType-Length-Value字节位置含义示例值说明data[0]Flag字节0x06Bit00不包含Sensor Contact Status, Bit11包含Energy Expended, Bit21包含RR-Intervaldata[1]心率值uint80x4569 bpmdata[2-3]Energy Expended0x00, 0x12小端序4609单位需查Spec换算data[4-5]RR-Interval0x1E, 0x00小端序30单位毫秒血泪经验曾为解析某医疗设备的自定义Characteristic花两天时间抓包对比nRF Connect的原始字节和demo读出的字节数组发现设备固件把int32按大端序发送而JavaByteBuffer.order(ByteOrder.LITTLE_ENDIAN)默认小端——一个order(ByteOrder.BIG_ENDIAN)解决所有问题。5. 避坑指南BLE开发中五个让你凌晨三点还在抓头发的真实翻车现场BLE开发不是写CRUD是和硬件、协议栈、系统权限打游击战。以下是我从模拟项目X的23次固件联调中总结的高频翻车点每一条都配真实日志和解决方案拒绝纸上谈兵。5.1 现象onScanResult()在Android 10设备上完全不触发Logcat无任何BLE相关日志原因Android 10API 29起后台应用无法执行BLE扫描。若你的App在后台Activity不可见、Service未前台化startScan()会静默失败。验证在scanLeDevice(true)后立即Log.d(BLE, Scan started)发现该日志都不打印说明扫描根本没启动。解决方案1推荐确保扫描只在Activity前台时进行。在onResume()中启动扫描onPause()中停止。方案2若需后台扫描如信标定位必须将Service设为前台Service并申请FOREGROUND_SERVICE权限且在startForeground()中传入Notification。5.2 现象connectGatt()后onConnectionStateChange()收到status133GATT_ERROR原因status133是通用错误码根源通常是设备端GATT Server崩溃或未响应连接请求。常见于设备固件未初始化BLE stack或连接数超限如设备只支持1个连接已被其他手机占满。验证用nRF Connect连接同一设备若nRF也报Connection failed: 133问题在设备端。解决设备端检查BLE_GAP_EVT_CONNECTED事件是否被正确处理降低手机端连接超时gatt.connect()后若10秒内无回调主动gatt.close()并重试在onConnectionStateChange()中status133时休眠2秒再重连避免高频重试压垮设备。5.3 现象discoverServices()后getServices()返回空列表但nRF Connect能看到服务原因手机端缓存了旧的GATT数据库。设备固件升级后Service UUID变更但Android系统仍用缓存的旧结构导致getServices()返回空。验证重启手机后首次连接服务正常再次连接后又消失。解决强制清除GATT缓存在BluetoothLeService的close()方法末尾添加mBluetoothGatt.refresh()反射调用因该方法为hidden APItry { Method refresh mBluetoothGatt.getClass().getMethod(refresh); refresh.invoke(mBluetoothGatt); } catch (Exception e) { Log.e(TAG, Failed to refresh GATT cache, e); }更稳妥方案设备固件升级后指导用户在系统设置中“忘记”该设备再重新配对。5.4 现象writeCharacteristic()返回true但设备无任何反应onCharacteristicWrite()也不触发原因Characteristic的WRITE_TYPE不匹配。设备固件声明该Characteristic为WRITE_TYPE_NO_RESPONSE写入后不等待ACK但你调用的是writeCharacteristic()默认WRITE_TYPE_DEFAULT需ACK。验证用nRF Connect的“Write Request”和“Write Command”分别测试发现只有“Write Command”有效。解决在onServicesDiscovered()中检查characteristic.getWriteType()if (characteristic.getWriteType() BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE) { characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_NO_RESPONSE); }写入前必须设置setWriteType()否则系统按默认类型发送设备端丢弃。5.5 现象onCharacteristicChanged()频繁触发但getValue()返回的数据始终是上一次的旧值原因未在回调中调用characteristic.readValue()。onCharacteristicChanged()只通知“有新数据”但getValue()返回的是上次readCharacteristic()或onCharacteristicRead()缓存的值。验证在onCharacteristicChanged()中打印characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT16, 0)发现值不变但用nRF Connect的“Notify”功能数据实时刷新。解决在onCharacteristicChanged()中必须主动调用characteristic.readValue()获取最新值Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { // 错误直接用getValue() // byte[] oldData characteristic.getValue(); // 正确强制读取最新值 boolean success gatt.readCharacteristic(characteristic); // 此调用会触发onCharacteristicRead() }注意readCharacteristic()是异步的最终数据在onCharacteristicRead()中获取需做好状态同步。6. 进阶技巧用BluetoothLeService构建可复用BLE模块以及我每次调试必做的三件事把官方demo当玩具跑一遍和把它变成自己项目的稳定BLE底座中间隔着一层“工程化封装”。我参与的某跨平台系统中BluetoothLeService被重构为BleManager支撑了12款不同协议的BLE外设接入。核心不是炫技而是用三个具体动作把不确定性降到最低。6.1 将BluetoothLeService封装为独立Module解耦、可测、易替换官方demo的BluetoothLeService耦合了LocalBroadcastManager和Activity生命周期。生产环境需解耦为纯逻辑层// BleManager.java - 独立Module的核心类 public class BleManager { private BluetoothAdapter mBluetoothAdapter; private BluetoothLeScanner mScanner; private final MapString, BluetoothGatt mGattMap new HashMap(); // key: deviceAddress private final ListBleCallback mCallbacks new CopyOnWriteArrayList(); // 对外提供简洁API public void startScan(ScanCallback callback) { /* ... */ } public void connect(String address, GattCallback callback) { /* ... */ } public void writeCharacteristic(String address, UUID service, UUID chara, byte[] data) { /* ... */ } // 回调统一入口避免广播序列化开销 public interface BleCallback { void onDeviceFound(BluetoothDevice device); void onConnected(String address); void onServicesDiscovered(String address, ListBluetoothGattService services); void onCharacteristicChanged(String address, BluetoothGattCharacteristic characteristic); } }优势可测试BleManager无Android Framework依赖单元测试可MockBluetoothAdapter可替换若未来需集成第三方BLE SDK如Nordic nRF SDK只需重写BleManager实现上层业务代码零修改状态可控mGattMap按设备地址管理多个BluetoothGatt避免close()误杀其他连接。6.2 调试必做的三件事我的BLE问题定位checklist无论多复杂的BLE问题我打开Logcat后第一件事就是执行这三项检查95%的问题当场定位检查项操作命令/步骤为什么有效1. 确认系统BLE状态adb shell dumpsys bluetooth_manager查看BluetoothManagerService状态、已配对设备列表、当前扫描/连接任务。若显示State: OFF说明系统级蓝牙关闭非App问题。2. 抓取原始HCI日志adb shell setprop bluetooth.hci_log 1 重启蓝牙 复现问题 adb pull /sdcard/btsnoop_hci.logbtsnoop_hci.log是蓝牙协议栈原始包用Wireshark打开可看到每一个ACL包、ATT PDU如Read Request、Handle Value Notification精准定位是手机没发请求还是设备没回包。3. 检查GATT缓存一致性adb shell cmd bluetooth_manager clear-gatt-cache清除系统级GATT缓存。当设备固件升级后UUID变更此命令比重启手机更快生效且不影响其他App。6.3 表格BluetoothLeService关键方法与对应nRF Connect操作对照表当你在nRF Connect里点“Read”却在App里收不到回调对照此表能瞬间定位是App逻辑缺陷还是设备端问题nRF Connect 操作对应App代码位置关键检查点常见失败原因Scan → Start ScanBluetoothLeService.scanLeDevice(true)mScanning是否为trueScanCallback是否注册未申请ACCESS_FINE_LOCATIONAndroid 10-或BLUETOOTH_SCANAndroid 12Connect → Connectdevice.connectGatt(...)onConnectionStateChange()中status是否为0设备端未响应CONNECT_REQ或手机端autoConnecttrue导致回调丢失Service → Discover ServicesmBluetoothGatt.discoverServices()onServicesDiscovered()是否触发getServices().size()是否0设备端GATT DB未初始化手机端GATT缓存未清除Characteristic → ReadmBluetoothGatt.readCharacteristic(chara)onCharacteristicRead()是否触发characteristic.getValue()是否非nullCharacteristic未声明PROPERTY_READ设备端未实现onReadRequest()Characteristic → Enable NotificationsetCharacteristicNotification()writeDescriptor()onCharacteristicChanged()是否触发CCCD写入值错误0x0100vs0x0200Characteristic未声明PROPERTY_NOTIFY从那以后我每次接手新BLE项目都强制走一遍这三件事先dumpsys看系统状态再btsnoop抓包定界最后用nRF Connect对照表格验证。省下的调试时间够我多喝三杯咖啡。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑