资讯详情

Android蓝牙开发源码全解析:从扫描到连接的避坑实战指南

📅 2026/10/15 19:43:13 | 华诺云谱 👁 阅读
Android蓝牙开发源码全解析:从扫描到连接的避坑实战指南
简介这是一份面向Android开发者的蓝牙开发源码工程包涵盖蓝牙适配器初始化、BLE设备扫描、Gatt连接及数据读写等核心流程适用于初学蓝牙通信或需快速搭建设备互联功能的开发者。压缩包共23个文件包含Java源码、AndroidManifest.xml、资源文件以及编译生成的apk、dex、class等既可直接阅读源码也可运行包内APK验证效果整体仅91KB轻量便于下载。已有209人学习浏览这份工程包具有一定参考价值。通过分析源码可掌握BluetoothManager与BluetoothAdapter的获取方式、ScanCallback扫描回调处理、BluetoothGatt与BluetoothGattCallback的用法还能理解Android不同版本对扫描权限、扫描模式的要求从而减少实际开发中的兼容性问题。1. Android 蓝牙开发源码从能扫到设备到能顺利交付的完整路径搜“android 蓝牙开发源码”结果基本分两种要么是几年前的 Demo 工程要么是封装得看不出调用逻辑的框架代码。实际上 Android 蓝牙开发属于“看起来不难、跑起来全是细节”的模块——权限、扫描、配对、连接、服务发现、数据收发每一环都可能因为设备兼容性翻车而且一旦翻车报错信息常常让你摸不着头脑。这篇文章不打算给你一份虚构的全家桶源码包而是把一条可复现的开发路径拆开讲透先搞懂选型再跑通最小工程接着调关键参数最后落在一份能用的避坑清单上。不管你是新接手蓝牙功能还是已经写了两天还卡在连接阶段按这个顺序走一遍对自己手上的源码该看哪里、改哪里、为什么这么改会有清晰的方向感。2. 先选型再读码经典蓝牙与 BLE 的源码结构差异2.1 经典蓝牙与 BLE协议栈差异决定了代码走向Android 的蓝牙技术栈从 API 层面分两条路线。经典蓝牙走 BluetoothSocket适合音频、文件传输这类持续大流量场景低功耗蓝牙BLE走 BluetoothGatt适合传感器数据、遥控器、智能硬件这类小包低频场景。两者的源码结构差异很大经典蓝牙的核心是BluetoothDevice.createRfcommSocketToServiceRecord()BLE 的核心是BluetoothGattCallback里那一堆异步回调。选型选错了后面的代码几乎等于白写。实际项目里“android 蓝牙开发源码”的需求大部分落在 BLE 上。原因无非三点功耗低、协议灵活、手机兼容性好。但如果你要接的是车载蓝牙、蓝牙音箱、老式蓝牙串口模块那就绕不开经典蓝牙的 Socket 路线。我的习惯是先问自己三个问题数据量多大传输频率多高对端设备是什么数据量超过每秒几十 KB、且要求稳定流式传输就选经典蓝牙否则优先 BLE。这个决定直接决定了你手上那份源码的代码组织方式。两条路线在连接流程上的差异也要清楚这会影响你排查问题的方向。经典蓝牙需要先配对再创建 Socket连接后走输入输出流BLE 不需要配对直接连接 GATT 服务然后通过特征值Characteristic读写。配对失败的报错在经典蓝牙里非常常见而 BLE 里根本没有“配对”这个概念取而代之的是连接参数协商和 MTU 协商。如果你拿到的源码是 BLE 的却在翻“配对回调”说明你还没把协议模型转换过来。2.2 一份可用的蓝牙源码工程里该有哪些模块拿到一套蓝牙源码不用急着逐行读。先看它有没有把功能拆成模块。一个可维护的蓝牙工程至少应该包含初始化、扫描、连接、数据收发、服务解析、状态回调这六个部分。我习惯把它们列成一张对照表来快速对号入座模块职责关键类初始化检查硬件、权限、蓝牙开关状态BluetoothAdapter, BluetoothManager扫描发现周边设备区分经典蓝牙与 BLEBluetoothAdapter.startDiscovery / BluetoothLeScanner连接管理建立并维护连接持有连接状态BluetoothSocket / BluetoothGatt数据收发收发数据处理粘包与分包InputStream / OutputStream / GattCallback服务解析解析 BLE 服务与特征值BluetoothGattService / BluetoothGattCharacteristic状态回调向 UI 层通知连接状态变化Handler / LiveData / 接口回调如果一份源码里找不到独立的扫描模块多半是把扫描逻辑直接塞进了 Activity这种结构改起来成本很高设备一多就乱成一锅粥。我更在意的其实不是它的包名是否华丽而是扫描、连接、收发这三件事有没有解耦。三者独立之后后面你想从经典蓝牙切到 BLE或者换一个对端设备型号改动范围能被限制在一个模块内。模块划分还有一个现实价值蓝牙连接经常是异步的回调满天飞。如果所有逻辑都堆在 Activity 里配置变更旋转屏幕时回调就会崩或泄漏。把连接和扫描放到 ViewModel 或独立的单例管理器里持有 ApplicationContext是规避这类问题的常见且有效做法。2.3 拿到一份源码后先看哪几个文件按下面这个顺序读半小时内基本能判断一套源码靠不靠谱。首先打开 AndroidManifest.xml看蓝牙相关权限是否齐全是否适配了 Android 12API 31之后的运行时权限拆分。很多老源码在高版本设备上直接崩就是因为没适配 BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT 的动态申请。接着看蓝牙适配器的初始化位置。最规范的写法是通过 BluetoothManager 拿 BluetoothAdapter这样能准确拿到系统蓝牙服务的状态。如果源码里直接用BluetoothAdapter.getDefaultAdapter()也不是不行但你在新设备上要留意这个接口是否被系统标记为过时或受限。再看设备扫描的回调实现。经典蓝牙用的是 BroadcastReceiver 接收 ACTION_FOUNDBLE 用的是 ScanCallback。重点看回调里是按设备名做字符串匹配还是结合信号强度做过滤。直接拿设备名做模糊匹配的源码在设备密集的环境里基本不能用。最后看连接的建立与断开逻辑。经典蓝牙看 Socket 是否在子线程创建、是否有超时控制BLE 看 onConnectionStateChange 里的断线处理是否完整。很多源码只写了连接成功和失败没有重连逻辑这种工程连上线都会断交付时必被挑战。3. 把蓝牙源码跑通最小工程与调用链拆解3.1 权限声明与蓝牙适配器初始化权限是第一道门槛也是翻车率最高的地方。Android 12API 31之后蓝牙权限被拆成 BLUETOOTH_SCAN、BLUETOOTH_CONNECT、BLUETOOTH_ADVERTISE 三个运行时权限而 Android 11 及以下用的是 BLUETOOTH 和 BLUETOOTH_ADMIN另外定位权限对 BLE 扫描结果有直接影响。下面这段 Manifest 声明是我在新项目里一直沿用的兼容写法!-- Android 12 -- uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.permission.BLUETOOTH_ADVERTISE / !-- Android 11 及以下 -- uses-permission android:nameandroid.permission.BLUETOOTH android:maxSdkVersion30 / uses-permission android:nameandroid.permission.BLUETOOTH_ADMIN android:maxSdkVersion30 / uses-permission android:nameandroid.permission.ACCESS_FINE_LOCATION android:maxSdkVersion30 /usesPermissionFlagsneverForLocation表示不会用蓝牙扫描结果做定位这在应用市场审核时能降低权限敏感度。但如果你的应用确实需要根据蓝牙信号推算位置这句就不要加否则功能会受限。maxSdkVersion30的作用是让旧权限只在 Android 11 及以下生效避免高版本系统重复请求旧权限。适配器初始化我推荐用 BluetoothManager 的方式代码更稳// BluetoothManager 方式获取适配器兼容性更好 BluetoothManager manager (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); BluetoothAdapter adapter manager.getAdapter(); if (adapter null) { // 设备不支持蓝牙直接停止后续流程 return; } if (!adapter.isEnabled()) { // 先发系统 Intent 请求用户打开蓝牙而不是强制 enable Intent enableBtIntent new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT); }manager.getAdapter()在系统蓝牙服务异常重启后能重新拿到有效实例而getDefaultAdapter()在某些定制 ROM 上会返回一个不可用的空壳对象。这里没有直接用adapter.enable()是因为那是反射系统接口部分厂商 ROM 会拦截而且系统会弹权限确认框不做用户确认直接开蓝牙不符合交互规范。运行时权限请求建议把三个蓝牙权限和定位权限放在一次请求里完成用户只需要做一次授权操作体验会好很多。注意Android 13 及以上即使动态权限都给了系统设置里“附近设备”权限开关如果被用户手动关闭BLE 扫描仍然会静默失败。这个开关藏得很深遇到扫描不到设备时要去检查。3.2 设备扫描回调、过滤与扫描周期BLE 扫描的推荐做法是用 BluetoothLeScanner扫描回调的触发频率可以很高必须做过滤和去重。下面是一个带信号强度过滤的扫描实现能有效避免扫描列表被远处设备刷屏private MapString, BluetoothDevice deviceMap new HashMap(); private ListBluetoothDevice deviceList new ArrayList(); private ScanCallback scanCallback new ScanCallback() { Override public void onScanResult(int callbackType, ScanResult result) { BluetoothDevice device result.getDevice(); String name device.getName(); int rssi result.getRssi(); // 信号强度低于 -85dBm 的设备直接忽略减少列表干扰 if (rssi -85) { return; } // 按设备地址去重避免同一设备在回调中反复出现 if (!deviceMap.containsKey(device.getAddress())) { deviceMap.put(device.getAddress(), device); deviceList.add(device); // 回到主线程更新 UI 列表 runOnUiThread(() - adapter.notifyItemInserted(deviceList.size() - 1)); } } }; private void startBleScan() { BluetoothLeScanner scanner BluetoothAdapter.getDefaultAdapter().getBluetoothLeScanner(); if (scanner null) { // 当前设备不支持 BLE 扫描提示用户换设备 return; } ScanFilter filter new ScanFilter.Builder() .setDeviceName(MyDevice) // 按广播名精确过滤 .build(); ScanSettings settings new ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) // 高频率扫描 .setReportDelay(0) // 实时上报不做批量延迟 .build(); scanner.startScan(filter, settings, scanCallback); }ScanFilter能按设备名或服务 UUID 过滤能显著减少回调次数。但按设备名过滤的前提是广播名稳定有些设备会在名称后面加随机后缀用setDeviceName就会漏掉。遇到这种对端我建议要么去掉过滤器要么按服务 UUID 过滤。setScanMode有两档最常用LOW_LATENCY 延迟最低但耗电高适合用户停留在扫描页的场景LOW_POWER 适合后台扫描但发现速度慢很多。setReportDelay(0)表示实时回调如果设成大于 0 的值系统会积攒一批结果一起返回这个对 UI 列表刷新并不友好。经典蓝牙扫描走的是startDiscovery加广播接收器。需要注意的是startDiscovery是全局扫描会扫描所有经典蓝牙设备而且扫描期间会干扰已经建立的蓝牙连接在低端设备上尤其明显。我的习惯是只在用户主动点击“搜索”时启动扫描到目标设备立刻cancelDiscovery()不要让扫描在后台持续运行。3.3 建立连接与数据收发Socket 与 GATT 两条路线经典蓝牙的连接必须在子线程里做connect()是阻塞调用而且没有内置超时。部分设备响应慢连接可能拖十几秒。下面这段代码把连接放到单线程池并依赖外层做超时控制private void connectClassic(BluetoothDevice device) { ExecutorService executor Executors.newSingleThreadExecutor(); executor.execute(() - { try { // 创建 Socket使用 SPP 服务 UUID经典蓝牙串口通用值 BluetoothSocket socket device.createRfcommSocketToServiceRecord( UUID.fromString(00001101-0000-1000-8000-00805F9B34FB)); // 连接前主动取消扫描避免扫描干扰连接建立 bluetoothAdapter.cancelDiscovery(); // 阻塞调用建议用 Future.get(timeout) 包装做超时 socket.connect(); InputStream in socket.getInputStream(); OutputStream out socket.getOutputStream(); // 启动独立读线程持续读取对端数据 readLoop(in); } catch (IOException e) { // 连接失败记录日志并触发重试或提示用户 e.printStackTrace(); } }); }createRfcommSocketToServiceRecord的第一个参数是服务 UUID。SPP 的通用 UUID 是00001101-0000-1000-8000-00805F9B34FB绝大多数蓝牙串口设备都认这个值。但如果你的对端是定制硬件就需要查阅设备文档确认 UUID 是否不同。connect()成功后读写操作都在各自的线程里完成不能把读线程和 UI 线程混在一起。BLE 的代码风格完全相反。Gatt 是异步的连接结果、服务发现结果、数据通知全部通过BluetoothGattCallback回调返回。下面是连接加订阅通知的完整流程private BluetoothGatt gatt; private final BluetoothGattCallback gattCallback 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) { // 连接断开在这里做重连或通知上层 } } Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { if (status ! BluetoothGatt.GATT_SUCCESS) { // 服务发现失败关闭连接并重试 gatt.disconnect(); gatt.close(); return; } // 按协议文档里的 UUID 查找服务和特征值 BluetoothGattService service gatt.getService(serviceUuid); if (service null) { return; } BluetoothGattCharacteristic characteristic service.getCharacteristic(charUuid); // 启用通知让设备可以主动推送数据 gatt.setCharacteristicNotification(characteristic, true); // 有些设备还必须写描述符 CCCD 才能收到通知 BluetoothGattDescriptor descriptor characteristic.getDescriptor( UUID.fromString(00002902-0000-1000-8000-00805F9B34FB)); if (descriptor ! null) { descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.writeDescriptor(descriptor); } } Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { byte[] data characteristic.getValue(); // 对端设备推送的数据在这里处理不能做耗时操作 } }; private void connectBle(BluetoothDevice device) { // autoConnect 用 false主动连接速度更快 gatt device.connectGatt(context, false, gattCallback); }connectGatt的autoConnect参数值得单独说。false是主动连接速度快适合用户点击连接按钮的场景true是让系统在后台监测到设备可用时自动连接适合“靠近就自动连”的场景但首次连接很慢部分设备固件还不支持。我几乎全用false然后自己做重连策略这样时序是可控的。setCharacteristicNotification之后很多设备还必须写描述符 0x2902CCCD否则数据不会推送上来。这是 BLE 开发里最容易漏掉的一步漏掉的表现就是连接正常、服务发现正常但onCharacteristicChanged永远不触发。3.4 数据收发缓冲与线程模型蓝牙数据没有理想的包边界。经典蓝牙的 InputStream 是字节流你不知道一帧数据在哪里结束BLE 的onCharacteristicChanged每次回调是一个特征值的数据包但如果特征值超过 MTU对端会把它拆成多个包依次推送。应用层必须自己维护一个累积缓冲来做粘包和分包处理。常用的做法是维护一个 ByteArrayOutputStream把每个 chunk 写入再按自定义的帧协议去判断哪些是完整的包、哪些还差数据。帧协议一般就是包头、长度、负载、校验这个套路。比如你规定帧以 0xAA 0x55 开头以 0x0D 0x0A 结尾那应用层的处理逻辑就是先把数据塞进 buffer然后循环检查 buffer 里是否有完整的帧有就提取出来处理剩下的残留在 buffer 里等下一个 chunk 凑齐。没有协议文档的话可以抓一段对端的日志统计一组完整数据的长度和起止标志用启发式方式找边界。这个过程是蓝牙开发里最耗时、也最容易让开发者产生“数据对不对得上”困惑的地方值得多投入时间验证。线程模型方面原则只有一条蓝牙回调线程不碰 UI。经典蓝牙的读线程在子线程BLE 的回调运行在系统 Binder 线程都不能直接更新界面。用 Handler 或 LiveData 把数据切到主线程。另外onCharacteristicChanged回调频率可能很高如果在这里做耗时的业务处理会拖慢整个蓝牙消息队列导致后续数据包丢失这点在高频推送场景下尤其要留心。4. 源码改造时最容易翻车的配置项UUID、扫描周期与重连策略4.1 UUID 不一致连接失败的第一大原因UUID 是蓝牙开发里出问题最多的配置项。经典蓝牙里创建 Socket 用的 UUID 必须与对端设备监听的服务 UUID 一致BLE 里GATT 服务 UUID、特征值 UUID 必须与设备广播的完全一致。UUID 是 128 位的十六进制字符串形如00001101-0000-1000-8000-00805F9B34FB但不同设备文档的写法差别很大有的省略前导零有的把厂商自定义段放在中间一不留神就配错。我的经验是拿到设备文档后先把所有 UUID 整理到一个常量类里逐个对照格式补全成完整的 128 位字符串。BIG 需要注意的是大小写不敏感但连字符位置必须正确有些框架层会做格式化但你自己手写的字符串不一致就会失败。另外一个常用手段是先用成熟的 BLE 调试工具连接设备查看它实际广播的服务和特征值列表再和源码里的常量逐个比对能快速定位写错的位置。注意调试工具连过的设备会在系统层留下缓存同一台设备在应用里连接时可能被系统返回旧缓存遇到这种情况要把设备从系统蓝牙设置里移除再测试。4.2 扫描周期与服务发现时序回调没触发时先看时序扫描周期的设置直接影响两个体验设备出现速度和耗电。持续以 LOW_LATENCY 模式扫描手机发热和耗电都会明显上升尤其在扫描列表页面长时间停留的场景。我一般把扫描拆成“扫描 6 秒、停 3 秒”的循环来平衡功耗和发现速度。注意停扫描时要传入同一个scanCallback实例不要传 null传 null 是全局停止会把其他模块发起的扫描也停掉。服务发现时序的问题也很典型。很多源码在onConnectionStateChange里调用discoverServices()之后就立刻去getService()但onServicesDiscovered可能几百毫秒后才回调。这时拿到的 service 是 null代码就崩了或者静默失败。正确做法是在onServicesDiscovered回调里去获取服务和特征值而不是在连接成功的回调里急着取。还有一类厂商设备的行为比较特殊连接成功后不会立刻响应服务发现需要先往某个特征值写一个唤醒命令然后等 200 毫秒左右再调discoverServices()才会正常完成服务枚举。如果你发现设备用通用工具一切正常、自己的代码却卡在服务发现阶段可以先试试延迟触发discoverServices()这个经验我在实际项目里验证过多次。4.3 重连策略与系统蓝牙缓存断线重连不是简单重调 connect很多人在连接断开后直接调用之前的connectGatt或socket.connect()结果要么连不上要么连上后行为异常。这个问题多半落在系统蓝牙缓存和资源未清理上。BLE 断线重连的正确姿势是private void reconnectBle(BluetoothDevice device) { if (gatt ! null) { // 先断开再关闭释放底层资源 gatt.disconnect(); gatt.close(); gatt null; } // 必须用新的 connectGatt 重新建立连接 gatt device.connectGatt(context, false, gattCallback); }gatt.disconnect()和gatt.close()的顺序不能反。先断开连接再释放资源。如果跳过close()系统底层会保留一个 GATT 客户端实例下一次connectGatt很容易失败或串线。这是 BLE 资源管理里最重要的一步。经典蓝牙的 Socket 也一样一个BluetoothSocket对象成功connect()一次之后再断开不能复用第二次。重连时必须重新createRfcommSocketToServiceRecord()创建新 Socket旧对象直接close()。如果代码里复用了同一个 Socket 对象第二次连接大概率抛异常或卡死。经典蓝牙的重连流程必须是“新建 Socket → 连接 → 使用 → 关闭 → 新建”没有捷径。系统蓝牙缓存的另外一个表现是设备改了广播名或服务 UUID但手机扫到的还是旧的。这是系统级缓存应用层控制不了只能引导用户在系统蓝牙设置里取消配对后重新扫描。你的代码里能做的就是尽量保持设备端广播的 UUID 和名称稳定从源头减少缓存导致的幽灵数据问题。5. Android 蓝牙开发避坑指南五条高频踩坑记录5.1 坑一权限声明了但一启动就崩溃现象AndroidManifest.xml 里权限一个不少运行在 Android 12 以上设备仍然崩溃日志报 SecurityException 或 MissingPermission。原因targetSdkVersion 在 31 以上时BLUETOOTH_SCAN 和 BLUETOOTH_CONNECT 属于运行时权限必须在代码里动态申请。只写在 Manifest 里等于没写。另一个隐蔽点部分厂商 ROM 在 targetSdkVersion 30 及以下也会强制要求动态申请否则直接不给权限。解决在启动流程中先判断 SDK 版本31 及以上请求三个蓝牙权限30 及以下请求定位权限。用ActivityResultContracts.RequestMultiplePermissions统一管理授权结果。同时检查系统设置里“附近设备”权限开关是否开启这个开关与运行时权限是独立的用户关掉后任何扫描和连接 API 都会静默失败。5.2 坑二扫描回调一直不触发扫描列表永远为空现象代码完全照着官方文档写但onScanResult一次都没被调用。或者扫描到了设备但列表里只出现一两个离得近的设备反而不出现。原因最常见的是系统“附近设备”权限被关闭扫描 API 被静默拦截不报任何错误。其次是 ScanFilter 配置过严比如setDeviceName拼错、服务 UUID 和设备广播不一致过滤后把目标设备也滤掉了。还有一种情况是上一次stopScan没正常执行新的startScan不会有回调。解决先用最宽松的方式测试不传任何 Filter直接scanner.startScan(callback)。回调能触发就说明过滤条件有问题逐个把条件加回去排查。回调仍不触发就去检查系统权限和位置服务开关。另外 Android 13 以上定位服务开关关闭也会导致 BLE 扫描不到设备这个要准备在文档里写清楚否则用户会认为软件有问题。5.3 坑三BLE 连接成功后立刻断连状态码是 133现象onConnectionStateChange先收到 STATE_CONNECTED不到一秒又收到 STATE_DISCONNECTEDstatus 值是 133GATT_ERROR。原因133 表示底层 GATT 连接出错。常见原因有上一次连接没有close()旧实例还占着资源两个应用同时连接同一台 BLE 设备设备端主动断开。还有一个容易被低估的原因是 MTU 协商不成功部分老固件不支持高 MTU超出范围会直接断开连接。解决先检查代码里是否在每次连接前都调用了close()处理旧连接。然后排除多应用干扰把系统蓝牙设置里所有配对设备取消关掉其他使用蓝牙的应用再测试。MTU 问题用gatt.requestMtu(247)时注意捕获结果回调不要假设一定成功。如果设备固件不支持高 MTU应用层仍要按 20 字节分包处理数据。5.4 坑四多设备并发连接时数据窜了现象同时连接多台设备A 设备的数据跑到了 B 设备的回调里断开 A 设备后 B 设备也跟着断了。原因如果多个连接共用一个BluetoothGattCallback所有设备的回调都会进同一个实例。回调方法里的gatt参数能区分设备但很多源码直接忽略这个参数数据自然就窜了。还有更糟的情况多个连接共用一个BluetoothGatt实例系统会返回同一个已存在的对象设备之间互相干扰。解决每个设备使用独立的BluetoothGatt实例回调里用gatt.getDevice().getAddress()区分设备维护一个MapdeviceAddress, BluetoothGatt。处理数据时始终携带设备地址上下文不要只把 byte[] 传给上层。并发连接数也有限制手机硬件虽然支持多个 BLE 连接但实际项目中连接超过三个就会出现不稳定不要设计成大规模一对多并发。5.5 坑五设备端固件不支持 MTU 协商数据包被静默吞掉现象连接一切正常服务发现也成功应用写入大块数据也不报错但对端就是没有响应。日志里看不到任何异常似乎数据发出去就消失了。原因应用主动requestMtu(247)但设备固件协商失败后静默退回默认 MTU一般是 23 字节。应用层还按照扩展 MTU 一次性写入了超过 20 字节的数据包底层把超长部分丢掉造成数据静默丢失。解决在onMtuChanged回调里确认协商结果拿到实际 MTU 值再根据实际值调整每次写入的字节数。不需要强行追求大包应用层可以按 20 字节分包写入或者等 MTU 协商成功后再切大包模式。这个排查动作我很早就应该做结果在一个项目里耗了快一周最后靠协议日志才发现设备端拒绝了 MTU 协商。6. 强化你的调试习惯先抓蓝牙协议日志再动代码调试蓝牙源码光靠 Logcat 里的应用日志远远不够。很多连接层的异常发生在系统协议栈内部应用层只能看到状态的跳变看不到原因。这时候必须抓系统蓝牙 HCI 日志。开启方法是在开发者选项里打开“蓝牙 HCI 信息收集日志”Bluetooth HCI snoop log复现问题后日志文件会记录系统蓝牙协议栈收发的所有 HCI 数据包。文件通常保存在/data/misc/bluetooth/logs/可以用 adb 导出然后用 Wireshark 的解析工具查看。日志里能看到完整的 ATT/GATT 数据包来回记录比如某个特征值写入是否被设备拒收、连接断开前底层发生了什么事件。我的排查习惯是先看应用日志确认在哪个回调卡住再决定要不要抓 HCI 日志。比如连接断开在 state133HCI 日志会显示底层是否有 Link Loss 事件onCharacteristicChanged收不到数据HCI 日志能看出设备到底有没有主动发 Notification——如果日志里根本没有设备发来的包那问题在对端如果有包但应用没收到那问题在订阅 Descriptor 或者 MTU 协商上。这种定位方式能避免“盲目改代码然后反复试”的死循环。还有一个更省钱的前置手段在 Logcat 里过滤系统蓝牙日志关键字包括 BluetoothGatt、bt_btif、bt_gatt。系统层很多关键状态变化会打点在这里。配合 Debug.startMethodTracing可以查看蓝牙回调线程上的耗时操作。把回调里的耗时控制在一毫秒以内是避免背靠背数据包丢失的前提这在大量数据连续推送时尤其重要。回头再看我调试过的蓝牙项目花时间最多的往往不是写代码而是定位“谁没按协议走”。一开始遇到连接异常就闷头改业务代码改了两天发现根本没用后来养成先看协议日志、再动代码的习惯很多问题一眼就能看到症结。如果这篇文章能让你少走这个弯路那投入的方向就是对的。希望你也能养成这个习惯——遇到蓝牙异常先抓日志再下结论。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑