简介:本资源是一个面向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.0(BLE)开发的人,第一反应是“怎么配对?”——但 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.0(API 23)到 Android 14(API 34)的权限适配、后台扫描限制、GATT 操作队列阻塞等真实坑点。适合正在调试心率手环、温湿度传感器、电子标签等 BLE 外设的 Android 开发者,尤其当你发现 Logcat 里反复出现D/BluetoothGatt: onClientConnectionState() - status=133 clientIf=7 device=XX:XX:XX:XX:XX:XX却不知所措时,这篇就是为你写的。
2. 从 BluetoothAdapter 初始化到扫描启动:权限、兼容性与扫描参数的硬性约束
2.1 权限声明与运行时校验必须分两步走,缺一不可
Android 12(API 31)起,BLE 扫描被划入BLUETOOTH_SCAN特权权限,且需在AndroidManifest.xml中显式声明android:usesPermissionFlags="neverForLocation"。但仅声明不够,必须在代码中动态申请:
<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />提示:
ACCESS_FINE_LOCATION是 Android 10(API 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 List<ScanFilter> 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:name="android.hardware.bluetooth_le" android:required="true"/>,否则部分 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(List<ScanResult> 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); // errorCode=2 表示硬件忙;errorCode=3 表示参数非法;需重试或提示用户重启蓝牙 } };2.3.1 广告数据(AD Structure)解析的关键字段提取
BLE 广播包由多个 AD 结构(Advertising Data Structure)拼接而成,每个结构含Length(1字节)、AD Type(1字节)、AD Data(变长)。常用类型:
| AD Type (Hex) | 含义 | 提取方式 |
|---|---|---|
0x08 | Shortened Local Name | parseAdStructure(data, 0x08) |
0x09 | Complete Local Name | parseAdStructure(data, 0x09) |
0x02 | Flags | data[2] & 0x04判断是否支持 LE General Discoverable |
0x16 | Service 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 但后续无回调,大概率是权限未授予或autoConnect=true时设备未在范围内。
3.2 BluetoothGattCallback 的四大核心回调及其触发条件
GATT 操作完全异步,所有结果通过BluetoothGattCallback回调。Android-ble-master.zip中最易忽略的是回调触发顺序的强约束:
| 回调方法 | 触发条件 | 关键约束 |
|---|---|---|
onConnectionStateChange() | 连接建立/断开 | 必须在此回调中state == STATE_CONNECTED后,才能调用discoverServices() |
onServicesDiscovered() | 服务发现完成 | 必须在此回调中调用gatt.getService(uuid),否则返回null |
onCharacteristicRead() | 特征值读取完成 | 必须在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 Queue<Runnable> 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-00805f9b34fb(Client Characteristic Configuration)Descriptor 写入特定值:
| Descriptor Value | 含义 |
|---|---|
0x0000 | 禁用通知/指示 |
0x0001 | 启用通知(Notify) |
0x0002 | 启用指示(Indicate) |
private 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 10(API 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 Channel(Android 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:name="android.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 相关 tag(Android 10+) adb logcat -s BluetoothAdapter:W BluetoothDevice:W BluetoothGatt:W BluetoothLeScanner:W # 或聚焦 GATT 操作 adb logcat -s BluetoothGatt:D # 查看连接状态变化(status=133 是经典超时错误) adb logcat | grep -i "onconnectionstatechange\|status=133\|status=8\|status=129"6.2 关键 GATT status 码含义与应对措施
| Status Code | 含义 | 常见原因 | 解决方案 |
|---|---|---|---|
0x80(128) | GATT_REQ_NOT_SUPPORTED | 外设不支持该操作 | 检查特征值属性,确认是否支持读/写/通知 |
0x81(129) | GATT_INVALID_HANDLE | 特征值句柄无效 | 重新discoverServices(),确认服务 UUID 和特征值 UUID |
0x85(133) | GATT_CONNECTION_TIMEOUT | 连接超时 | 检查设备是否在范围内、电量是否充足、是否被其他设备占用 |
0x08(8) | GATT_BUSY | GATT 通道忙 | 实现操作队列,避免并发调用 |
0x0e(14) | GATT_INSUF_AUTHORIZATION | 权限不足 | 检查BLUETOOTH_CONNECT是否授予,外设是否需配对 |
6.3 使用 nRF Connect 验证外设行为的实操技巧
nRF Connect 是验证 BLE 外设行为的黄金标准工具。关键验证步骤:
- 连接后立即查看 Services 列表:确认目标服务(如
00001809-...)是否存在; - 展开服务查看 Characteristics:检查目标特征值的 Properties 是否含
Notify; - 长按特征值 → Enable Notifications:观察是否成功写入 CCCD Descriptor(Logcat 应出现
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"显示status=0且 nRF Connect 能成功启用通知,而你的 App 仍收不到onCharacteristicChanged(),请立即检查setCharacteristicNotification()是否在writeDescriptor()之前调用——这是Android-ble-master.zip中复现率最高的逻辑错误。
本文还有配套的精品资源,点击获取