简介:本资源是基于Android官方蓝牙低功耗(BLE)开发指南实现的完整Demo工程,面向Android应用开发者及嵌入式通信初学者,聚焦BLE核心流程实践:设备扫描(scan)、连接建立(connection)、断连管理(disconnect)与服务发现(discover service),覆盖UUID识别、GATT协议交互等关键知识点,适用于智能硬件App对接、IoT终端调试等典型场景。压缩包共70个文件,含26个Class类文件与7个Java源码构成主逻辑,15个XML布局与配置文件支撑UI与权限声明,9个PNG图标及1个APK安装包便于快速验证,辅以JAR依赖库、ProGuard混淆配置及项目元数据文件,结构完整、开箱即用,总大小仅1.34MB。目前已有525人学习下载,提供从Manifest声明、BluetoothAdapter初始化到Service/Characteristic解析的全链路代码参考,目录层级清晰,适合作为BLE入门实践模板或二次开发基线工程。
1. 这不是个“点开就连上”的玩具:Android BLE 官方 Demo 是一套可落地的连接状态机骨架
你手上有台 Android 手机,旁边放着一块 nRF52840 开发板,想让 App 读取它的电池电量——结果跑通官方 BLE Demo 后发现:扫描能扫到设备,点击却卡在Connecting...半分钟不动,再点一次直接崩出BluetoothGatt: Unhandled exception in callback。这不是你代码写错了,而是官方 Demo 本就不封装重连逻辑、不处理 GATT 操作队列、不兜底连接中断后的服务发现重试。它是一套高度解耦的“BLE 连接状态机骨架”,把 scan → connect → discover services → read/write/notify 的每个环节都拆成独立回调入口,让你看清底层到底在什么时候发了什么包、回调里该做什么事、哪些状态必须手动维护。适合正在调试硬件通信协议、需要自定义连接超时策略、或准备接入多设备并发管理的开发者。如果你只是想快速展示“手机连灯带”,它反而比第三方库更费劲;但如果你正被GATT_FAILURE错误折磨得睡不着,这个 Demo 就是你该逐行盯的源码教科书。
2. 项目结构与技术栈:从 AndroidX 蓝牙 API 到状态驱动的 Fragment 架构
2.1 目录结构解析:为什么DeviceScanActivity和DeviceControlFragment是核心?
整个项目采用典型的 AndroidX + ViewModel + Fragment 架构,但 BLE 相关逻辑全部下沉到BluetoothLeService(继承自Service)中,UI 层仅负责触发和响应。目录结构如下:
app/ ├── src/main/ │ ├── java/com/example/android/bluetoothlegatt/ │ │ ├── DeviceScanActivity.java // 主界面:启动扫描、跳转设备详情 │ │ ├── DeviceControlFragment.java // 设备控制页:服务发现、特征值读写、通知开关 │ │ ├── BluetoothLeService.java // 核心服务:封装 BluetoothAdapter、BluetoothGatt 全生命周期 │ │ ├── SampleGattAttributes.java // 静态 UUID 映射表:将 16-bit 短 UUID 转为标准字符串 │ │ └── Utils.java // 工具类:hex 字符串转换、时间戳格式化等 │ └── res/ │ └── layout/ │ ├── activity_device_scan.xml // 扫描列表页布局 │ └── fragment_device_control.xml // 控制页布局(含 service/characteristic 列表)关键点在于:BluetoothLeService并非简单包装BluetoothGattCallback,而是显式维护了mBluetoothGatt实例的引用生命周期,并在onConnectionStateChange()中主动判断newState == BluetoothProfile.STATE_CONNECTED后才调用discoverServices()。这避免了某些低端芯片在连接成功后未及时返回onServicesDiscovered()导致的“假死”——你看到界面上显示已连接,但服务列表始终为空。而DeviceControlFragment通过LocalBroadcastManager接收服务广播,把 UI 更新和业务逻辑彻底解耦,方便你后续替换成LiveData或Flow。
2.2 技术选型依据:为什么用BluetoothGattCallback而不用BluetoothGattServerCallback?
项目只实现GATT Client 角色(即手机作为中心设备),因此全程使用BluetoothGattCallback处理远程外设事件。注意三个高频回调的触发时机与责任边界:
onConnectionStateChange(BluetoothGatt gatt, int status, int newState)status == BluetoothGatt.GATT_SUCCESS且newState == BluetoothProfile.STATE_CONNECTED时,必须手动调用gatt.discoverServices()。这是官方 Demo 最易被忽略的“玄学”步骤——很多开发者以为连接成功就自动完成服务发现,实际必须显式发起。onServicesDiscovered(BluetoothGatt gatt, int status)status == BluetoothGatt.GATT_SUCCESS才代表服务发现完成。此时gatt.getServices()返回的 List 才可用。若失败(如status == BluetoothGatt.GATT_FAILURE),不能直接重试discoverServices(),需先gatt.close()再gatt.connect()重建连接(见第 4 章避坑)。onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status)
读操作完成后触发。注意characteristic.getValue()返回的是byte[],需按协议解析(如电池电量是uint8,需characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT8, 0))。
提示:
BluetoothGattServerCallback用于实现 GATT Server(即手机作为外设被其他设备连接),本 Demo 完全不涉及。混淆两者会导致onCharacteristicWriteRequest()永远不触发。
2.3 权限与配置:Android 12+ 必须声明BLUETOOTH_SCAN且动态申请
AndroidManifest.xml中声明的权限已随系统版本演进:
<!-- Android 12+ 必须 --> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <!-- Android 11 及以下仍需 --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />关键变化:BLUETOOTH_SCAN和BLUETOOTH_CONNECT是运行时危险权限,必须在代码中动态申请。Demo 中DeviceScanActivity的checkPermissionsAndStartScan()方法做了兼容处理:
// Android 12+ 使用新权限组 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.BLUETOOTH_SCAN) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT}, PERMISSION_REQUEST_CODE); return; } } 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}, PERMISSION_REQUEST_CODE); return; } } startScan(); // 权限通过后才真正开始扫描注意:BLUETOOTH_CONNECT权限无法在 manifest 中声明android:maxSdkVersion,必须无条件申请。若遗漏,connectGatt()会静默失败(Logcat 仅提示Permission denied),且不会抛出异常。
3. 核心模块拆解:从扫描到特征值读取的完整链路
3.1 扫描模块:BluetoothLeScanner的过滤与回调陷阱
扫描由BluetoothLeScanner执行,其startScan()方法接受三种参数组合。Demo 使用最严格的ScanSettings+ScanFilter方式:
private void scanLeDevice(final boolean enable) { final BluetoothLeScanner scanner = mBluetoothAdapter.getBluetoothLeScanner(); if (enable) { // 构建过滤器:只扫描含特定 Service UUID 的设备 ScanFilter.Builder filterBuilder = new ScanFilter.Builder(); filterBuilder.setServiceUuid(ParcelUuid.fromString("0000180F-0000-1000-8000-00805F9B34FB")); // Battery Service ScanFilter filter = filterBuilder.build(); // 扫描设置:低功耗模式,持续扫描 ScanSettings settings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) // 关键!避免耗电过高 .build(); // 启动扫描 scanner.startScan(Arrays.asList(filter), settings, mScanCallback); } else { scanner.stopScan(mScanCallback); } }mScanCallback是ScanCallback实现类,其中onScanResult()是唯一可靠回调:
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(); // 原始广播包数据 // 注意:此处 device.getName() 可能为 null!需从 scanRecord 解析 AD Structure // 官方 Demo 直接用 device.getName(),导致部分设备名称显示为空 } };关键细节:
device.getName()在 Android 上不可靠。真实场景中应解析scanRecord中的AD Type 0x09 (Complete Local Name)或0x08 (Shortened Local Name)。Demo 未做此处理,是典型“演示性代码”与“生产代码”的分水岭。
3.2 连接与服务发现:BluetoothGatt状态机的手动推进
连接操作在DeviceControlFragment中触发,核心逻辑在BluetoothLeService的connect()方法:
public boolean connect(final String address) { if (mBluetoothAdapter == null || address == null) return false; final BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); if (device == null) { Log.w(TAG, "Device not found. Unable to connect."); return false; } // 关闭旧连接(重要!避免资源泄漏) if (mBluetoothGatt != null) { mBluetoothGatt.close(); mBluetoothGatt = null; } // 建立新连接:autoConnect=false 表示立即连接(非后台连接) mBluetoothGatt = device.connectGatt(mService, false, mGattCallback); Log.d(TAG, "Trying to create a new connection."); return true; }mGattCallback是BluetoothGattCallback实现,其onConnectionStateChange()必须严格处理状态跃迁:
private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { String intentAction; if (newState == BluetoothProfile.STATE_CONNECTED) { intentAction = ACTION_GATT_CONNECTED; broadcastUpdate(intentAction); Log.i(TAG, "Connected to GATT server."); // 关键:连接成功后必须主动发现服务! gatt.discoverServices(); // ← 这行漏掉,后续所有操作都无效 } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { intentAction = ACTION_GATT_DISCONNECTED; broadcastUpdate(intentAction); Log.i(TAG, "Disconnected from GATT server."); } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED); Log.i(TAG, "Services discovered."); // 此时可安全获取服务列表 List<BluetoothGattService> services = gatt.getServices(); for (BluetoothGattService service : services) { Log.d(TAG, "Service: " + service.getUuid()); // 遍历特征值... } } else { Log.w(TAG, "onServicesDiscovered received: " + status); } } };注意:
discoverServices()是异步操作,回调onServicesDiscovered()可能延迟数百毫秒。Demo 中 UI 层通过广播监听ACTION_GATT_SERVICES_DISCOVERED更新列表,而非轮询getServices()—— 这是响应式编程的基本素养。
3.3 特征值读写与通知:BluetoothGattCharacteristic的操作队列管理
读取特征值(如电池电量)的完整流程:
// 1. 获取目标特征值(需先确保服务已发现) BluetoothGattService batteryService = mBluetoothGatt.getService( UUID.fromString("0000180F-0000-1000-8000-00805F9B34FB")); BluetoothGattCharacteristic batteryLevelChar = batteryService.getCharacteristic( UUID.fromString("00002A19-0000-1000-8000-00805F9B34FB")); // 2. 发起读请求(异步) if (mBluetoothGatt.readCharacteristic(batteryLevelChar)) { Log.d(TAG, "Read battery level request sent."); } else { Log.e(TAG, "Failed to read battery level."); } // 3. 在 onCharacteristicRead() 回调中处理结果 @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { // 解析 uint8 类型的电池电量 int batteryLevel = characteristic.getIntValue( BluetoothGattCharacteristic.FORMAT_UINT8, 0); Log.d(TAG, "Battery Level: " + batteryLevel + "%"); broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); } }启用通知(Notify)的步骤更复杂,需两步操作:
// Step 1: 设置本地通知开关(客户端侧) if (mBluetoothGatt.setCharacteristicNotification(batteryLevelChar, true)) { Log.d(TAG, "Notification enabled locally."); } else { Log.e(TAG, "Failed to set notification."); return; } // Step 2: 写入 CCCD(Client Characteristic Configuration Descriptor) BluetoothGattDescriptor descriptor = batteryLevelChar.getDescriptor( UUID.fromString("00002902-0000-1000-8000-00805F9B34FB")); // CCCD UUID descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); if (mBluetoothGatt.writeDescriptor(descriptor)) { Log.d(TAG, "CCCD write request sent."); } else { Log.e(TAG, "Failed to write CCCD."); }关键点:
setCharacteristicNotification(true)仅开启本地接收,必须写入 CCCD 才能通知外设“请给我发数据”。两者缺一不可,且writeDescriptor()也需等待onDescriptorWrite()回调确认成功。
4. 避坑指南:BLE 开发中 5 个血泪经验总结
4.1 现象:扫描列表里设备名称全是null,但地址正确
原因:BluetoothDevice.getName()依赖设备广播中的Complete Local NameAD 结构,而很多 BLE 模块(尤其 Nordic nRF52 系列)默认关闭该字段以节省广播包空间。
解决:解析ScanResult.getScanRecord().getBytes(),手动提取 AD Type0x09数据。参考Utils.parseScanRecord()方法补全逻辑,或直接用result.getDevice().fetchUuidsWithSdp()(但会阻塞主线程,慎用)。
4.2 现象:connectGatt()后onConnectionStateChange()不触发,Logcat 无报错
原因:Android 12+ 缺少BLUETOOTH_CONNECT权限,或BluetoothAdapter未启用(isEnabled() == false)。
解决:在connect()前强制检查:
if (!mBluetoothAdapter.isEnabled()) { Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); return; }4.3 现象:discoverServices()后onServicesDiscovered()永远不回调,状态卡在STATE_CONNECTED
原因:外设 GATT 服务端未正确响应Exchange MTU Request,或手机与外设 MTU 值不匹配(默认 23 字节,大包需协商)。
解决:在onConnectionStateChange()中连接成功后立即协商 MTU:
gatt.requestMtu(128); // 请求 128 字节 MTU // 等待 onMtuChanged() 回调后再调用 discoverServices()4.4 现象:readCharacteristic()成功但onCharacteristicRead()的status为GATT_READ_NOT_PERMITTED
原因:目标特征值的 Properties 不包含PROPERTY_READ(如只有PROPERTY_NOTIFY)。
解决:在onServicesDiscovered()中检查特征值属性:
if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) == 0) { Log.w(TAG, "Characteristic not readable: " + characteristic.getUuid()); return; }4.5 现象:连续快速点击“读取”按钮,onCharacteristicRead()回调乱序或丢失
原因:BluetoothGatt内部是单线程操作队列,但 Demo 未做防抖,多次readCharacteristic()调用会堆积。
解决:在BluetoothLeService中添加操作锁:
private boolean mIsOperationPending = false; public boolean readCharacteristic(BluetoothGattCharacteristic characteristic) { if (mIsOperationPending) return false; mIsOperationPending = true; boolean result = mBluetoothGatt.readCharacteristic(characteristic); if (!result) mIsOperationPending = false; return result; } // 在 onCharacteristicRead() 结尾置为 false5. 二次开发实战:如何把 Demo 改造成支持多设备并发的连接池
5.1 问题定位:原 Demo 的单连接瓶颈
官方 Demo 的BluetoothLeService内部只持有一个mBluetoothGatt引用,所有操作都基于这一个实例。这意味着:
- 无法同时连接多个外设(如温湿度传感器 + 心率带 + 电子秤);
- 断开当前设备后,
mBluetoothGatt.close()会销毁整个连接上下文,无法复用; broadcastUpdate()发送的 Intent 没有携带设备地址,UI 层无法区分数据来源。
要支撑多设备,必须将“连接实例”与“设备标识”绑定,并抽象出连接生命周期管理。
5.2 改造方案:引入BluetoothGattPool与DeviceConnection状态类
第一步:定义连接状态容器DeviceConnection
public class DeviceConnection { public final String deviceAddress; public BluetoothGatt gatt; public ConnectionState state; // CONNECTING, CONNECTED, DISCONNECTED public long lastActiveTime; // 用于空闲断开检测 public DeviceConnection(String address) { this.deviceAddress = address; this.state = ConnectionState.DISCONNECTED; this.lastActiveTime = System.currentTimeMillis(); } public enum ConnectionState { DISCONNECTED, CONNECTING, CONNECTED, FAILED } }第二步:构建连接池BluetoothGattPool(单例)
public class BluetoothGattPool { private static final int MAX_CONNECTIONS = 8; private final Map<String, DeviceConnection> mConnections = new ConcurrentHashMap<>(); private final BluetoothAdapter mBluetoothAdapter; public BluetoothGattPool(BluetoothAdapter adapter) { this.mBluetoothAdapter = adapter; } public DeviceConnection getOrCreate(String address) { return mConnections.computeIfAbsent(address, DeviceConnection::new); } public void connect(String address, BluetoothGattCallback callback) { DeviceConnection conn = getOrCreate(address); if (conn.state == DeviceConnection.ConnectionState.CONNECTED) { // 已连接,直接返回 return; } BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); conn.gatt = device.connectGatt(/* context */, false, callback); conn.state = DeviceConnection.ConnectionState.CONNECTING; conn.lastActiveTime = System.currentTimeMillis(); } public void disconnect(String address) { DeviceConnection conn = mConnections.get(address); if (conn != null && conn.gatt != null) { conn.gatt.disconnect(); conn.gatt.close(); conn.gatt = null; conn.state = DeviceConnection.ConnectionState.DISCONNECTED; } } public void cleanupIdleConnections(long idleMs) { long now = System.currentTimeMillis(); mConnections.values().removeIf(conn -> conn.state == DeviceConnection.ConnectionState.DISCONNECTED && (now - conn.lastActiveTime) > idleMs ); } }第三步:改造BluetoothLeService使用连接池
// 在 BluetoothLeService 中替换原有 mBluetoothGatt private BluetoothGattPool mGattPool; @Override public void onCreate() { super.onCreate(); mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); mGattPool = new BluetoothGattPool(mBluetoothAdapter); } // connect() 方法改为 public boolean connect(String address) { mGattPool.connect(address, mGattCallback); return true; } // 在 mGattCallback 中,所有回调方法增加设备地址参数 private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { String address = gatt.getDevice().getAddress(); DeviceConnection conn = mGattPool.getOrCreate(address); if (newState == BluetoothProfile.STATE_CONNECTED) { conn.state = DeviceConnection.ConnectionState.CONNECTED; conn.gatt = gatt; // 确保引用最新 gatt.discoverServices(); } // 广播时带上 address Intent intent = new Intent(ACTION_GATT_CONNECTED); intent.putExtra(EXTRA_DEVICE_ADDRESS, address); LocalBroadcastManager.getInstance(mService).sendBroadcast(intent); } };5.3 UI 层适配:DeviceControlFragment支持多设备 Tab
原 Demo 的DeviceControlFragment是单设备视图。改造后需支持动态 Tab:
// 在 DeviceScanActivity 中,点击设备后启动新 Fragment 并传入 address Bundle args = new Bundle(); args.putString("device_address", device.getAddress()); DeviceControlFragment fragment = new DeviceControlFragment(); fragment.setArguments(args); getSupportFragmentManager().beginTransaction() .replace(R.id.fragment_container, fragment) .addToBackStack(null) .commit();DeviceControlFragment的onCreate()中获取地址并注册专属广播:
@Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); mDeviceAddress = getArguments().getString("device_address"); // 注册只监听本设备的广播 IntentFilter filter = new IntentFilter(); filter.addAction(BluetoothLeService.ACTION_GATT_CONNECTED); filter.addAction(BluetoothLeService.ACTION_GATT_DISCONNECTED); filter.addAction(BluetoothLeService.ACTION_GATT_SERVICES_DISCOVERED); filter.addAction(BluetoothLeService.ACTION_DATA_AVAILABLE); // 添加地址过滤 filter.addCategory(mDeviceAddress); LocalBroadcastManager.getInstance(getContext()).registerReceiver(mGattUpdateReceiver, filter); }从那以后我每次接手 BLE 项目,第一件事就是删掉
mBluetoothGatt单例,强制走BluetoothGattPool。哪怕当前需求只连一台设备,这个抽象层能帮你省下后期加多设备时 80% 的重构时间。连接池不是过度设计,是 BLE 开发的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取