
经常有朋友拿着蓝牙问题来找我问得最多的一类是“我代码明明是按文档写的为什么扫描不到设备”“连上了但几秒就断到底哪里错了”。以前我会先让他们把logcat抓来后来发现logcat能回答的问题很有限。真正让我把整条链路串起来的是最近帮人排查一个Android 12适配问题App升级到targetSdk 31之后扫描彻底没反应没有异常、没有日志界面上一片死寂。最后打开开发者选项里的“蓝牙HCI信息收集日志”导出的btsnoop文件里连一条扫描命令都没有问题才浮出水面——新的运行时权限没申请命令在系统服务层就被拦下了。这件事让我意识到Android 12把蓝牙的权限模型、协议栈结构、系统服务组织方式都改了不少如果只停留在调API的层面遇到问题根本无从下手。这篇文章我打算把从HCI到应用层的完整链路拆开讲一遍每一层对应什么代码、什么日志、什么排查工具全部说清楚。适合三类人平时只调用BluetoothAdapter、对协议栈陌生的App开发者做系统定制、经常跟蓝牙服务打交道的Framework工程师以及正打算把项目targetSdk升到31、担心蓝牙权限踩坑的团队。1. 为什么我要把这条链路完整走一遍1.1 一次“HCI日志不会说谎”的手把手复盘先回到开头那个案例。朋友的App在Android 11上跑得好好的升到Android 12之后扫描回调一个都不触发logcat里也没有任何SecurityException。他查了Manifest里面有BLUETOOTH和BLUETOOTH_ADMIN两个权限按老经验来说扫描没问题。但我打开btsnoop后发现协议栈压根没有收到上层下发的LE Set Scan Enable命令。这就说明问题不在射频、不在设备端而是在HCI层之上某处把命令拦住了。顺着调用链往上看Android 12API 31在蓝牙权限模型上做了一个比较大的调整新增了BLUETOOTH_SCAN、BLUETOOTH_ADVERTISE、BLUETOOTH_CONNECT三个运行时权限。对于targetSdk 31的应用扫描必须动态申请BLUETOOTH_SCAN并且依然需要定位权限配合。老权限BLUETOOTH和BLUETOOTH_ADMIN虽然在Manifest里声明了但已经降级为普通权限不再具备开启扫描的能力。表面上看“代码没变”实际入口早已换了规则。这个案例给我最大的触动是在HCI层看到的每一个数据包都是上层所有逻辑执行完之后的最终结果。如果扫描命令压根没出现意味着问题100%在系统服务层往上如果命令出现了但控制器没回应才能往协议栈和硬件方向查。这种“由底向上”的判断顺序比在上层乱猜高效得多。1.2 一条链路视图从蓝牙芯片到App回调的六层为了便于后文展开先给一张整体视图。一次完整的蓝牙操作数据大致经过以下六层控制器Controller也就是蓝牙SoC或外挂芯片负责射频收发、底层链路管理。HCI接口主机与控制器之间的协议接口物理载体常见UART、USB、SDIO。协议栈Host StackAndroid上主要是Bluedroid以及Android 12开始引入的Gabeldorsche负责L2CAP、SMP、GATT等核心协议。JNI把协议栈的C接口桥接给Java层的AdapterService。系统服务包括system_server进程里的BluetoothManagerService以及独立蓝牙进程里的AdapterService负责权限检查、进程间调度、配对状态管理。应用层开发者直接接触的BluetoothAdapter、BluetoothGatt等API。各层对应的故障表现差别很大我整理了一个简表层级核心机制常见故障特征控制器HCI Event、ACL/SCO/ISO包无Advertising Report、连接后立刻丢链路HCI接口UART/USB/PCIe传输日志中断、CRC错误、无响应协议栈L2CAP/GATT/SMP状态机配对失败、MTU协商失败、服务发现卡住JNIbt_interface_t回调native crash、回调丢失系统服务Binder、权限检查、BondStateMachineSecurityException、配对框不弹应用层BluetoothAdapter/GattCallback扫描无回调、onConnectionStateChange异常后文我就按这个顺序逐层深入每一层都讲清楚“是什么”“在哪看”“怎么排障”。2. HCI层全链路中唯一不会撒谎的事实2.1 四种HCI包与它们各自扮演的角色HCI的全称是Host Controller Interface是蓝牙主机协议栈与蓝牙控制器之间的标准接口。它定义了一套固定的包格式不论底层的物理传输是UART还是USB承载的内容都是同一套协议。理解HCI是理解整条链路最低层、最可靠事实的基础。HCI包一共有四种类型Command包主机发给控制器让控制器执行操作比如开启扫描、发起连接。Event包控制器回复主机表示命令执行结果或主动上报状态比如Command Complete、Connection Complete。ACL Data包承载传统蓝牙的异步无连接数据比如GATT的ATT请求响应都会封装在ACL包中传输。SCO/ISO Data包承载同步语音或LE同步数据比如蓝牙通话的音频流、LE Audio的同步流。Command包的格式很固定前两个字节是Opcode其中高6位是OGFOpcode Group Field低10位是OCFOpcode Command Field后面跟着1字节的参数长度和若干参数。举个例子经典蓝牙的Inquiry命令opcode是0x0401收到后会触发控制器返回Inquiry Result事件。BLE扫描的LE Set Scan Enable命令opcode是0x200C参数为01 00时表示开启扫描。Event包的结构是1字节事件码加1字节参数长度再加事件参数。比如Command Complete事件的事件码是0x0ELE Meta Event的事件码是0x3E而扫描到的广播数据就包含在LE Meta Event的LE Advertising Report子事件里。看btsnoop时如果能认出这些十六进制值基本就不会被各种上层日志带偏。2.2 怎么打开btsnoop日志以及Wireshark里该看什么在Android设备上抓HCI日志最标准的途径是开发者选项里的“蓝牙HCI信息收集日志”不同厂商的ROM叫法可能略有差异但底层逻辑一致开启后系统会把HCI层收发到的所有Command、Event、ACL数据封装成btsnoop格式写到固定目录。默认路径通常是/data/misc/bluetooth/logs/btsnoop_hci.log。拿到日志后我建议用Wireshark打开它自带btsnoop解析器能把HCI包结构、opcode、事件类型、status字段都解码成可读文本。常用的分析思路是这样如果问题是“扫描不到设备”先看有没有LE Set Scan Enable再看有没有LE Advertising Report。前者缺失说明命令没下发后者缺失说明命令发了但控制器没扫描到任何广播。如果问题是“连接失败”先看LE Create Connection的Command Status返回值再找对应的LE Connection Complete事件status字段能直接反映底层失败原因。如果问题是“连接后秒断”重点找Disconnection Complete事件它前面的连接状态和原因代码能说明是被对端断开还是本机主动断开。提示抓btsnoop期间尽量别重启手机。有些设备在蓝牙服务重启后会重新创建日志文件导致刚才的问题现场丢失。2.3 从HCI包立刻能判断的几类故障很多人一遇到蓝牙问题就去看应用层异常但很多问题的答案在HCI层非常直接。我总结过几类常见场景场景一广播数据存在但App收不到。如果btsnoop里LE Advertising Report一直出现说明控制器层没问题问题可能出在协议栈的扫描过滤参数或者应用层注册的ScanFilter把结果过滤掉了。场景二连接请求发了但一直没有Connection Complete。这说明控制器已经尝试建链但对端没有回应或者距离太远、跳频同步失败。此时不应当怀疑App代码而要去看对端设备是否在广播、是否进入可连接状态。场景三配对时出现Authentication Failure。这通常是SMP层的安全要求不匹配。比如对端要求MITM保护而本机使用Legacy Pairing且没有启用保护或者双方保存的Link Key已经失配。处理办法是先取消配对、删除已保存的配对信息再重新配对。3. 协议栈Bluedroid与Android 12的新架构调整3.1 Mainline化之后代码目录都发生了什么Android 12源码里蓝牙模块的代码组织方式有明显变化。早期Android版本中蓝牙协议栈长在system/bt目录下和操作系统绑定得很紧。Android 12里整个蓝牙模块被收敛到packages/modules/Bluetooth意味着它更像一个独立可升级的模块而不是操作系统内核不可分割的一部分。对开发者来说最大的影响是厂商定制时不能再随意改协议栈底层逻辑否则跨版本升级时会被标准实现冲掉。这个目录下你可以看到几棵关键代码树一部分是老牌的Bluedroid实现代码里能找到btif、bta、stack、hci等子模块另一部分是Android 12新增的Gabeldorsche目录名通常是system/gd。两者会同时存在于源码中编译时根据目标选择启用哪个作为默认实现。绝大多数手机厂商的Android 12出货机仍然跑BluedroidGabeldorsche更多作为技术演进和验证的载体。3.2 Bluedroid内部五层BTIF/BTU/BTM/Stack/ChipBluedroid这个老栈虽然名字听起来过时但在Android上服役了非常久结构也相对清晰。读源码时建议按下面这五层去理解BTIFBluetooth Interface最顶层给JNI提供统一的C接口。JNI里很多btif_xxx函数就是这一层的入口它的职责是隔离上层实现与下层协议细节。BTUBluetooth Upper负责事件分发和任务调度。老栈里的核心事件循环就在这一层所有模块的消息都会汇聚到这里再分发下去。BTMBluetooth Manager设备管理中枢负责配对、链路策略、设备发现状态的维护。很多系统级的设备缓存和策略都落在这一层。Stack协议实现区里面按协议拆成了L2CAP、RFCOMM、SDP、GATT、SMP等子目录。其中L2CAP负责信道的建立、分片和重组GATT负责服务发现和属性读写SMP负责配对和密钥分发。Chip/Controller负责和HCI硬件通信包括固件下载、HCI命令的封装发送、HCI事件的接收解析。这五层之间的关系很像快递系统BTIF是前台收件柜台BTU是分拣中心BTM是配送调度Stack是干线运输Controller是最终派送的快递员。任何一个环节出问题都会在下游表现为不同的症状。3.3 Gabeldorsche新栈如何做到“可逐步替换”Android 12引入Gabeldorsche出发点很简单老栈Bluedroid模块耦合太重改动一个模块常常要靠其他模块配合而且单元测试覆盖不足。Gabeldorsche采用更小的组件化单元每个单元通过定义良好的接口与其他单元通信并且内置了更完整的测试基建。对于不深入做系统开发的读者知道这一点就够新栈在设计上就是为了逐步替代老栈的。它会先接管HCI层、再慢慢往上替换最后再把上层的蓝牙App服务层迁过去。因此在Android 12源码里你会看到Gabeldorsche和Bluedroid同时存在的过渡期。排查问题时如果发现某些log里有gd字样说明当前设备可能已经切换到新栈如果全是btif/btm字样说明还在老栈逻辑里。两者虽然接口不同但HCI层的数据是一样的所以btsnoop仍然是万能的底层参照。4. 系统服务层蓝牙App、BluetoothService与Binder三角关系4.1 App和协议栈之间为什么非要隔一层BinderAndroid的蓝牙系统进程模型比较特殊很多人在这一块理解有偏差。实际架构是system_server进程里有一个BluetoothManagerService它更像一个管家负责管理蓝牙开关状态、系统级配置和 binder 分发真正的蓝牙服务逻辑运行在独立的com.android.bluetooth进程里这个进程里的核心服务叫AdapterService它通过JNI直接跟协议栈通信。应用进程在调用BluetoothAdapter时实际会通过Binder先访问BluetoothManagerService再由它把请求转发给蓝牙进程的AdapterService最后通过JNI进入协议栈。之所以要绕这么一圈是因为蓝牙模块要能独立升级、独立崩溃重启如果直接让每个App持有协议栈的native句柄一个App的崩溃就可能拖垮整个蓝牙系统。Binder隔离了进程崩溃范围同时也让权限检查有了统一的入口。这也解释了排查问题时要看两个进程的logcatsystem_server里看的是权限和管理层的日志com.android.bluetooth进程里看的是协议栈和服务回调日志。只盯一个进程很容易漏掉另一半真相。4.2 Android 12权限模型的变更三个新运行时权限Android 12在蓝牙权限上的改动是这次版本升级里对App开发者影响最大的部分。新模型下原来的BLUETOOTH和BLUETOOTH_ADMIN变成了普通权限而真正干活的能力被拆分到三个运行时权限中权限作用范围权限类型BLUETOOTH_SCAN执行BLE扫描、经典蓝牙发现运行时权限BLUETOOTH_ADVERTISE发送BLE广播外设角色运行时权限BLUETOOTH_CONNECT连接、配对、GATT操作运行时权限对targetSdk 31及以上的应用使用这些能力时必须在运行时动态申请。这里有一个特别容易忽略的点即使在Android 12上BLE扫描仍然需要定位权限配合ACCESS_FINE_LOCATION和BLUETOOTH_SCAN是“并且”的关系缺一不可。很多团队只加了新蓝牙权限却忘了定位权限结果扫描照样不回调。另外要留意的是如果你的应用还需要支持Android 11及以下的旧版本Manifest里要同时保留旧的BLUETOOTH和BLUETOOTH_ADMIN权限代码里针对SDK版本做分支低版本走旧权限路径高版本走新权限动态申请路径。否则在Android 11设备上功能会退化。4.3 配对状态机与“配对框不弹”的常见原因配对流程在系统服务层由BondStateMachine管理状态值对外体现为几个常量BOND_STATE_NONE10表示未配对BOND_STATE_BONDING11表示配对中BOND_STATE_BONDED12表示已配对。一次典型的配对过程App端会收到ACTION_BOND_STATE_CHANGED广播依次看到BONDING到BONDED的状态变化。我在实际项目中遇到过不止一次“配对框不弹”的问题原因往往并不在协议栈而在这几个地方蓝牙App所在进程被系统杀掉或处于受限后台状态配对请求的UI不能正常弹出。某些定制ROM在系统设置里默认关闭了配对确认弹窗改成了自动接受。设备端已经保存过旧Link Key导致配对时双方密钥失配协议栈直接回了Authentication FailureUI层没有机会弹框。排查这类问题时我会先看logcat里蓝牙服务有没有bond state相关日志确认配对请求有没有真正到达蓝牙进程再抓btsnoop看SMP层的配对交互判断是UI层、协议栈还是对端策略的问题。5. 应用层API从权限申请到connectGatt的完整调用链5.1 targetSdk31后一套能跑的BLE权限申请代码应用层的权限申请在Android 12上不能只靠Manifest里声明了事必须走运行时请求。下面这段代码是我在实际项目里验证过的用Activity Result API请求三个权限比传统requestPermissions方式更简洁private val bluetoothPermissions arrayOf( Manifest.permission.BLUETOOTH_SCAN, Manifest.permission.BLUETOOTH_CONNECT, Manifest.permission.ACCESS_FINE_LOCATION ) private val permissionLauncher registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result - val scanGranted result[Manifest.permission.BLUETOOTH_SCAN] true val connectGranted result[Manifest.permission.BLUETOOTH_CONNECT] true val locationGranted result[Manifest.permission.ACCESS_FINE_LOCATION] true if (scanGranted connectGranted locationGranted) { startBleScan() } else { // 提示用户去系统设置补充授权 } } fun requestBluetoothPermission() { val notGranted bluetoothPermissions.filter { ContextCompat.checkSelfPermission(this, it) ! PackageManager.PERMISSION_GRANTED } if (notGranted.isEmpty()) { startBleScan() } else { permissionLauncher.launch(notGranted.toTypedArray()) } }这里有几个细节要注意BLUETOOTH_ADVERTISE我单独没申请因为该场景只做中心设备扫描连接。如果App还要做外设广播必须加进来。另外定位权限的申请时机要放在用户真正执行扫描之前不要在App启动时一股脑全问否则用户拒绝概率很高。5.2 connectGatt后每一层发生了什么附链路表应用层调用connectGatt之后系统内部会经历一串很明确的层级传递。我把每个关键步骤整理成了一张表阶段所在进程关键操作应用调用App进程创建BluetoothGatt对象调用connectGatt()Binder转发system_serverBluetoothManagerService检查权限把请求转给蓝牙进程服务处理com.android.bluetoothBluetoothGattService创建客户端调用JNI的btif_ble_connect协议栈Bluetooth进程L2CAP建立LE连接封装LE Create Connection命令HCI下发Controlleropcode0x200D发送到控制器控制器回Command Status连接完成Controller控制器返回LE Connection Complete事件逐层回调到App理解这个链路最大的好处是连接失败时可以按阶段快速定位如果在LE Connection Complete里看到status不为0说明问题在底层射频或对端设备如果这个事件压根没出现可能连接请求都没发出去如果App的onConnectionStateChange没回调但btsnoop里已经完成了连接那问题就在JNI回调或Binder回传环节。5.3 扫描与连接的最佳实践应用层的代码虽然简单但实际项目中很多问题出在“用错了方式”。我在项目里沉淀了几条经验基本可以避开常见的坑扫描回调用的是onScanResult如果想在这个回调里直接connectGatt务必先调用stopScan停止扫描。频繁的扫描会大量占用射频资源影响连接稳定性。扫描时间不要超过30秒。设备广播通常有间隔长时间扫描既耗电又容易被系统限制不如做周期性的短扫描。连接超时一定要自己控制。connectGatt本身没有内置超时逻辑建议包一层Handler在12秒左右还没收到onConnectionStateChange成功回调时主动断开并提示用户。在Android 12上BLUETOOTH_CONNECT权限的申请结果要持久化判断。用户如果选择了“仅本次允许”下次进App后还要重新申请。6. 三个真实排障案例由底向上的排查思路6.1 扫描无回执先从btsnoop确认命令有没有下发回到开头的案例。排查步骤我再用清单方式完整过一遍方便你照着做打开开发者选项里的蓝牙HCI日志开关复现问题导出btsnoop_hci.log。用Wireshark打开过滤hci_cmd找LE Set Scan Enable。如果这条命令不存在说明问题在HCI层之上。先检查Manifest有没有声明BLUETOOTH_SCAN再检查代码有没有运行时申请它和定位权限。如果命令存在但LE Advertising Report一个都没有再看控制器是否实际收到了广播。可以换一台手机对比或者把设备的广播间隔调短再试。如果报告有但App回调不触发那问题就在JNI回调、BluetoothLeScanner内部的过滤逻辑或者广播接收的进程优先级上。这个案例最后定位到Manifest直接声明了旧权限却没动态申请新权限。修复后在btsnoop里能看到LE Set Scan Enable正常下发紧接着LE Advertising Report开始出现App端扫描也就恢复了。6.2 连接被拒从HCI Error逃不掉另一位朋友遇到的场景是设备能扫到但连接后一两秒就断开App端只收到一个onConnectionStateChange状态变化没法确定原因。抓了btsnoop之后看到Disconnection Complete事件之前出现了Authentication Failure说明断开原因是配对认证没通过。处理办法分两步走先在系统设置里把该设备的蓝牙配对记录删除重新走一次完整配对如果仍然失败再看协议栈日志里SMP层有没有报key missing或key mismatched。很多经典蓝牙耳机在经历过固件升级后本机还保留着旧Link Key就会导致认证失败删除重配通常能解决。如果反复出现就要怀疑对端设备固件的问题而不是Android侧代码。6.3 老代码在Android 12上失效权限模型迁移最后说一个很多人会踩的典型场景targetSdk从30升到31后原来好好的蓝牙扫描突然失灵而且没有任何报错。原因是权限模型迁移后新权限和旧权限在Android 12上有着完全不同的语义。旧的BLUETOOTH权限在API 31上虽然被系统自动授予但它已经不能代表“允许扫描”的授权只有动态申请BLUETOOTH_SCAN并通过用户同意后扫描能力才真正打开。迁移建议是保留Manifest里的旧权限以兼容低版本同时新增三个新权限代码里通过Build.VERSION.SDK_INT 31判断走哪套逻辑权限申请失败时的引导文案要明确说明“用于发现附近的蓝牙设备”避免用户误解。如果应用同时使用BLE广播别忘了BLUETOOTH_ADVERTISE也是独立申请的否则广播功能在Android 12上会静默失效。这几个案例给我的共同教训是蓝牙问题的排查顺序永远是从底层事实往上层逻辑推。先把btsnoop抓到手里看看HCI层到底发生了什么再决定是去改应用代码、系统配置还是直接换设备测试。HCI日志不撒谎它给出的信息往往比logcat里那些花哨的错误码可靠得多。调试蓝牙时把HCI日志养成常开的习惯等真出问题时你已经握住了通往真相最快的钥匙。