简介:本资源是基于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. 一个连不上设备的 BLE Demo,反而教会我怎么真正看懂 Android 蓝牙通信全链路
你有没有试过:下载了 Google 官方 BLE Demo(BluetoothLeGatt),照着 README 点开 App,界面上“SCAN”按钮按下去——列表空空如也?手机明明能连上智能手环、耳机,但这个 Demo 就是扫不到隔壁工位那台正在广播的开发板?别急,这不是你手机坏了,也不是 Demo 写错了,而是它根本就不是个“开箱即用”的玩具,而是一份带完整注释的 BLE 协议教学源码包。它不帮你封装权限、不自动适配 Android 12+ 的后台扫描限制、不隐藏 GATT 层的繁琐回调嵌套——它把从BluetoothAdapter初始化、到ScanCallback触发、再到BluetoothGatt连接后 discoverServices() 的每一步“血肉”都摊开给你看。适合谁?适合正卡在“能扫到设备但连不上”“连上了但读不出 characteristic 值”“服务 UUID 对得上却找不到对应 characteristic”的 Android 中级开发者;也适合刚学完《BLE 4.0 协议栈》理论、急需一份可调试、可断点、可改参数的真实工程来验证理解的人。这不是蓝牙入门课件,这是你调试 BLE 设备时,真会打开并逐行加 Log 的那个项目。
2. 项目结构与技术栈:从 build.gradle 到 res/layout,看清官方 Demo 的骨架
2.1 目录结构:精简但绝不省略关键层
这个 Demo 是典型的 Android Studio 单 Module 工程,没有复杂分包,所有核心逻辑集中在app/src/main/下。它的结构不是为“大项目”设计的,而是为“讲清楚 BLE 生命周期”服务的:
app/ ├── src/main/ │ ├── java/com/example/android/bluetoothlegatt/ │ │ ├── MainActivity.java // 主界面:启动扫描、展示设备列表、触发连接 │ │ ├── DeviceScanActivity.java // 扫描主控:管理 BluetoothAdapter、ScanCallback、扫描状态 │ │ ├── DeviceControlActivity.java // 连接后界面:操作 GATT 服务、读写 characteristic │ │ ├── BleService.java // 核心后台服务:封装 BluetoothGatt 回调、重连逻辑、数据分发 │ │ └── Utils.java // 工具类:UUID 字符串转换、MAC 地址校验、BLE 状态码转义 │ ├── res/ │ │ ├── layout/ │ │ │ ├── activity_main.xml // 启动页:SCAN 按钮 + 设备列表 RecyclerView │ │ │ ├── device_scan.xml // 扫描页:实时设备列表 + 扫描控制开关 │ │ │ └── device_control.xml // 控制页:服务/characteristic 展开树 + 读/写/通知开关 │ │ └── values/strings.xml // 关键字符串:BLE_STATE_CONNECTING、GATT_SUCCESS 等状态提示 │ └── AndroidManifest.xml // 权限声明、Activity 声明、服务声明 └── build.gradle // 构建配置:targetSdkVersion=33、compileSdk=34、依赖 androidx.core:core-ktx注意:它没有使用 Jetpack Compose,全部基于View+RecyclerView实现 UI;没有引入 RxJava 或 Coroutines 封装回调,所有 BLE 操作都通过原生Handler或runOnUiThread()切回主线程;没有抽象出 Repository 或 DataStore,设备列表直接存在ArrayList<BluetoothDevice>里。这种“原始感”恰恰是它教学价值所在——你不会被框架层遮蔽,一眼就能看到onConnectionStateChange()是怎么被触发的,onServicesDiscovered()的status参数为什么有时是BluetoothGatt.GATT_FAILURE。
2.2 技术栈选型:为什么是这些 API?而不是别的?
BluetoothAdapter+BluetoothLeScanner:Android 5.0(API 21)起官方推荐的低功耗扫描方式,替代已废弃的startDiscovery()。Demo 中DeviceScanActivity使用ScanCallback接收扫描结果,而非轮询,这是功耗优化的关键。BluetoothGatt+BluetoothGattCallback:GATT 客户端核心类。Demo 将其生命周期完全绑定到BleService中,避免 Activity 重建导致 GATT 连接中断。BleService通过LocalBroadcastManager向 UI 发送状态变更,解耦清晰。androidx.core:core-ktx:仅用于ContextCompat.checkSelfPermission()和ActivityResultLauncher(AndroidX Activity Result API),处理运行时权限申请,兼容 Android 6.0+。targetSdkVersion = 33:明确支持 Android 13 的后台位置权限变更(需声明ACCESS_FINE_LOCATION并在AndroidManifest.xml中添加android:foregroundServiceType="location"),这是很多开发者翻车的第一关。
提示:这个 Demo 的
build.gradle里没有implementation 'com.jakewharton:butterknife'或viewBinding true,UI 绑定全靠findViewById()。这不是过时,而是刻意为之——让你聚焦在 BLE 流程本身,而不是 View 绑定语法。
2.3 权限与清单配置:少一行,整个流程就卡死
AndroidManifest.xml是 BLE 功能的“准入证”,漏掉任何一项,App 都会在某一步静默失败。Demo 中的关键配置如下:
<!-- 必须声明蓝牙权限 --> <uses-permission android:name="android.permission.BLUETOOTH" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" /> <!-- Android 12+ 强制要求 --> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <!-- 扫描需要位置权限(即使设备不提供位置信息) --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <!-- Android 12+ 后台扫描需声明此权限 --> <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" /> <!-- 声明蓝牙硬件特性(非必需,但建议) --> <uses-feature android:name="android.hardware.bluetooth_le" android:required="true" /> <!-- Service 声明,BleService 是 BLE 通信的核心载体 --> <service android:name=".BleService" android:enabled="true" android:exported="false" />特别注意BLUETOOTH_CONNECT和BLUETOOTH_SCAN:它们是 Android 12(API 31)引入的运行时危险权限,必须动态申请,且不能和ACCESS_FINE_LOCATION合并在一个requestPermissions()调用中——这是官方 Demo 在DeviceScanActivity.java中用ActivityResultLauncher分两步申请的根本原因。
3. 核心模块拆解:从扫描到连接,逐行解析 BLE 通信四步链路
3.1 扫描启动:BluetoothLeScanner如何发起一次有效扫描
扫描不是“按个按钮就出列表”,它是一次有策略、有参数、有回调的异步操作。Demo 中DeviceScanActivity.java的scanLeDevice()方法是入口:
private void scanLeDevice(final boolean enable) { if (enable) { // 1. 清空旧设备列表 mLeDevices.clear(); mDeviceListAdapter.notifyDataSetChanged(); // 2. 构建扫描设置:低功耗模式、不扫描已知设备 ScanSettings settings = new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) // 关键:省电模式 .setReportDelay(0) // 立即上报,不聚合 .build(); // 3. 构建过滤器:只扫特定服务 UUID(可选,提高效率) List<ScanFilter> filters = new ArrayList<>(); if (!mFilterUuids.isEmpty()) { for (UUID uuid : mFilterUuids) { filters.add(new ScanFilter.Builder().setServiceUuid(ParcelUuid.fromString(uuid.toString())).build()); } } // 4. 启动扫描,传入回调 mBluetoothLeScanner.startScan(filters, settings, mScanCallback); mScanButton.setText(R.string.stop_scan); mScanning = true; } else { mBluetoothLeScanner.stopScan(mScanCallback); mScanButton.setText(R.string.scan); mScanning = false; } }关键参数说明:
SCAN_MODE_LOW_POWER:适用于长时间后台扫描,间隔约 1.28 秒,功耗最低。若需快速发现设备(如配网场景),应换为SCAN_MODE_BALANCED(约 200ms 间隔)或SCAN_MODE_LOW_LATENCY(约 5ms,耗电高)。setReportDelay(0):设为 0 表示“发现即报”,设为 >0 则会聚合多个扫描结果再回调,减少主线程压力,但会增加发现延迟。ScanFilter:不是必须,但强烈建议。若你的设备只广播某个特定服务(如0000180F-0000-1000-8000-00805F9B34FB电池服务),加上 filter 可大幅降低 CPU 占用和误报率。注意:filter 的 UUID 必须是 128-bit 完整格式,不能只写0x180F!
3.2 扫描回调:ScanCallback如何解析广播数据
mScanCallback是ScanCallback的匿名实现,它接收每一个被扫描到的设备:
private ScanCallback mScanCallback = new ScanCallback() { @Override public void onScanResult(int callbackType, ScanResult result) { BluetoothDevice device = result.getDevice(); // 1. 过滤:跳过空设备、已连接设备、信号太弱设备(<-70dBm) if (device == null || device.getAddress() == null || result.getRssi() < -70) return; // 2. 解析广播数据:获取名称、服务 UUID 列表 ScanRecord scanRecord = result.getScanRecord(); String deviceName = scanRecord != null ? scanRecord.getDeviceName() : "Unknown"; List<ParcelUuid> serviceUuids = scanRecord != null ? scanRecord.getServiceUuids() : null; // 3. 去重:用 MAC 地址作为唯一 key if (!mLeDevices.containsKey(device.getAddress())) { mLeDevices.put(device.getAddress(), device); mDeviceListAdapter.addDevice(device, deviceName, result.getRssi(), serviceUuids); } } @Override public void onBatchScanResults(List<ScanResult> results) { // 当 setReportDelay > 0 时触发,此处为空实现 } @Override public void onScanFailed(int errorCode) { Log.e(TAG, "Scan failed with code: " + errorCode); // errorCode = 1 表示扫描已在进行;2 表示硬件忙;3 表示参数非法 // 此处应弹 Toast 或更新 UI 状态 } };血泪经验:onScanResult()中result.getScanRecord()可能为null!尤其当设备未在广播数据中包含AD Type 0x09(Complete Local Name)或0x02/0x03(Incomplete/Complete List of 16-bit Service Class UUIDs)时。不要假设scanRecord一定有值,务必判空。另外,result.getRssi()是信号强度,单位 dBm,负值越小(如 -90)表示信号越差,-30 是极近距离,-70 是中等距离,-90 以下基本不可靠。
3.3 连接建立:BluetoothGatt的三次握手与状态机
点击列表中的设备,触发DeviceScanActivity的connectToDevice(),最终调用BleService.connect():
public boolean connect(final String address) { if (mBluetoothAdapter == null || address == null) { Log.w(TAG, "BluetoothAdapter not initialized or unspecified address."); 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; } // 1. 调用 device.connectGatt(),返回 BluetoothGatt 实例 // autoConnect=false 表示立即连接(非后台自动重连) mBluetoothGatt = device.connectGatt(this, false, mGattCallback); Log.d(TAG, "Trying to create a new connection."); return true; }mGattCallback是BluetoothGattCallback的实现,它定义了连接全过程的状态回调:
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."); broadcastUpdate(ACTION_GATT_CONNECTED); // 2. 连接成功后,必须主动调用 discoverServices() gatt.discoverServices(); } else if (newState == BluetoothProfile.STATE_DISCONNECTED) { Log.i(TAG, "Disconnected from GATT server."); broadcastUpdate(ACTION_GATT_DISCONNECTED); } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { Log.i(TAG, "Services discovered."); broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED); // 3. 此时才能安全地 getServices()、getCharacteristics() displayGattServices(gatt.getServices()); } else { Log.w(TAG, "Services discovery failed with status: " + status); } } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic); } } };玄学时刻:onConnectionStateChange()的status参数永远为 0(GATT_SUCCESS),它只表示“连接请求已发出”,不代表物理链路已通!真正的连接确认在newState == STATE_CONNECTED时才发生。而onServicesDiscovered()的status才是关键——GATT_SUCCESS表示服务发现完成,GATT_FAILURE表示失败(常见于设备未响应、超时、MTU 不匹配)。很多开发者在这里卡住,以为连接成功就万事大吉,其实discoverServices()才是 GATT 通信的真正起点。
3.4 服务与特征值:如何从 UUID 字符串定位到可读写的 characteristic
displayGattServices()方法将BluetoothGattService列表解析为 UI 可展示的树形结构:
private void displayGattServices(List<BluetoothGattService> gattServices) { if (gattServices == null) return; String uuid = null; String unknownServiceString = getResources().getString(R.string.unknown_service); String unknownCharaString = getResources().getString(R.string.unknown_characteristic); // 遍历每个服务 for (BluetoothGattService gattService : gattServices) { uuid = gattService.getUuid().toString(); // 1. 显示服务 UUID(如 0000180f-0000-1000-8000-00805f9b34fb) Log.d(TAG, "Service: " + uuid); // 2. 遍历该服务下的所有 characteristic List<BluetoothGattCharacteristic> gattCharacteristics = gattService.getCharacteristics(); for (BluetoothGattCharacteristic gattCharacteristic : gattCharacteristics) { uuid = gattCharacteristic.getUuid().toString(); int charaProp = gattCharacteristic.getProperties(); // 3. 判断该 characteristic 支持哪些操作 if ((charaProp & BluetoothGattCharacteristic.PROPERTY_READ) > 0) { Log.d(TAG, " Characteristic READ: " + uuid); } if ((charaProp & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) { Log.d(TAG, " Characteristic WRITE: " + uuid); } if ((charaProp & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { Log.d(TAG, " Characteristic NOTIFY: " + uuid); } } } }UUID 匹配技巧:官方 Demo 中DeviceControlActivity的readCharacteristic()方法,是通过BluetoothGattService的getCharacteristic(UUID)来查找的:
BluetoothGattCharacteristic characteristic = gattService.getCharacteristic(UUID.fromString("00002a19-0000-1000-8000-00805f9b34fb")); // 电池电量 if (characteristic != null) { mBluetoothGatt.readCharacteristic(characteristic); }注意:UUID.fromString()的字符串必须是标准 128-bit 格式(8-4-4-4-12),不能是0x2A19这样的 16-bit 缩写。如果设备使用的是标准 SIG UUID,可以查 Bluetooth SIG Assigned Numbers 文档,例如0x2A19对应电池电量,完整 UUID 是00002A19-0000-1000-8000-00805F9B34FB。
4. 避坑指南:那些让 BLE Demo “连不上、读不出、闪退”的真实翻车现场
4.1 现象:App 启动后 SCAN 按钮点击无反应,Logcat 无任何 BLE 日志
原因:AndroidManifest.xml中漏掉了BLUETOOTH_SCAN或BLUETOOTH_CONNECT权限,或targetSdkVersion低于 31 但代码中仍尝试申请新权限。Android 12+ 会静默拒绝扫描请求,不抛异常,也不打日志。
解决:
- 检查
AndroidManifest.xml是否包含<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />和<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />; - 确认
build.gradle中targetSdkVersion >= 31; - 在
DeviceScanActivity.java的onCreate()中,加一行Log.d(TAG, "Target SDK: " + Build.VERSION.SDK_INT);确认版本; - 若
targetSdkVersion < 31,则必须降级使用BLUETOOTH_ADMIN,并移除新权限申请代码。
4.2 现象:能扫到设备,点击连接后onConnectionStateChange()立即返回STATE_DISCONNECTED
原因:设备端未正确进入可连接状态(如广播间隔过长、未使能可连接标志位),或手机蓝牙未开启,或connectGatt()时autoConnect=true导致后台连接失败。
解决:
- 用 nRF Connect 等专业工具确认该设备是否真的处于可连接状态(nRF Connect 中显示 “Connectable”);
- 在
connect()方法中,强制使用autoConnect=false(Demo 默认就是 false,但有人会误改); - 在
onConnectionStateChange()中,status != GATT_SUCCESS时,打印完整status值(如 133 表示 GATT 连接超时,常见于设备响应慢); - 终极排查:在
BleService.java的connect()方法开头,加Log.d(TAG, "Connecting to: " + address);,确认传入地址正确(MAC 地址是否含冒号?Demo 中BluetoothDevice.getAddress()返回的是AA:BB:CC:DD:EE:FF格式,不能直接拼接)。
4.3 现象:连接成功,onServicesDiscovered()被调用,但gattService.getCharacteristics()返回空列表
原因:设备端未在 GATT Server 中正确添加 characteristic,或 characteristic 的属性(Properties)未设置READ/WRITE/NOTIFY,或discoverServices()调用过早(在onConnectionStateChange()的STATE_CONNECTED分支外调用)。
解决:
- 用 nRF Connect 连接同一设备,查看其服务树是否正常展开——若 nRF 也看不到 characteristic,则是设备固件问题;
- 检查
onServicesDiscovered()的status是否为GATT_SUCCESS,若为GATT_FAILURE,则大概率是设备未响应; - 关键检查点:确保
discoverServices()是在onConnectionStateChange()的newState == STATE_CONNECTED分支内调用,且只调用一次。多次调用会导致状态混乱; - 在
displayGattServices()中,gattService.getCharacteristics()前加Log.d(TAG, "Service " + gattService.getUuid() + " has " + gattService.getCharacteristics().size() + " characteristics");,确认 size > 0。
4.4 现象:能读取 characteristic,但onCharacteristicRead()的status为GATT_READ_NOT_PERMITTED
原因:设备端该 characteristic 的属性未设置PROPERTY_READ,或设备固件中对该 characteristic 的读操作做了权限校验(如需先配对)。
解决:
- 用 nRF Connect 尝试读取同一 characteristic,若同样失败,则确认设备文档;
- 在
displayGattServices()中,打印charaProp的十进制值,并对照BluetoothGattCharacteristic.PROPERTY_*常量判断(如PROPERTY_READ = 1,PROPERTY_WRITE = 2,PROPERTY_NOTIFY = 16); - 若
charaProp & PROPERTY_READ == 0,则该 characteristic 确实不支持读,不要调用readCharacteristic(); - 若设备要求配对后才能读,需在
onConnectionStateChange()后,调用gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH)并等待配对完成(onCharacteristicRead()会等到配对后才回调)。
4.5 现象:Android 12+ 设备上,App 进入后台后扫描停止,前台也无法恢复
原因:Android 12+ 对后台位置权限管控极严,ACCESS_BACKGROUND_LOCATION权限未申请,或AndroidManifest.xml中Service未声明android:foregroundServiceType="location"。
解决:
- 在
AndroidManifest.xml中,为BleService添加android:foregroundServiceType="location"; - 在
DeviceScanActivity.java中,申请ACCESS_BACKGROUND_LOCATION权限(需用户手动授权,系统弹窗); - 更优方案:避免后台扫描,改为前台持续扫描(
ScanMode = LOW_LATENCY),或使用WorkManager+ForegroundService组合,在通知栏显示持续扫描状态; - 测试时,务必在设置中手动开启 App 的“后台位置权限”,系统设置路径:
设置 > 应用 > [你的App] > 权限 > 位置 > 允许后台使用位置。
5. 二次开发实战:如何把官方 Demo 改造成你的 BLE 设备调试器
5.1 改造目标:从“展示服务”到“发送自定义指令”
官方 Demo 的DeviceControlActivity只能读取预设 characteristic(如电池电量),无法发送任意 Hex 指令。我们要让它支持手动输入01 02 03这样的字节序列,并写入指定 characteristic。
第一步:修改device_control.xml,增加输入框和发送按钮
<EditText android:id="@+id/edit_hex_input" android:layout_width="match_parent" android:layout_height="wrap_content" android:hint="Enter hex bytes, e.g. 01 02 03" android:inputType="textCapCharacters|textNoSuggestions" /> <Button android:id="@+id/btn_send_hex" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Send Hex" />第二步:在DeviceControlActivity.java中,绑定事件并解析 Hex
// 在 onCreate() 中 findViewById(R.id.btn_send_hex).setOnClickListener(v -> { EditText hexInput = findViewById(R.id.edit_hex_input); String hexStr = hexInput.getText().toString().trim(); if (hexStr.isEmpty()) return; // 解析 "01 02 03" -> byte[]{0x01, 0x02, 0x03} byte[] data = hexStringToByteArray(hexStr); if (data == null) { Toast.makeText(this, "Invalid hex format", Toast.LENGTH_SHORT).show(); return; } // 获取当前选中的 characteristic(Demo 中已存为 mCharacteristic) if (mCharacteristic != null && (mCharacteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_WRITE) > 0) { mCharacteristic.setValue(data); mBluetoothGatt.writeCharacteristic(mCharacteristic); } }); // 工具方法:Hex 字符串转 byte[] private byte[] hexStringToByteArray(String s) { String[] parts = s.split("\\s+"); byte[] bytes = new byte[parts.length]; try { for (int i = 0; i < parts.length; i++) { bytes[i] = (byte) Integer.parseInt(parts[i], 16); } return bytes; } catch (NumberFormatException e) { return null; } }第三步:监听写入结果,增强反馈
在BleService.java的mGattCallback中,补充onCharacteristicWrite():
@Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { Log.d(TAG, "Write success to " + characteristic.getUuid()); broadcastUpdate(ACTION_DATA_WRITE_SUCCESS, characteristic); } else { Log.e(TAG, "Write failed with status: " + status); broadcastUpdate(ACTION_DATA_WRITE_FAILED, characteristic); } }然后在DeviceControlActivity.java的onReceive()中,接收ACTION_DATA_WRITE_SUCCESS并 Toast 提示。
5.2 改造目标:支持批量读取多个 characteristic,提升调试效率
设备常有多个 sensor 数据(温度、湿度、气压)分布在不同 characteristic 中。官方 Demo 每次只能读一个,来回点击太慢。我们改成一键读取所有PROPERTY_READ的 characteristic。
第一步:在DeviceControlActivity.java中,添加“Batch Read”按钮
<Button android:id="@+id/btn_batch_read" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Batch Read All Readable" />第二步:实现批量读取逻辑
findViewById(R.id.btn_batch_read).setOnClickListener(v -> { if (mBluetoothGatt == null) return; List<BluetoothGattService> services = mBluetoothGatt.getServices(); int total = 0; for (BluetoothGattService service : services) { for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { if ((characteristic.getProperties() & BluetoothGattCharacteristic.PROPERTY_READ) > 0) { mBluetoothGatt.readCharacteristic(characteristic); total++; } } } Toast.makeText(this, "Batch read started: " + total + " characteristics", Toast.LENGTH_SHORT).show(); });注意:Android BLE 栈对并发读操作有限制(通常 1~3 个),所以这里实际是串行触发。onCharacteristicRead()会按顺序回调,无需额外同步。
5.3 改造目标:持久化常用设备,避免每次重新扫描
每次调试都要重新扫描、点选,效率低下。我们可以将已连接过的设备 MAC 地址和名称存入SharedPreferences,下次启动时直接显示在列表顶部。
第一步:在DeviceScanActivity.java中,添加保存逻辑
// 在 onConnectionStateChange() 的 STATE_CONNECTED 分支中 if (newState == BluetoothProfile.STATE_CONNECTED) { // ...原有逻辑 saveConnectedDevice(device.getAddress(), device.getName()); } private void saveConnectedDevice(String address, String name) { SharedPreferences prefs = getSharedPreferences("ble_devices", MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putString(address, name); editor.apply(); }第二步:在onCreate()中,加载并显示已保存设备
private void loadSavedDevices() { SharedPreferences prefs = getSharedPreferences("ble_devices", MODE_PRIVATE); Map<String, ?> all = prefs.getAll(); for (Map.Entry<String, ?> entry : all.entrySet()) { String address = entry.getKey(); String name = (String) entry.getValue(); BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); if (device != null) { mLeDevices.put(address, device); mDeviceListAdapter.addDevice(device, name, -50, null); // RSSI 设为 -50(占位) } } }血泪教训:getRemoteDevice(address)在address格式错误(如多了一个空格)时会返回null,务必判空。另外,SharedPreferences存的是字符串,MAC 地址中不能有冒号(AA:BB:CC:DD:EE:FF),建议存为AABBCCDDEEFF格式,读取时再格式化。
5.4 改造目标:添加 MTU 协商,支持大数据包传输
某些设备(如固件升级)需要 >20 字节的 MTU。官方 Demo 默认 MTU 为 23,需手动协商。
第一步:在BleService.java的onServicesDiscovered()后,调用requestMtu()
@Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { // ...原有逻辑 // 请求 MTU 为 512(最大值,实际由设备决定) gatt.requestMtu(512); } } @Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { Log.d(TAG, "MTU changed to: " + mtu); // 此时可安全发送 >20 字节的数据 broadcastUpdate(ACTION_MTU_CHANGED, mtu); } }第二步:在DeviceControlActivity.java中,监听ACTION_MTU_CHANGED并更新 UI
private BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); if (ACTION_MTU_CHANGED.equals(action)) { int mtu = intent.getIntExtra(EXTRA_DATA, 23); TextView mtuView = findViewById(R.id.text_mtu); mtuView.setText("MTU: " + mtu); } } };从那以后我每次拿到一个新的 BLE 模块,第一件事就是把它连到 nRF Connect 上,确认服务树结构,再把它的 UUID 复制进 Demo 的mFilterUuids里,开启精准扫描;第二件事是打开BleService.java,在onConnectionStateChange()和onServicesDiscovered()里各加一行Log.d(),确保我能看见连接和发现的每一个状态码;第三件事是把Utils.java里的parseUUID()方法替换成自己的,支持0x180F这种缩写格式,省得每次去查 SIG 文档。这三步走完,90% 的 BLE 连接问题都能在 5 分钟内定位。希望帮到你。
本文还有配套的精品资源,点击获取