简介这份资源面向Android蓝牙开发初学者聚焦BLE蓝牙4.2的扫描、连接与数据收发三大核心操作通过精简后的示例代码帮助新手快速理解低功耗蓝牙通信流程可用于智能硬件调试、透传模块对接等场景。压缩包共1244个文件整体约9.24MB以flat、dex、class等编译产物为主辅以json配置、xml布局、java源码及少量png图片与properties属性文件完整保留了工程构建与运行所需的目录结构便于直接导入Android Studio参考。目前已有509人学习下载说明其在入门群体中具有一定参考价值。读者可从中获取BLE扫描与连接的核心实现思路、数据透传的简化写法以及工程配置与依赖组织的参考范例配合作者博文中的BLE蓝牙4.2数据透传操作介绍能更快上手并排查常见连接与收发问题。1. 从一份蓝牙 4.2 源码包说起Android BLE 开发到底难在哪如果你做过 Android BLE 开发大概率经历过这样的场景设备扫描到了连上了但读写特征值的时候要么超时要么回调不触发要么在某个机型上一切正常、换个机型就彻底失灵。Android 蓝牙 4.2 这套协议栈从 API 18 引入 BLE 支持开始到 4.2 版本在连接参数、数据长度扩展、隐私机制上做了不少调整但碎片化问题一直没消停过。这份 bluetooth4.zip 就是一套围绕 Android BLE 4.2 的完整源码包覆盖了扫描、连接、服务发现、特征值读写、通知订阅、MTU 协商、广播解析这些核心链路。它适合两类人一是刚接触 BLE 想找一个能跑通的参考实现二是已经踩过坑但想看看别人怎么处理连接状态机和线程调度的老手。下面我按实际拆包和复现的顺序把这份资源从结构到用法到坑点讲透。2. 拆开 bluetooth4.zip模块划分与核心类职责2.1 包结构与关键文件清单拿到压缩包后先别急着导入 IDE用文件管理器看一眼目录结构心里有个数。典型的 BLE 4.2 项目会按功能分层而不是把所有逻辑塞进一个 Activity。这份源码的包结构大致如下目录/文件职责是否可独立替换ble/BluetoothLeService.java连接管理、GATT 回调分发、广播接收核心不建议直接换ble/ScanManager.java扫描调度、过滤规则、超时控制可替换为自定义扫描策略ble/GattAttributes.javaUUID 常量表、服务与特征值映射按项目需求增删ble/BleOperationQueue.java读写通知的串行队列建议保留BLE 操作必须串行ui/DeviceScanActivity.java扫描界面、列表展示、权限请求可替换为你的 UIui/DeviceControlActivity.java连接后操作界面、数据展示可替换utils/BleUtils.java字节转换、CRC 校验、日志封装按需扩展AndroidManifest.xml权限声明、Service 注册必须检查这个划分的好处是连接逻辑和 UI 完全解耦BluetoothLeService继承自Service通过 Binder 或广播把状态抛给上层。我一般会先看BluetoothLeService里的onConnectionStateChange和onCharacteristicChanged两个回调它们决定了整个数据链路的稳定性。2.2 连接状态机与 GATT 回调链路BLE 连接不是一次性的connect()就完事它有一个明确的状态迁移过程。这份源码里用了一个枚举来管理// BluetoothLeService.java 中的状态定义 public enum ConnectionState { DISCONNECTED, // 初始状态或断开后 CONNECTING, // 调用 connectGatt 之后 CONNECTED, // onConnectionStateChange 返回 STATE_CONNECTED DISCOVERING, // discoverServices 进行中 READY // 服务发现完成可以读写 }状态迁移的触发点全部来自BluetoothGattCallback。关键回调有三个onConnectionStateChange负责连接和断开onServicesDiscovered负责服务发现完成onCharacteristicChanged负责通知数据到达。很多新手会忽略onServicesDiscovered的status参数直接开始读写结果就是GATT_ERROR或者特征值找不到。正确的做法是Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status BluetoothGatt.GATT_SUCCESS) { // 先遍历服务找到目标 Service for (BluetoothGattService service : gatt.getServices()) { if (service.getUuid().equals(UUID.fromString(TARGET_SERVICE_UUID))) { // 再找特征值确认属性 BluetoothGattCharacteristic ch service.getCharacteristic( UUID.fromString(TARGET_CHAR_UUID)); if (ch ! null) { // 根据属性决定是读、写还是订阅 enableNotification(gatt, ch); } } } mConnectionState ConnectionState.READY; } else { // 服务发现失败常见于连接参数不稳定 disconnect(); } }这段代码的逻辑是服务发现成功后先按 UUID 定位目标服务再定位特征值最后根据特征值属性决定操作类型。参数说明TARGET_SERVICE_UUID和TARGET_CHAR_UUID需要替换成你设备实际的 UUID通常从设备厂商的协议文档里拿。enableNotification内部会先写CLIENT_CHARACTERISTIC_CONFIG描述符再设置setCharacteristicNotification顺序反了通知不会生效。2.3 扫描与广播解析的线程模型扫描这块源码里用了ScanManager单独封装而不是直接在 Activity 里调startLeScan。原因很简单扫描是耗电大户必须能随时停而且 Android 5.0 之后推荐用BluetoothLeScanner配合ScanSettings和ScanFilter。这份源码同时兼容了新旧 API通过Build.VERSION.SDK_INT做分支。// ScanManager.java 中的扫描启动逻辑 public void startScan(ScanCallback callback) { if (Build.VERSION.SDK_INT Build.VERSION_CODES.LOLLIPOP) { ScanSettings settings new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 高功耗但响应快 .setReportDelay(0) // 立即上报不批量 .build(); ScanFilter filter new ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(TARGET_SERVICE_UUID)) .build(); ListScanFilter filters new ArrayList(); filters.add(filter); mBluetoothLeScanner.startScan(filters, settings, callback); } else { // 旧 API 兼容注意这里没有过滤需要在回调里手动判断 mBluetoothAdapter.startLeScan(mLeScanCallback); } }参数说明SCAN_MODE_LOW_LATENCY适合前台扫描功耗高但能快速发现设备如果是在后台长时间扫描换成SCAN_MODE_LOW_POWER。setReportDelay(0)表示每个广播包立即回调如果设成大于 0 的值系统会批量上报适合做数据聚合但不适合实时性要求高的场景。ScanFilter按服务 UUID 过滤能大幅减少无关回调但前提是设备广播里确实带了服务 UUID有些设备只在扫描响应里带这时候过滤器会失效需要改成addScanFilter或者干脆不过滤、在回调里手动判断。3. 跑通第一个 BLE 读写从权限到数据回传的完整链路3.1 权限声明与运行时请求Android 6.0 之后蓝牙扫描需要位置权限因为 BLE 广播可以被用来推断位置。很多人只加了BLUETOOTH和BLUETOOTH_ADMIN结果扫描回调一个都不触发查半天以为是硬件问题。这份源码的AndroidManifest.xml里声明了uses-permission android:nameandroid.permission.BLUETOOTH/ uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN/ uses-permission android:nameandroid.permission.ACCESS_COARSE_LOCATION/ uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION/注意Android 10 之后还需要ACCESS_BACKGROUND_LOCATION如果要在后台扫描但这份源码主要面向前台场景所以没加。运行时请求的代码在DeviceScanActivity的onCreate里if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_LOCATION); }逻辑说明只在权限未授予时请求避免重复弹窗。参数REQUEST_LOCATION是自定义的请求码在onRequestPermissionsResult里判断。如果用户拒绝了后续扫描会静默失败所以最好在拒绝后给一个提示引导去设置页手动开启。3.2 连接与 MTU 协商的实操步骤连接设备用device.connectGatt(context, false, gattCallback)第二个参数autoConnect设为false表示直接连接设为true表示等待设备广播后再连后者适合设备已经配对但不在范围内的场景。连接成功后第一件事不是读写而是协商 MTU。默认 MTU 是 23 字节实际可用载荷 20 字节如果你要传的数据超过这个长度必须协商。// 连接成功后请求 MTU Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState BluetoothProfile.STATE_CONNECTED) { // 请求 MTUAndroid 5.0 以上支持 if (Build.VERSION.SDK_INT Build.VERSION_CODES.LOLLIPOP) { gatt.requestMtu(247); // 请求 247实际返回可能不同 } gatt.discoverServices(); } } Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { if (status BluetoothGatt.GATT_SUCCESS) { // 实际 MTU 可能小于请求值以回调为准 mMaxPayload mtu - 3; // 减去 ATT 头 } }参数说明requestMtu(247)是常见做法因为 BLE 4.2 支持数据长度扩展最大可以到 251 字节。但实际能协商到多少取决于设备端和手机端双方的支持情况所以必须以onMtuChanged回调的mtu为准。mMaxPayload mtu - 3是因为 ATT 协议头占 3 字节。如果协商失败就退回默认的 20 字节分包发送。3.3 特征值读写与通知订阅的代码模板读写特征值是 BLE 开发里最频繁的操作但也是最容易出问题的地方。这份源码用了一个操作队列来保证串行执行因为 BLE 协议栈不支持并发操作同时发多个请求会导致回调错乱。// BleOperationQueue.java 中的写操作 public void writeCharacteristic(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] data) { // 入队保证串行 mQueue.add(() - { characteristic.setValue(data); characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); boolean success gatt.writeCharacteristic(characteristic); if (!success) { // 写失败可能是队列满了或者连接断了 notifyWriteFailed(); } }); processNext(); } // 通知订阅 public void enableNotification(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { // 先写描述符 BluetoothGattDescriptor descriptor characteristic.getDescriptor( UUID.fromString(00002902-0000-1000-8000-00805f9b34fb)); if (descriptor ! null) { descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(descriptor); } // 再设置本地通知开关 gatt.setCharacteristicNotification(characteristic, true); }逻辑说明写操作入队后由processNext逐个执行每个操作完成后在对应的回调里调用processNext继续下一个。参数说明WRITE_TYPE_DEFAULT表示带响应的写设备会回一个写响应如果不需要响应用WRITE_TYPE_NO_RESPONSE速度更快但可靠性低。通知订阅的顺序必须是先写描述符再setCharacteristicNotification反过来在某些机型上不生效。描述符 UUID00002902是标准值所有 BLE 设备都一样。4. 避坑与排查BLE 4.2 开发中最容易翻车的五个点4.1 扫描不到设备回调静默现象调用startScan后没有任何回调日志里也看不到广播包。原因通常是位置权限没给或者蓝牙适配器状态不对。解决先检查BluetoothAdapter.getDefaultAdapter()是否为 null再确认isEnabled()返回 true最后检查位置权限是否授予。Android 12 之后还需要BLUETOOTH_SCAN和BLUETOOTH_CONNECT权限如果目标版本是 12 以上必须在 Manifest 里加上并做运行时请求。4.2 连接成功但服务发现失败现象onConnectionStateChange返回STATE_CONNECTED但onServicesDiscovered的status不是GATT_SUCCESS或者根本不回调。原因可能是连接参数不稳定或者设备端在连接后需要延迟才能响应服务发现。解决在STATE_CONNECTED之后延迟 200 到 500 毫秒再调discoverServices()给协议栈一点时间。另外有些设备需要先配对才能发现服务这时候要检查device.getBondState()。4.3 通知订阅成功但收不到数据现象setCharacteristicNotification返回 true描述符也写了但onCharacteristicChanged不触发。原因通常是描述符写失败或者特征值本身不支持通知。解决在onDescriptorWrite回调里确认status是GATT_SUCCESS如果不是说明描述符写失败可能是 MTU 不够或者连接已断。另外检查特征值的属性是否包含PROPERTY_NOTIFY或PROPERTY_INDICATE如果不包含订阅永远不会生效。4.4 多线程并发操作导致回调错乱现象同时发起读和写结果读的回调里收到了写的数据或者回调顺序完全乱掉。原因就是 BLE 协议栈不支持并发所有 GATT 操作必须串行。解决用操作队列每个操作完成后在回调里触发下一个。这份源码的BleOperationQueue就是干这个的。如果你自己写记住一个原则任何时候只有一个 GATT 操作在飞行中。4.5 断开后重连失败报 GATT_ERROR 133现象第一次连接正常断开后再连就报 133或者一直连不上。原因通常是 GATT 资源没释放干净或者手机端缓存了旧的服务列表。解决每次断开后调用gatt.close()而不是只调disconnect()close()会释放底层资源。另外在重连之前调BluetoothGatt.refresh()清缓存但这个方法是隐藏 API需要通过反射调用而且不是所有机型都支持。更稳妥的做法是断开后等 500 毫秒再重连给系统一点清理时间。5. 进阶技巧用日志和状态机把 BLE 调试变成可复现的流程BLE 调试最头疼的是“玄学”问题——同样的代码这台手机行那台不行今天行明天不行。要把它变成可复现的流程我的习惯是两件事第一把所有 GATT 回调的status和newState都打日志包括时间戳第二用一个显式的状态机来驱动连接流程而不是靠回调里到处写 if-else。这份源码里有一个BleLog工具类封装了Log.d并加了统一前缀方便在 Logcat 里过滤。我一般会再加一个环形缓冲区把最近 200 条 BLE 日志存在内存里出问题的时候直接导出比翻 Logcat 快得多。// BleLog.java 中的环形缓冲 public class BleLog { private static final int MAX_ENTRIES 200; private static final LinkedListString sBuffer new LinkedList(); public static void d(String tag, String msg) { String entry System.currentTimeMillis() [ tag ] msg; synchronized (sBuffer) { if (sBuffer.size() MAX_ENTRIES) { sBuffer.removeFirst(); } sBuffer.addLast(entry); } Log.d(tag, msg); } public static String dump() { synchronized (sBuffer) { StringBuilder sb new StringBuilder(); for (String s : sBuffer) { sb.append(s).append(\n); } return sb.toString(); } } }逻辑说明sBuffer用LinkedList实现 FIFO超过 200 条就移除最旧的。dump()方法可以在出问题时一次性导出所有日志。参数说明MAX_ENTRIES可以根据需要调整200 条大概覆盖最近几分钟的操作足够定位大部分问题。另一个技巧是状态机驱动。不要在各个回调里散落if (mState CONNECTED)这样的判断而是把状态迁移集中在一个方法里private void transitionTo(ConnectionState newState) { BleLog.d(TAG, State: mState - newState); mState newState; switch (newState) { case CONNECTING: // 发起连接 break; case CONNECTED: // 请求 MTU 和服务发现 break; case READY: // 开始读写 break; case DISCONNECTED: // 清理资源 break; } }这样每次状态变化都有日志出问题的时候一眼就能看出卡在哪一步。从那以后我每次调 BLE 都强制走一遍这个状态机加日志的流程再也没有出现过“不知道卡在哪”的情况。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?