
简介面向Android中高级开发者的蓝牙调试助手完整源码包聚焦设备发现、RFCOMM连接、数据收发及BLE低功耗通信等核心场景既能作为实战教学范例也可直接改造成自定义调试工具。包内共53个文件以Java源码、XML布局与配置、class编译产物及jar依赖库为主附带APK安装包与工程配置文件整体仅116KB结构轻量便于快速查看代码逻辑文件明细涵盖21个class、9个xml、5个java等。已有1026人学习下载。通过研读工程可系统掌握BluetoothAdapter、BluetoothSocket等API用法、动态权限处理、广播接收器注册与多线程I/O管理同时借鉴其UI事件设计和异常处理思路从设备扫描配对到Socket连接与流数据读写再到异常回退与性能优化源码均有清晰呈现适合边读边改、梯度进阶对提升Android蓝牙应用开发能力很有帮助。1. 蓝牙调试助手这类源码值得拆的不是界面而是协议栈很多 Android 开发者拿到「蓝牙调试助手」类源码第一反应是看扫描列表怎么刷新、聊天式收发界面怎么写但真正有信息量的部分在蓝牙协议栈与业务代码的接缝处。系统蓝牙框架屏蔽了大部分底层细节可一旦你是做 IoT 设备调试、外设固件验证或者嵌入式联调就会立刻发现官方示例只覆盖连接和数据收发真正让你卡住的是 MTU 协商失败、连接参数不被外设接受、服务发现拿不到 128 位 UUID、断连后重连策略写得很粗暴这类问题。这份源码的价值不在于它是一个「能跑的 App」而是它把 BLE 的扫描、连接、服务发现、特征读写、通知订阅这一整条链路用最小代码量串了起来而且保留了 Log 输出和异常分支。对新手来说它是理解 BluetoothGattCallback 状态机的最佳范本对有经验的人而言它提供了一个可以快速改造成自动化测试工具的骨架。这篇文章就顺着这条链路把源码里最值得读的模块、参数和坑位逐个拆开。2. Android 蓝牙调试助手的框架选型经典蓝牙与 BLE 的分工2.1 为什么现代调试助手默认走 BLE而不是传统 socket RFCOMM蓝牙调试助手类源码通常会同时保留两套入口经典蓝牙的 BluetoothSocket 和低功耗蓝牙的 BluetoothGatt但重点投入一定在 BLE。原因很直接新出厂的物联网设备、传感器、穿戴外设基本都是 BLE 协议经典蓝牙在大数据量音频传输之外的存在感越来越弱。从源码结构上就能看出来经典蓝牙部分往往只有一个连接线程和一个 IO 线程而 BLE 部分会拆出 Scanner、Connector、ServiceDiscover、CharacteristicOperation 至少四个模块。做选型时有一个常见误区以为 BLE 和经典蓝牙在应用层可以共用一套收发逻辑。实际上完全不行。经典蓝牙走的是 Socket 流式读写收发模型是 InputStream/OutputStreamBLE 走的是 GATT 属性协议每次读写在属性层有最大长度限制通知数据也有 20 字节起步的 MTU 约束。调试助手源码里如果这两套 IO 逻辑混在一个类中后期处理分包和粘包会非常痛苦这不是代码风格问题是协议模型决定的。2.2 权限模型是源码里最容易过时的部分拿到一份蓝牙调试助手源码先别急着跑第一件事是检查权限适配。Android 6 之前只需要 BLUETOOTH 和 BLUETOOTH_ADMINAndroid 6 到 11 需要动态申请 ACCESS_FINE_LOCATIONAndroid 12 开始又拆出了 BLUETOOTH_SCAN、BLUETOOTH_CONNECT、BLUETOOTH_ADVERTISE 三个运行时权限。很多老源码只声明了位置权限在 Android 12 设备上会出现扫描不到设备、连接直接回 133 错误这类问题。以一份兼容到 Android 13 的源码为例必须在 AndroidManifest.xml 里补齐如下声明同时在代码里走运行时权限申请uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION android:maxSdkVersion30 /这里有一个值得注意的细节neverForLocation标志表示你的扫描结果不会用于推导物理位置。如果调试助手源码里声明了这个标志但代码中又把扫描到的 MAC 地址用于设备标识逻辑上是自洽的但如果你把扫描到的 RSSI 和 MAC 上传到了远程服务器做定位分析就必须去掉这个标志否则会被系统判定为权限滥用。权限申请顺序上先申请连接相关权限再申请扫描权限因为部分国产 ROM 对 BluetoothGatt.connect() 的权限校验比官方系统更严格。2.3 服务发现与 GATT 分层结构在代码中的组织方式源码中对 GATT 的封装通常包含四层BluetoothGatt 代表一个连接会话BluetoothGattService 代表外设暴露的一个服务BluetoothGattCharacteristic 是读写的基本单元BluetoothGattDescriptor 则用于配置 CCCD客户端特征配置描述符。调试助手的 UI 上呈现的「服务列表」本质上就是遍历gatt.services集合把每个 Service 下的 Characteristic 和 Descriptor 递归展开。我建议读源码时先看封装层有没有把 onServicesDiscovered 回调里的枚举逻辑和 UI 刷新解耦。实践中常见的问题是直接在回调线程里更新 ListView 或 RecyclerView导致列表刷新偶发崩溃。更稳的写法是把扫描结果和 GATT 回调都切到 Handler 或主线程再操作 UI源码里如果用了 runOnUiThread 包裹基本就对了。3. 源码主流程拆解从扫描到特征值读写的最小闭环3.1 用 BluetoothLeScanner 实现低功耗扫描的正确姿势扫描是调试助手源码里第一个需要认真看的模块。现代 Android 源码中已经很少直接用 startLeScan而是用 BluetoothLeScanner 配合 ScanFilter 和 ScanSettings。一个可复现的扫描实现长这样private void startBleScan() { BluetoothLeScanner scanner mBluetoothAdapter.getBluetoothLeScanner(); ScanFilter filter new ScanFilter.Builder() .setServiceUuid(ParcelUuid.fromString(0000fee7-0000-1000-8000-00805f9b34fb)) .build(); ScanSettings settings new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .setReportDelay(0) .build(); scanner.startScan(Collections.singletonList(filter), settings, mScanCallback); }ScanFilter 里填的是服务 UUID这是扫描逻辑里最关键的过滤条件。调试源码时如果外设支持多个服务建议只过滤最核心的那个主服务过滤条件越多漏报概率越高。ScanMode 的取值需要根据场景调整LOW_LATENCY 适合主动连接场景但耗电明显BALANCED 在多数调试场景够用LOW_POWER 用于后台扫描。ReportDelay 设为 0 表示有结果立即回调设成大于 0 的值则系统会批量上报批量模式适合需要统计 RSSI 分布的场景。回调接口onScanResult中拿到的ScanResult对象包含getRssi()和getScanRecord()其中getDevice()返回的不是一个稳定的设备标识因为 Android 6 之后扫描到的 MAC 地址是随机化的。调试助手源码如果在列表里把 MAC 当主键存储连续两次扫描同一外设会得到两个 ID这是一个必须做去重处理的坑点。去重逻辑我一般用设备地址加广播包中的 DeviceName 联合判断单纯用名称判断不稳定因为厂商之间对外设名称的处理差异很大。3.2 连接与状态机BluetoothGattCallback 里每个回调的含义连接模块是整个源码的骨架。看一份调试助手源码写得是否成熟不用看界面直接看 BluetoothGattCallback 里处理了哪些回调、每个回调里做了什么。一个合格的最小实现应该覆盖以下六个回调BluetoothGattCallback callback new BluetoothGattCallback() { Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { if (newState BluetoothProfile.STATE_CONNECTED) { gatt.discoverServices(); // 连接成功下一步一定是服务发现 } else if (newState BluetoothProfile.STATE_DISCONNECTED) { closeAndCleanup(gatt); } } Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status BluetoothGatt.GATT_SUCCESS) { updateServiceListUi(gatt.getServices()); } } Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value, int status) { appendHexLog(READ, formatBytes(value)); } Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { appendHexLog(WRITE, status status); } Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] value) { appendHexLog(NOTIFY, formatBytes(value)); } Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { Log.d(TAG, descriptor write: status); } };onConnectionStateChange里常见的一个问题是 newState 和 status 混为一谈。status 为 133 表示连接失败为 8 表示设备已断开而 newState 只有 0 和 2 两种取值。如果你在回调里只判断 newState 而忽略 status那么连接失败时也会走 STATE_CONNECTED 的分支导致 discoverServices 在无效连接上执行然后卡在 onServicesDiscovered 永远不回调。源码里如果看到这种错误逻辑重连机制的稳定性基本无从谈起。关于线程模型有一个必须遵守的约定所有gatt.writeCharacteristic()调用必须在主线程执行但回调可能出现在任意线程。源码中如果有在子线程里直接调用 BleGatt 写的操作大概率会在高并发写入时触发java.lang.IllegalStateException。固定做法是把写操作 post 到主线程的 Handler 中或者在 Application 层建立一个串行写入队列。3.3 特征值的读写与通知订阅源码里最实用的三个操作方法调试助手的核心价值在于三个操作读特征值、写特征值、开启通知。这三个操作对应到源码中通常封装成三个公开方法public boolean readCharacteristic(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { return gatt.readCharacteristic(characteristic); } public boolean writeCharacteristic(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, byte[] payload) { characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); characteristic.setValue(payload); return gatt.writeCharacteristic(characteristic); } public boolean enableNotification(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { BluetoothGattDescriptor descriptor characteristic.getDescriptor(UUID.fromString(00002902-0000-1000-8000-00805f9b34fb)); descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); return gatt.writeDescriptor(descriptor); }写操作注意setWriteType的设置。WRITE_TYPE_DEFAULT 表示带响应的写外设处理完会回 ACK适合需要确认的场景WRITE_TYPE_NO_RESPONSE 表示无响应写吞吐量高但外设不确认适合固件升级这类大数据场景。部分外设强制要求使用无响应写比如某些 Nordic 芯片的方案如果源码里写死了 DEFAULT 类型这些外设会直接忽略写入。开启通知时Notification 和 Indication 的区别在于后者每包数据都要求主机回复确认在信号差的环境中 Indication 丢包率更低但速度更慢。源码中如果两种模式都支持应该在 UI 上提供切换入口而不是写死某一种。4. MTU 协商、连接参数与抓包排错连上了却收不到数据的真相4.1 MTU 协商机制与 20 字节限制的来源刚接触 BLE 的人最容易困惑的一个现象是单次写入明明可以发 20 字节以上但外设就是只回前 20 字节。这个限制的根源是 4.0 协议默认 MTU 只有 23 字节扣除 3 字节属性头后有效载荷为 20 字节。Android 设备与外设建立连接后需要通过协商拿到更大的 MTU。Android 5.0 起官方提供了BluetoothGatt.requestMtu(int mtu)源码里正确用法是在服务发现完成后就发起协商gatt.requestMtu(247);MTU 协商是双向的。主机请求 247从机可能只支持 64 或 128最终以从机响应值为准。你可以通过onMtuChanged(BluetoothGatt gatt, int mtu, int status)拿到实际协商值并把数据包按这个值重新切分。这里有一个容易被忽略的细节MTU 协商是逐连接的不是全局配置每次重连后都必须重新 requestMtu。调试助手的发数据模块如果缓存了旧连接的 MTU 值重连之后没有重新协商就会出现写入成功但外设收不到完整包的情况。4.2 连接参数连接间隔、从机延迟与超时时间的取舍BLE 的连接参数由从机决定但主机可以发起更新请求。Android 源码中调用gatt.requestConnectionPriority只支持三个档位分别为高优先级、平衡优先级和低功耗优先级。做调试助手时我一般会在设置页暴露三个参数参数取值范围调试建议Connection Interval7.5ms - 4000ms调试透传场景设 30ms - 50ms太低会增加功耗Slave Latency0 - 499调试场景设 0确保每个事件都响应Supervision Timeout100ms - 32000ms建议大于连接间隔乘以 4防止误判超时很多厂商的外设因为固件里连接参数协商逻辑写得激进会主动拒绝 Android 发来的更新请求此时 onConnectionPriorityChange 状态码会是非 0。如果外设不接受你的参数不要强行反复请求这会造成连接不稳定。更可靠的做法是在外设固件端把参数预设成和调试器一致应用端只做查询。4.3 用 logcat 和 hcidump 定位断连与无回调问题当调试助手出现连接后无通知、写数据无响应这类问题源码里的 Log 通常只打到了应用层找不到蓝牙协议栈内部的信息。这时候需要用系统级手段抓取日志。Android 原生系统中运行蓝牙服务的是 bluetooth 进程其日志默认不输出到应用 logcat。你可以通过开发者选项开启 Bluetooth HCI snoop log然后通过 adb 拉取抓包文件adb shell settings put global bluetooth_hci_log true adb shell am broadcast -a android.bluetooth.intent.action.HCI_LOG_ENABLED adb logcat -d | grep -i bluetooth adb pull /data/misc/bluetooth/logs/ .抓到的 btsnoop_hci.log 可以用 Wireshark 打开里面能看到 ATT Write Request、ATT Handle Value Notification 等协议帧。调试中出现最多的三类问题从抓包里能直接看出来一是在 ATT Read By Group Type Request 阶段没有响应说明外设服务发现逻辑卡住了二是 Write Request 发出后没有收到 Write Response说明外设没有处理写入或 MTU 太小导致分包错误三是 Notification 频繁触发时出现大量 0x13 错误说明外设端发送过快主机缓冲区溢出。字段层面的排查结论远比「代码明明写了但设备不工作」这种判断有说服力。4.4 源码里常见的四个断连陷阱第一个陷阱是连接成功后立即调用 discoverServices 太早。虽然官方 API 允许这么做但部分外设初始化较慢建议在连接回调后延迟 100ms 左右再发现服务实测对某些国产芯片外设成功率提升明显。第二个陷阱是未处理 onConnectionStateChange 中 status 不等于 GATT_SUCCESS 的断连此时应直接调用gatt.close()而不是重连否则会累积无效连接句柄。第三个陷阱是扫描回调中直接发起连接正确做法是先 stopScan 再 connect否则某些 ROM 上连接会失败。第四个陷阱是在 onCharacteristicChanged 里做了阻塞操作这个回调频率高且在外设通知密集时会连续触发一旦阻塞会导致后续通知丢失。5. 把调试助手改造成 BLE 自动化测试器三个立即可用的进阶技巧5.1 保存执行序列并用脚本回放以复现外设 bug调试蓝牙问题最常见的痛苦是手动操作不可复现。可以改造源码把每次连接参数、写入长度、写入间隔、已订阅的通知 UUID 都记录到一个 JSON 文件然后提供回放模式。核心思路是让调试助手支持加载「操作序列」并自动执行{ device: C4:5B:BE:01:02:03, mtu: 247, operations: [ { type: connect, timeout_ms: 5000 }, { type: discover, timeout_ms: 3000 }, { type: subscribe, service: fee7, characteristic: fee8 }, { type: write, payload_hex: 010203040506, interval_ms: 20 }, { type: wait_notify, expected_prefix_hex: 01, timeout_ms: 1000 } ] }这里的 wait_notify 操作很关键它等待一个通知事件并且校验返回数据前缀是否匹配。自动化测试蓝牙外设时只校验「收到数据」不够还要校验具体内容。把每次执行的整个流程日志保存在本地文件出现问题后让外设工程师直接对照序列分析比任何口头描述都有用。5.2 把通知流导出为 Wireshark 可读的文本格式gatt 层的收发日志是调试 SDK 协议最常用的数据。很多源码的 log 格式是05-21 10:30:00.123 12345 12345 D BLE: RX: e1020304...这种格式没法直接导入分析工具。你可以为源码补充一个导出模块把收发记录按标准格式输出WAIT RX 0x0011 01 02 03 04 05 TX 0x0012 06 07 08导出这个文件后配合外设厂商协议文档逐条核对。注意这里不是让你解析 GATT 包而是把应用层看到的完整 payload 与对应特征值句柄对应起来用于排查双方对 payload 字段的不同理解。这个能力在设备联调阶段极为实用因为很多外设协议问题是因大小端字节序不一致引发的。5.3 用 Intent 暴露连接参数供外部测试框架调用最后一个技巧是给源码加上一个可以被外部应用或自动化框架调用的入口。Android 的标准做法是定义一个后台服务并导出广播接收器让 Appium、UiAutomator 或自研的测试控制器通过 adb 指令远程触发连接和数据发送adb shell am broadcast -a com.example.btdebug.CONNECT \ --es address C4:5B:BE:01:02:03 \ --ei timeout 5000 adb shell am broadcast -a com.example.btdebug.WRITE \ --es payload 0102030405 \ --ez hex true上述指令通过 Intent extra 传递设备地址、写入内容和超时时间。调试助手的广播接收器收到后执行相应操作并把结果通过 ordered broadcast 或日志返回。这个能力把蓝牙调试助手从「手动工具」升格为「半自动化测试平台」。实际使用中建议保持一次只执行一个操作避免并发广播导致状态机混乱同时在结果里带上耗时和 MTU 协商后的真实值便于你在自动化报告里判断连接质量变化。对于做嵌入式相关开发、做产测工具的人来说这三项改造加进源码工具的复用价值会成倍提高而且整个过程不需要改动协议栈全部在应用层完成。本文还有配套的精品资源点击获取