Android BLE官方API全链路实践:扫描连接到GATT读写

发布时间:2026/10/26 13:58:56
Android BLE官方API全链路实践:扫描连接到GATT读写 简介本资源是基于Android官方BLE蓝牙低功耗开发指南实现的完整可运行Demo面向Android应用开发者及嵌入式通信初学者聚焦BLE核心流程设备扫描scan、连接建立connection、断连管理disconnect与服务发现discover service特别适配Android 4.0系统可用于快速理解UUID服务识别、GATT协议交互等关键概念。压缩包共70个文件涵盖26个Class类文件含核心BLE逻辑封装、15个XML布局与配置资源、9个PNG图标资源、7个Java源码文件如BluetoothLeService、DeviceScanActivity等主干实现以及APK安装包、JAR依赖库和项目构建配置文件整体仅1.34MB轻量易导入。已有527人学习下载提供开箱即用的工程结构——含标准Android Studio项目目录src/res/gen/bin/libs、完整AndroidManifest权限声明、proguard混淆配置及ic_launcher-web.png等规范资源便于读者直接调试、对照官网文档逐模块分析源码逻辑与回调机制。1. 这不是个“Hello World”Demo它用官方API跑通了BLE从扫描、连接、发现服务到读写特征值的全链路你手边有块开发板或者就是一台支持BLE的安卓手机想验证一个传感器模块能否被App稳定识别并读取温度数据——但卡在了“能扫到设备却连不上”“连上了却找不到服务UUID”“发现服务了但读特征值返回0x80”这种玄学问题上。这时候官方demo的价值就不是“学习”而是“对照”。这个 Android 蓝牙BLE官方demo基于 Android BluetoothLeScanner BluetoothGatt 官方API封装不是教学玩具它是一份可运行、可调试、可断点跟踪的端到端参考实现它完整覆盖了 Android 平台 BLE 设备交互的四个硬骨头——低功耗扫描scan、安全连接建立connect、GATT服务发现discover services、以及基于UUID的特征值读写read/write characteristic。它不依赖任何第三方SDK所有逻辑都落在BluetoothAdapter、BluetoothLeScanner、BluetoothGattCallback这套原生API上适合嵌入式联调、硬件协议验证、或作为自研BLE框架的baseline。如果你正被onConnectionStateChange卡在STATE_CONNECTING、被onServicesDiscovered的status 133报错反复暴击或者搞不清BluetoothGattCharacteristic的WRITE_TYPE_NO_RESPONSE和WRITE_TYPE_DEFAULT到底该选哪个——这份代码就是你的后悔药。2. 项目结构与核心依赖从AndroidManifest到Gradle配置的真实约束2.1 目录结构精简但无冗余每个文件都承担明确职责项目采用标准 Android Studio 模块结构无额外library子模块所有BLE逻辑集中在app/src/main/java/com/example/android/bluetoothlegatt/下bluetoothlegatt/ ├── MainActivity.java # 主界面触发扫描、展示设备列表、启动连接 ├── DeviceScanActivity.java # 扫描界面使用BluetoothLeScanner执行后台扫描实时更新RecyclerView ├── DeviceControlActivity.java # 控制界面连接后展示服务/特征值树支持读/写/使能通知 ├── BleService.java # 核心服务类封装BluetoothGatt生命周期管理、重连逻辑、超时控制 ├── SampleGattAttributes.java # UUID字典预置常见服务/特征值UUID如Battery Service, Heart Rate Measurement ├── Utils.java # 工具类byte[] ↔ hex string、RSSI转信号强度等级等 └── /res/layout/ # 仅4个布局文件activity_main.xml, device_scan.xml等无fragment提示BleService.java是整个项目的中枢它不是Android Service组件而是一个纯Java管理类——这是官方demo的刻意设计避免绑定Service带来的生命周期耦合让Gatt连接状态更可控。很多开发者误以为必须用IntentService或ForegroundService才能保活其实这里用HandlerThreadBluetoothGatt引用管理已足够应对常规场景。2.2 AndroidManifest.xml权限与声明的硬性门槛BLE功能在Android 6.0需运行时权限manifest中必须显式声明uses-permission android:nameandroid.permission.BLUETOOTH/ uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN/ !-- Android 6.0 必须声明 -- uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION/ !-- Android 12 需要蓝牙扫描权限 -- uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation/ uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT/ !-- 声明硬件特性Google Play会据此过滤不支持设备 -- uses-feature android:nameandroid.hardware.bluetooth_le android:requiredtrue/注意BLUETOOTH_SCAN和BLUETOOTH_CONNECT是Android 12API 31引入的运行时权限且不能通过uses-permission静态声明获得。必须在代码中动态申请否则startScan()直接抛SecurityException。官方demo在DeviceScanActivity.java的onResume()里做了完整适配if (Build.VERSION.SDK_INT Build.VERSION_CODES.S) { if (!hasPermissions(this, Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT)) { requestPermissions(new String[]{ Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT }, PERMISSION_REQUEST_CODE); } }2.3 build.gradleModule: apptargetSdkVersion决定API可用性关键配置项必须匹配Android版本演进android { compileSdk 34 defaultConfig { applicationId com.example.android.bluetoothlegatt minSdk 18 // BLE最低支持API 18Android 4.3 targetSdk 34 // 必须≥31才能使用BLUETOOTH_SCAN等新权限 versionCode 1 versionName 1.0 } } dependencies { implementation androidx.appcompat:appcompat:1.6.1 implementation com.google.android.material:material:1.10.0 // 无其他第三方BLE库依赖 —— 这是它作为“官方参考”的核心价值 }提示minSdk 18是硬性下限。若设为17或更低BluetoothLeScanner类根本不存在编译直接失败。而targetSdk 34不仅影响权限模型还决定了BluetoothGatt.requestConnectionPriority()等高级API是否可用——很多开发者把targetSdk锁在28导致无法设置连接优先级最终在弱信号下频繁断连却归咎于硬件。3. BLE扫描与设备发现从startScan到onScanResult的完整生命周期3.1 扫描参数配置ScanSettings决定你能“看见”什么DeviceScanActivity.java中的扫描启动逻辑是理解BLE行为的关键入口private void scanLeDevice(final boolean enable) { final BluetoothLeScanner bluetoothLeScanner bluetoothAdapter.getBluetoothLeScanner(); if (enable) { // 构建扫描设置这是影响扫描结果的核心 ScanSettings settings new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 高频扫描耗电高 .setReportDelay(0) // 实时上报不聚合 .build(); // 构建扫描过滤器可选但强烈建议添加以减少CPU占用 ListScanFilter filters new ArrayList(); // 示例只扫描广播包含特定服务UUID的设备如心率服务 ScanFilter filter new ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(0000180D-0000-1000-8000-00805F9B34FB)) // Heart Rate Service .build(); filters.add(filter); bluetoothLeScanner.startScan(filters, settings, mScanCallback); } else { bluetoothLeScanner.stopScan(mScanCallback); } }参数说明SCAN_MODE_LOW_LATENCY每毫秒级轮询适合需要快速发现设备的场景如配网但手机发热明显SCAN_MODE_BALANCED默认模式约1.28秒扫描窗口平衡功耗与发现速度SCAN_MODE_LOW_POWER最长可达数秒才扫描一次适合后台长期监听但可能错过短广播设备setReportDelay(0)设为0表示立即回调onScanResult()若设为1000则每秒汇总一次结果降低主线程压力ScanFilter不是必须但强烈推荐。未加过滤器时手机会接收所有BLE广播包包括iBeacon、手机热点、耳机等导致onScanResult()被高频触发UI线程卡顿。添加UUID过滤后系统芯片层即过滤极大减轻APP负担。3.2 扫描回调onScanResult中的设备去重与信号处理mScanCallback的实现位于DeviceScanActivity.java内部private ScanCallback mScanCallback new ScanCallback() { Override public void onScanResult(int callbackType, ScanResult result) { BluetoothDevice device result.getDevice(); int rssi result.getRssi(); byte[] scanRecord result.getScanRecord().getBytes(); // 原始广播数据 // 【关键】设备去重避免同一设备重复添加到列表 if (mLeDevices.containsKey(device.getAddress())) { // 更新RSSI不重复添加 mLeDevices.get(device.getAddress()).setRssi(rssi); } else { // 新设备构造LeDevice对象并加入Map LeDevice leDevice new LeDevice(device, rssi, scanRecord); mLeDevices.put(device.getAddress(), leDevice); } // 【关键】RSSI转信号强度官方demo用Utils.calculateSignalLevel() // 其逻辑是rssi ∈ [-100, -30] → level ∈ [0, 4]-55dBm以上为满格 int signalLevel Utils.calculateSignalLevel(rssi, 4); updateListView(); // 刷新RecyclerView } Override public void onBatchScanResults(ListScanResult results) { // 当setReportDelay 0时触发此处可批量处理 for (ScanResult result : results) { onScanResult(ScanCallback.CALLBACK_TYPE_ALL_MATCHES, result); } } Override public void onScanFailed(int errorCode) { Log.e(TAG, Scan failed with code: errorCode); // 常见errorCode2SCANNER_BUSY, 3SCAN_FAILED_INTERNAL_ERROR // 此处应触发UI提示并尝试重启扫描 } };逻辑说明onScanResult()每次只返回单个设备的最新扫描结果即使设备持续广播也会高频触发此回调mLeDevices使用device.getAddress()作key是去重的唯一可靠依据设备名可能为空或重复scanRecord是原始广播包包含服务UUID、设备名、制造商数据等SampleGattAttributes.java中的lookup()方法正是解析此数据获取服务UUID的关键onScanFailed()的errorCode必须处理2表示扫描器正忙如其他App正在扫描应延迟后重试3表示内部错误需检查蓝牙开关状态。3.3 广播数据解析从byte[]到服务UUID的转换SampleGattAttributes.java提供了lookup()工具方法用于从scanRecord中提取服务UUIDpublic static String lookup(String uuid, String unknown) { String key uuid.toUpperCase(); if (sAttributeDescriptions.containsKey(key)) { return sAttributeDescriptions.get(key); } return unknown; } // 在DeviceScanActivity中调用 String serviceUuid null; if (result.getScanRecord() ! null) { // 解析广播包中的16位UUID列表常见于Beacon、传感器 ListParcelUuid uuids result.getScanRecord().getServiceUuids(); if (uuids ! null !uuids.isEmpty()) { serviceUuid uuids.get(0).getUuid().toString(); } } String displayName SampleGattAttributes.lookup(serviceUuid, Unknown Service);为什么这步不可跳过很多开发者直接用device.fetchUuidsWithSdp()获取UUID但该方法必须先建立RFCOMM连接耗时且失败率高。而广播包中的getServiceUuids()是芯片层直接解析的毫秒级返回是发现设备能力的第一手信息。4. 连接与GATT服务发现从connectGatt到onServicesDiscovered的深度解析4.1 连接发起autoConnect参数的生死抉择DeviceControlActivity.java中的连接逻辑private void connectToDevice(String address) { BluetoothDevice device bluetoothAdapter.getRemoteDevice(address); // 【关键】autoConnect false主动连接立即发起握手 mBluetoothGatt device.connectGatt(this, false, mGattCallback); // 若设为true则进入“自动连接”模式系统在设备出现时后台尝试连接 // 适合耳机等需无缝重连场景但首次连接延迟不可控且onConnectionStateChange回调时机难把握 }autoConnect参数详解autoConnect行为适用场景风险false主动连接立即向设备发送连接请求onConnectionStateChange在数百毫秒内回调配网、调试、一次性数据采集若设备关机会快速失败status133true自动连接系统监听设备广播设备一出现即后台连接APP无感知蓝牙耳机、手环等需长期在线设备首次连接可能延迟数秒设备短暂消失再出现时会自动重连但APP无法精确控制时机提示官方demo全程使用autoConnectfalse这是调试阶段的黄金准则——只有主动连接才能精确控制连接时机便于断点跟踪BluetoothGattCallback的每一步。4.2 GATT回调链从连接建立到服务发现的七步流程mGattCallback是整个BLE交互的神经中枢其方法调用顺序严格遵循GATT协议private final BluetoothGattCallback mGattCallback new BluetoothGattCallback() { Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState BluetoothProfile.STATE_CONNECTED) { Log.i(TAG, Connected to GATT server.); // 【步骤1】连接成功立即发起服务发现 gatt.discoverServices(); } else if (newState BluetoothProfile.STATE_DISCONNECTED) { Log.i(TAG, Disconnected from GATT server.); } } Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status BluetoothGatt.GATT_SUCCESS) { Log.i(TAG, Services discovered.); // 【步骤2】服务发现成功开始遍历服务 ListBluetoothGattService services gatt.getServices(); for (BluetoothGattService service : services) { // 【步骤3】遍历服务下的特征值 ListBluetoothGattCharacteristic chars service.getCharacteristics(); for (BluetoothGattCharacteristic chara : chars) { // 【步骤4】读取特征值若PROPERTY_READ标记存在 if ((chara.getProperties() BluetoothGattCharacteristic.PROPERTY_READ) ! 0) { gatt.readCharacteristic(chara); } // 【步骤5】使能通知若PROPERTY_NOTIFY标记存在 if ((chara.getProperties() BluetoothGattCharacteristic.PROPERTY_NOTIFY) ! 0) { gatt.setCharacteristicNotification(chara, true); // 【步骤6】写入Client Characteristic Configuration Descriptor (CCCD) BluetoothGattDescriptor descriptor chara.getDescriptor( UUID.fromString(00002902-0000-1000-8000-00805f9b34fb)); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(descriptor); } } } } else { Log.w(TAG, onServicesDiscovered received: status); // 【步骤7】status133最常见GATT连接超时或设备不响应 // 此时应调用gatt.close()并重新connectGatt() } } Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status BluetoothGatt.GATT_SUCCESS) { // 处理读取到的数据 byte[] data characteristic.getValue(); } } Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { // 写入完成回调 } Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { // 通知数据到达 } };关键点说明onServicesDiscovered()的status不是HTTP状态码而是GATT协议层返回码。133GATT_ERROR最常见原因包括设备未响应服务发现请求、连接质量差、设备固件BUGdiscoverServices()是阻塞式调用必须等待onServicesDiscovered()返回后才能进行后续操作。很多开发者在此处加日志却发现没打印其实是服务发现失败了setCharacteristicNotification(true)只是开启客户端通知开关必须紧接着写CCCD描述符否则设备不会推送数据CCCD描述符的UUID是固定值00002902-...这是BLE规范强制要求无需从服务中查找。4.3 服务发现失败status133的根因与绕过方案当onServicesDiscovered()返回status133官方demo的默认处理是toast提示“Unable to connect”但实际开发中需深入排查现象原因解决方案同一设备在iOS上能连在Android上status133Android芯片驱动对GATT MTU协商支持不一致在onConnectionStateChange()后立即调用gatt.requestMtu(512)等待onMtuChanged()回调后再discoverServices()连接后立即status133但稍等1秒再手动点击“Discover Services”按钮就成功设备固件未在连接后预留足够响应时间在onConnectionStateChange()中添加Handler.postDelayed()延迟300ms再调用discoverServices()多次连接后必现status133Android系统GATT缓存未清理在close()前调用gatt.refresh()反射调用非公开API仅作临时方案提示gatt.refresh()是隐藏API需通过反射调用try { Method refresh BluetoothGatt.class.getMethod(refresh); refresh.invoke(gatt); } catch (Exception e) { Log.w(TAG, Failed to refresh GATT, e); }这是血泪经验——某次联调某医疗传感器连续5次连接后必133加了refresh()后稳定运行2小时无异常。5. 常见问题排查BLE开发中那些让你怀疑人生的报错5.1 现象onConnectionStateChange()一直停留在STATE_CONNECTING永不回调STATE_CONNECTED原因Android系统在连接过程中会尝试多种物理层参数如连接间隔、超时时间若设备固件未按规范响应连接请求包CONNECT_REQ系统会不断重试直至超时通常15-30秒期间状态卡在STATE_CONNECTING。解决检查设备是否处于可连接模式非广播-only模式在设备端抓包确认是否收到并响应CONNECT_REQ临时方案在connectGatt()后启动计时器3秒未回调则gatt.close()并重试避免UI长时间假死。5.2 现象onServicesDiscovered()回调status133但设备确定支持GATT服务原因133是GATT层通用错误码实际含义需结合上下文。常见子因设备在连接后未及时响应DISCOVER_ALL_SERVICES请求Android设备蓝牙芯片驱动bug尤其部分MTK平台手机系统省电策略杀死后台BLE进程Android 8.0。解决在onConnectionStateChange()后添加gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)提升连接参数协商优先级对于Android 8.0在AndroidManifest.xml中为BluetoothGattCallback所在Activity添加android:exportedtrue若需跨进程通信终极手段捕获status133后调用gatt.close()延迟500ms再connectGatt()最多重试3次。5.3 现象onCharacteristicRead()返回status133但特征值确有PROPERTY_READ原因133在此处表示GATT读取操作失败而非服务发现失败。根因通常是特征值当前不可读如传感器未启动采集设备要求先写入某个控制特征值才能解锁读取权限Android系统GATT缓存未更新上次读取的旧值被返回。解决在readCharacteristic()前先gatt.readDescriptor()读取特征值的User Description DescriptorUUID2901确认设备是否返回有效描述若设备有控制特征值如0000FF01-...先writeCharacteristic()发送启动命令再延时100ms后readCharacteristic()强制清除缓存gatt.refresh()反射→gatt.discoverServices()→ 再读取。5.4 现象启用通知后onCharacteristicChanged()永不触发但onCharacteristicWrite()显示写CCCD成功原因CCCD写入成功只代表客户端开关已打开设备端是否真正推送数据取决于设备固件是否实现了通知逻辑设备当前是否有新数据生成如传感器未采样设备发送的通知包是否符合GATT格式长度≤MTU-3。解决用nRF Connect等工具连接同一设备确认通知功能正常——若nRF也不触发则是设备问题检查设备MTUgatt.requestMtu(247)后确保通知包长度 ≤247-3244字节在设备端日志中确认是否调用了sd_ble_gatts_hvx()Nordic SDK或等效API。5.5 现象Android 12 设备申请BLUETOOTH_SCAN权限后仍被拒绝startScan()抛SecurityException原因Android 12引入了更严格的蓝牙权限模型BLUETOOTH_SCAN必须声明android:usesPermissionFlagsneverForLocation用户授权后必须重启APP进程否则BluetoothLeScanner实例仍无权限某些定制ROM如MIUI会二次拦截需手动在系统设置中开启“蓝牙扫描”开关。解决Manifest中严格按格式声明权限权限申请后检测ContextCompat.checkSelfPermission()返回值若为PackageManager.PERMISSION_DENIED则finishAffinity()并System.exit(0)强制重启在APP设置页增加引导文案“请在系统设置 应用权限 [APP名] 蓝牙扫描中开启”。6. 二次开发改造指南从Demo到生产级BLE应用的三步跃迁6.1 第一步将BleService升级为可复用的SDK模块官方demo的BleService.java是Activity内嵌的生产环境需解耦为独立模块。我一般会做三件事抽象回调接口定义BleCallback统一事件避免Activity直接实现BluetoothGattCallback导致强耦合增加连接池管理维护MapString, BluetoothGatt支持同时连接多个设备如多传感器网关注入重连策略将autoConnectfalse改为可配置提供ExponentialBackoffRetryPolicy指数退避重连。改造后核心调用变为BleManager.getInstance() .connect(AA:BB:CC:DD:EE:FF) .retryPolicy(new ExponentialBackoffRetryPolicy(3, 1000, 2)) .callback(new BleCallback() { Override public void onConnected(BluetoothGatt gatt) { // 服务发现、读写逻辑 } Override public void onDisconnected(String address) { // 重连或告警 } }) .execute();6.2 第二步增强扫描稳定性——对抗Android碎片化不同厂商对ScanSettings的支持差异巨大。我在某跨平台项目中沉淀出一套兼容方案厂商问题我的对策华为EMUISCAN_MODE_LOW_LATENCY导致扫描器崩溃检测Build.HARDWARE.contains(kirin)自动降级为SCAN_MODE_BALANCED小米MIUI后台扫描被系统限制在onDestroy()中启动前台Service保活并申请FOREGROUND_SERVICE_SPECIAL_USE权限三星One UIsetReportDelay(0)触发高频ANR改用setReportDelay(100)Handler批量合并结果关键代码片段放在ScanHelper.java中public ScanSettings buildScanSettings() { ScanSettings.Builder builder new ScanSettings.Builder(); if (isHuaweiKirin()) { builder.setScanMode(ScanSettings.SCAN_MODE_BALANCED); } else if (isSamsung()) { builder.setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) .setReportDelay(100); // 100ms聚合一次 } else { builder.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY); } return builder.build(); }6.3 第三步构建BLE通信协议栈——超越UUID硬编码官方demo用SampleGattAttributes.java硬编码UUID生产环境必须动态解析。我的做法是定义协议描述文件ble_protocol.json{ services: [ { uuid: 0000180F-0000-1000-8000-00805F9B34FB, name: Battery Service, characteristics: [ { uuid: 00002A19-0000-1000-8000-00805F9B34FB, name: Battery Level, properties: [READ, NOTIFY] } ] } ] }运行时加载协议在BleService.init()中解析JSON构建MapString, ServiceDef特征值操作封装bleService.read(Battery Level)自动查找UUID并执行不再暴露底层BluetoothGattCharacteristic。这样做的好处是硬件团队只需更新JSON文件APP无需发版即可支持新设备。从那以后我每次接手BLE项目都强制走一遍这三步——先用官方demo跑通基础链路再用BleManager替换掉所有BluetoothGatt裸调用最后用协议JSON接管UUID管理。这套组合拳下来联调周期从平均3天压缩到4小时而且再也不用翻着BluetoothGatt源码猜status含义了。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询