
Android 12 的蓝牙框架从 HCI 到应用层我前后啃了两周多才把整条链路理顺。这篇文章不聊泛泛的架构图而是按我实际读 AOSP 源码和调试设备的顺序把 Android 12 蓝牙框架的各个分层、关键机制、应用层 API、安全模型以及排错手段串成一条完整主线。如果你正准备做蓝牙相关的系统定制、应用开发或者只是被蓝牙 Bug 折磨到怀疑人生这篇文章应该能帮你省下不少时间。先说一个结论Android 12 蓝牙框架最大的变化不是某个 API 改名而是整条链路的模块化重构。从控制器固件到应用进程每层都有各自的状态机、事件回调和缓冲机制任何一个环节出错现象都可能是“扫描不到设备”或“连接一直失败”。所以理解分层是第一步也是最关键的一步。1. 开篇先看全景Android 12 蓝牙框架的分层与定位1.1 从控制器到界面整条链路走一遍如果要把 Android 12 蓝牙框架从上到下分层我习惯这么划分应用层、系统服务层、协议栈层、HCI 层、控制器层。应用层就是你在 Android Studio 里写的那堆BluetoothAdapter、BluetoothLeScanner、BluetoothGatt代码这些 API 通过 Binder 调用系统服务BluetoothManagerService。系统服务层负责管理蓝牙开关状态、设备配对绑定、配置文件持久化同时持有对蓝牙进程的绑定关系。蓝牙进程里跑的是AdapterService和真正的协议栈实现libbluetooth这一层处理 L2CAP、GATT、RFCOMM、A2DP 等协议细节。协议栈往下就是 HCIHost Controller Interface这是软件和蓝牙控制器之间的分界线。协议栈把逻辑数据打包成 HCI 命令或 ACL 数据包通过 UART、USB 或 SDIO 发送给蓝牙 SoC蓝牙 SoC 执行完命令后再通过 HCI 事件包把结果返回给协议栈。控制器层就是蓝牙芯片内部固件包括链路层、基带、射频管理等部分。这套分层每个环节都有各自的生命周期。你在应用层调一个startDiscovery()它不会直接命令控制器开始扫描而是先经过系统服务状态检查再通知协议栈创建发现流程协议栈发送 HCI 命令给控制器控制器执行扫描并回报事件事件再一层层回调回应用层。中间任何一层丢一个回调应用层就可能永远等不到结果。1.2 Android 12 的新变化蓝牙模块 APEX 化Android 12 AOSP 里最值得注意的改动是蓝牙模块被拆成了可以独立更新的 Mainline 模块以 APEX 格式打包。以前你要更新蓝牙协议栈基本上只能等系统 OTA现在com.google.android.bluetooth这个模块和com.google.android.bluetooth.apex包可以独立升级修复蓝牙协议栈问题不再依赖整个系统镜像重发。这一点对做系统开发的兄弟影响很大。过去改蓝牙源码要看整包编译现在主要盯着packages/modules/Bluetooth这一棵目录就行编译产物也会单独输出。模块化的代价是模块边界更严格分层接口必须稳定Framework 层与协议栈之间改动的风险比以前大。你在 AOSP 里看代码时会发现很多接口定义在packages/modules/Bluetooth/framework下面而不是直接放在frameworks/base这就是模块化重构后的结果。另外Android 12 在隐私权限上还有一个显著变化扫描 BLE 设备不再强制要求定位权限取而代之的是BLUETOOTH_SCAN、BLUETOOTH_CONNECT、BLUETOOTH_ADVERTISE这一组运行时权限。这个在应用层开发部分我会详细说但它本质上反映出 Android 正在把“蓝牙”从“位置”里剥离开从系统框架层面降低权限滥用。2. HCI 层蓝牙控制器与协议栈之间的“交通要道”2.1 HCI 数据包长什么样HCI 层的核心是数据包格式。协议栈和控制器之间通过三种包通信命令包、事件包、数据包。命令包是从 Host 发给 Controller 的比如“开启扫描”“建立连接”事件包是 Controller 回应 Host 的比如“扫描发现设备”“连接完成”数据包则是实际传输的 ACL、SCO 或 ISO 数据。HCI 命令包的格式很容易理解前面是 2 字节的 Opcode分为 6 位 OGFOpcode Group Field和 10 位 OCFOpcode Command Field后面跟着参数总长度和具体参数。拿LE Create Connection命令举例OPcode 是0x200DOGF 是0x08LE Controller 命令组OCF 是0x000D。参数里要带上扫描间隔、窗口、主动/被动扫描策略、对端地址类型等一堆东西。你在看 btsnoop 日志时看到200D开头就能知道这是尝试建立 BLE 连接。事件包的第一字节是事件码常见的有0x0ECommand Complete、0x0FCommand Status、0x3ELE Meta Event。比如 LE 扫描发现一个设备Controller 会主动上报0x3E事件子事件码0x02是LE Advertising Report。这些 event 里的内容比如 RSSI、广播数据、地址类型最终会被协议栈解析成一个BluetoothDevice回调到应用层。2.2 传输载体与收包流程HCI 不是一种物理总线它只是一套协议约定底层传输可以用 UART、USB、SDIO甚至内存共享。Android 设备上最常见的载体是 UART尤其是经典蓝牙和 BLE 共存的方案里蓝牙 SoC 经常通过 UART 接到主控拨号就是这样。因为 UART 是字节流没有报文边界所以 H4 协议会在每个 HCI 包前面加一个 1 字节的包类型标识0x01表示命令包0x02表示 ACL 数据包0x04表示事件包。Android 的蓝牙 HAL 在这套物理传输之上又抽象了一层。从 Android 8 开始走 HIDL到 Android 12 这部分已经迁移到 AIDL。应用层要发送命令时流程是框架进程内的协议栈把 HCI 包写好通过 AIDL 接口交给 vendor 蓝牙 HALHAL 再驱动串口或 USB 控制器把字节发出去。反方向控制器有事件要通知时通过中断触发读取HAL 把数据交给协议栈的回调函数协议栈再把事件分发到各个 Profile。调试时最容易踩的坑是不知道到底该抓哪一层日志。如果 HCI 层没有发出去问题通常在协议栈或 HAL如果 HCI 命令发出去了但控制器没有任何响应那大概率是控制器固件或射频问题。所以拿到一个蓝牙 Bug第一步永远是确认 HCI 层的行为而不是在应用层反复看回调。2.3 从 btsnoop 日志看 HCI 现场我在实战里最依赖的工具是 btsnoop 日志。Android 开发者选项里有一个“蓝牙 HCI 信息收集日志”打开后系统会把所有 HCI 收发包记录到/sdcard/btsnoop_hci.log或者打包进 bugreport 里。这份日志本质上就是一个类似 pcap 的抓包文件可以用 Wireshark 打开。打开 btsnoop 之后你第一条要练出来的技能是看一眼命令和事件的对齐关系。比如说你发了一条LE Set Scan ParametersController 应该回一个 Command Complete并且状态码是0x00成功如果状态码是0x0CCommand Disallowed说明当前的链路状态不允许这个命令比如还在连接过程中不能重新扫描。很多“扫描失败”的案子不用读任何代码光看 btsnoop 就能定位到是协议栈乱发命令还是控制器固件不支持。抓日志时也有几个细节要注意。开发者选项里的 HCI snoop log 默认是关闭的打开后要重启蓝牙才生效复现问题后最好立刻抓 bugreport不然环形缓冲区可能把关键日志覆盖掉。还有如果你的设备蓝牙功能由 vendor 的 HAL 层直接处理HCI 日志不一定完整这种情况下只能配合串口日志或者 vendor 自己的 log 系统来看。3. 协议栈内部L2CAP、GATT 与经典蓝牙协议的关系3.1 L2CAP 怎么把数据“分道”送出去HCI 层负责把字节交给控制器但控制器本身不理解“服务”“UUID”这些概念。真正负责把上层数据和具体协议通道绑定的是 L2CAPLogical Link Control and Adaptation Protocol。L2CAP 在我的理解里就是个多路复用器有点像快递分拣中心上面各路的协议数据来了它会按协议类型打上标签再拼接成一个个 PDU 送进 HCI收到对端数据时它又根据 PDU 里的通道 ID 决定交给哪个上层协议。通道 IDCID是 L2CAP 的关键概念。BLE 里有几个固定信道比如0x0004是 ATT 协议信道0x0005是 LE L2CAP 信令信道0x0006是 SMP 安全管理信道。而经典蓝牙是通过 PSMProtocol/Service Multiplexer来选择协议的比如 RFCOMM 的 PSM 是0x0003AVDTP 的 PSM 是0x0019。连接导向通道建立后L2CAP 还要处理分段与重组。BLE 的 ATT 包最大只有 23 字节的默认 MTU但你要读一个几百字节的特征值不可能一个包塞进去。L2CAP 会负责把大包分成多个小片段通过 HCI ACL 包依次发送接收端再按顺序拼回来。Android 开发里常见的“MTU 协商失败导致读写不了长数据”说到底就是 L2CAP 这一段没配合好。Android 12 的协议栈代码里L2CAP 这部分逻辑依然在system/bt/stack/l2cap目录下。虽然模块化重构后目录结构有调整但核心设计没有推翻你按 L2CAP 信令事件流去读依然能看到经典的参数协商过程。3.2 GATT 与 ATTBLE 应用的核心通道应用层做 BLE 开发时接触最多的就是 GATT。GATT 本身定义的是数据组织和访问规则设备间通过 Service、Characteristic、Descriptor 三层结构描述能力比如一个心率计暴露 Heart Rate Service里面有 Heart Rate Measurement 特征和 Body Sensor Location 特征。但 GATT 自己不会传输数据真正的读写操作由底层 ATTAttribute Protocol完成。ATT 就像一组“访问属性表”的指令集GATT 则规定了属性表怎么组织。ATT 协议的请求响应模式很有意思它是一个简单的“一问一答”协议。应用层读一个特征值本质上是发起一个Read Request服务端返回Read Response写一个特征值就发起Write Request或Write Command。Android 12 的BluetoothGatt.readCharacteristic()回调里拿到的 byte 数组就是 ATT 协议一层层拆出来的结果。很多东西在代码里不明显但从 ATT 层看就很好理解。比如 GATT 客户端最多同时有一个待处理的请求你连续调两次readCharacteristic会导致第二个请求被挂起再比如 MTU 协商它其实是在 ATT 层协商的BluetoothGatt.requestMtu()只是把 Exchange MTU Request 发给对端收到响应后系统会自动更新后续 ATT 包的长度上限。3.3 经典蓝牙协议栈RFCOMM、A2DP、HFP很多人以为 Android 蓝牙都在搞 BLE其实经典蓝牙协议一样在协议栈里占着重要位置。RFCOMM 是基于 L2CAP 的串口仿真协议在 SPPSerial Port Profile里用得最多很多工业设备、车机或者串口透传模块就是靠 RFCOMM 传数据的。Android 的BluetoothSocket里createRfcommSocketToServiceRecord()最后就是通过 RFCOMM 建立连接。A2DP 是音频传输协议里面要协调编码器、流状态和音频时钟。Android 12 在音频链路上有个明显变化蓝牙音频开始全面支持 LE Audio 的方向A2DP 这个经典链路虽然仍是主流但系统框架已经在为音频 offload 和延迟控制做准备。HFP 则负责通话音频与电话接口、音频策略密切相关。经典蓝牙这些 Profile 在应用层暴露得比 BLE 少但排查问题时要心里有数。比如一台车机连上手机不出声音问题可能不是 A2DP 连接失败而是 AVRCP 的控制通道没建立导致手机认为媒体信息没送达。这种问题看 HCI 日志不一定能直接发现要结合协议栈的 profile 状态日志一起看。3.4 协议栈与 HCI 之间的消息流转为了把协议栈和 HCI 的关系说透我直接写一个“发起 BLE 连接”的完整消息流。应用层调用connectGatt()之后框架先建立到蓝牙进程的绑定接着协议栈通过 L2CAP 层的管理器发出LE Create Connection命令。HCI 层把这个命令封装成0x200D的 Command Packet交给 HAL 发送给控制器。控制器开始寻呼对端设备后HCI 层会收到Command Status事件表示连接流程已经开始但还没成功。连接成功时控制器发送LE Connection Complete事件。协议栈拿到事件后首先更新链路状态然后启动 GATT 层的服务发现流程也就是发送Exchange MTU、Read By Group Type等 ATT 请求。等到服务发现完成系统才会回调onConnectionStateChange()和onServicesDiscovered()应用层这才算真正“连上”了。这个流转过程说明一个事实应用层回调的时机往往比 HCI 层的实际状态慢好几拍。如果你在onConnectionStateChange里立刻发数据可能底层 ATT 通道还没准备好。遇到这种情况经验之谈是等onServicesDiscovered或手动discoverServices()回调后再操作。4. 系统服务与框架BluetoothManagerService 的角色4.1 蓝牙开关、配对与状态机系统服务层是应用层和协议栈之间的门卫。BluetoothManagerService是 Android 里最核心的蓝牙系统服务负责管理蓝牙适配器的开关状态也维护一个状态机。这个状态机的状态包括STATE_OFF、STATE_TURNING_ON、STATE_ON、STATE_TURNING_OFF应用层的BluetoothAdapter.isEnabled()和系统设置里的蓝牙开关读的都是这个状态。开关蓝牙只是它的基础职责。配对流程也是由它管理的比如createBond()最终通过 Binder 调用到BluetoothDeviceService然后往协议栈下发配对请求。系统服务层还负责保存配对绑定数据你在系统设置里看到的“已配对的设备”列表就是它从数据库读出来的。调试时用adb shell dumpsys bluetooth_manager输出能看到这个服务的很多状态当前 adapter 状态、bonded devices 列表、正在处理的 profile 连接、甚至广播接收器的注册情况。这个命令在定位“蓝牙开关一直打不开”的问题时特别有用因为它能区分是 framework 状态问题还是底层协议栈死掉导致状态回不到 ON。4.2 AdapterService 与底层协议栈的绑定BluetoothManagerService虽然是系统服务但真正依赖的蓝牙进程是独立运行的。Android 里蓝牙功能跑在com.android.bluetooth这个系统 App 进程里里面有AdapterService、A2dpService、GattService等一堆服务。系统蓝牙框架启动时BluetoothManagerService会 bind 到com.android.bluetooth进程也就是通过BluetoothAdapterService接口建立连接。这就造成了一个开发中很常见的现象如果你杀掉了com.android.bluetooth进程蓝牙开关就死在那里打不开。因为在 Android 12 上系统服务和协议栈是分离的两个进程协议栈挂掉并不会自动拉起。描述这个问题时我会先在logcat里搜索BluetoothManagerService和BluetoothAdapterService关键 log确认 bind 是否成功而不是直接去看 HCI。AdapterService启动后它会加载 JNI 库也就是libbluetooth_jni这个 JNI 再调用核心协议栈库libbluetooth。Android 12 的模块化重构之后这些底层库都封装在 Bluetooth APEX 模块内。所以做系统定制时如果你只看应用层代码根本摸不到协议栈必须知道 JNI 和进程间调用的链路。4.3 模块化的管理从系统 App 到 APEX 更新Android 12 把蓝牙模块 APEX 化之后整个更新流程也变了。过去蓝牙框架和协议栈是系统镜像的一部分只能跟着 OTA 走现在com.google.android.bluetooth模块的更新包可以直接通过应用商店或系统更新模块单独下发。这也意味着Google 可以在不发布完整系统版本的情况下修复大范围的蓝牙安全漏洞。模块化对权限也有影响。系统服务层和应用层之间的 Binder 调用现在多了模块间的签名校验和权限限制。你自定义一个第三方 App 去调BluetoothManagerService的隐藏 APIAndroid 12 通常会抛SecurityException除非你加签名权限。所以做厂商应用时要么直接走公开 API要么以系统签名身份运行。模块化还带来一个细节packages/modules/Bluetooth的结构和system/bt有明显差异。做源码移植时最好先读packages/modules/Bluetooth/android/app和packages/modules/Bluetooth/system下面的结构再对照旧的体系去迁移。很多新手直接拿 Android 11 的代码去找某个文件结果发现路径变了就懵了。5. 应用层实战权限、扫描、连接与配对5.1 Android 12 的蓝牙权限模型Android 12 对蓝牙权限的改造是开发者在适配时最容易出问题的地方。以前ACCESS_FINE_LOCATION是 BLE 扫描的必需品到了 Android 12系统引入了BLUETOOTH_SCAN、BLUETOOTH_CONNECT、BLUETOOTH_ADVERTISE这三个邻近设备相关权限把蓝牙能力从定位权限里剥离出来。如果你的应用 targetSdkVersion 是 31 或更高Manifest 里声明权限时要注意BLUETOOTH_SCAN和BLUETOOTH_CONNECT都是运行时权限需要像请求定位一样动态申请。而且BLUETOOTH_SCAN在声明时不要加usesPermissionFlagsneverForLocation否则在部分逻辑上会被系统判定为只能用于测距不能用于常规扫描导致某些外设连不上的怪问题。Manifest 写法大致是这样uses-permission android:nameandroid.permission.BLUETOOTH_SCAN android:usesPermissionFlagsneverForLocation / uses-permission android:nameandroid.permission.BLUETOOTH_CONNECT / uses-permission android:nameandroid.permission.BLUETOOTH_ADVERTISE /注意neverForLocation这个 flag 是把双刃剑。如果你只是做设备连接不加也没问题但如果你的应用在 Google Play 上架审核可能会要求你说明为什么需要扫描蓝牙而不用定位。实际开发我发现有很多老项目还残留着ACCESS_FINE_LOCATION这时候最好一起保留兼容 Android 11 及以下的设备同时申请新权限以避免 Android 12 上扫描不到。5.2 扫描周边设备经典与 BLE 的差异很多人以为扫描蓝牙就是一个startDiscovery其实经典蓝牙和 BLE 的扫描路径完全不同。经典蓝牙用BluetoothAdapter.startDiscovery()发现的是所有 BR/EDR 设备通过广播BluetoothDevice.ACTION_FOUND上报结果BLE 用BluetoothLeScanner.startScan()走回调接口支持更精细的过滤和筛选。写经典蓝牙扫描时一个常见的坑是只调了startDiscovery()但忘了在onDestroy里调cancelDiscovery()导致后台持续扫描耗电甚至触发系统扫描频率限制。Android 12 上普通应用在后台执行蓝牙扫描也有严格限制前台服务都不一定能豁免。BLE 扫描示例val scanCallback object : ScanCallback() { override fun onScanResult(callbackType: Int, result: ScanResult) { val device result.device val rssi result.rssi val scanRecord result.scanRecord?.bytes // 处理扫描结果 } } val settings ScanSettings.Builder() .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) .build() val filters mutableListOfScanFilter().apply { add(ScanFilter.Builder().setDeviceName(MyDevice).build()) } bluetoothLeScanner.startScan(filters, settings, scanCallback)除非有非常明确的 filter不然建议扫描结果上来后先做一次服务 UUID 判断而不是直接把第一个设备拿去连接。BLE 广播数据里有很多杂散设备过滤得太宽会严重拖慢连接速度。5.3 GATT 连接与数据读写GATT 连接是 BLE 应用的核心。用BluetoothDevice.connectGatt(context, false, gattCallback)发起连接连接结果在onConnectionStateChange回调里。Android 12 上要注意一个新的行为变化connectGatt的 autoConnect 参数如果传true系统会进入后台白名单扫描模式连接速度会变慢如果传false它会立即尝试直接连接但设备不在附近时容易快速失败。连接成功后再调discoverServices()枚举对端设备的 GATT 服务表。读取特征值时readCharacteristic的结果要通过onCharacteristicRead回调拿到不能指望函数返回值写数据可以通过writeCharacteristic但有 Write Type 的区别WRITE_TYPE_DEFAULT需要等待对端确认WRITE_TYPE_NO_RESPONSE则只发不确认适合高吞吐场景。GATT 读写的时序问题是新手最头疼的。ATT 协议本身不支持并发请求所以在onCharacteristicWrite回调里再发下一条读或写是标准的串行处理姿势。如果硬要并发写结果通常是133错误本质就是 ATT 超时或请求重入。5.4 配对流程与状态广播配对分主动和被动两种情况。主动配对是应用层调用device.createBond()被动配对是对方设备发起配对请求系统会弹对话框用户确认后完成。不管哪种你都需要监听BluetoothDevice.ACTION_BOND_STATE_CHANGED广播从EXTRA_BOND_STATE拿当前状态。Android 12 上做配对广播接收时要特别注意BLUETOOTH_CONNECT权限。如果你的应用没有这个权限注册的广播接收器可能根本收不到配对状态变化。这也是为什么我在讲权限模型时反复强调Android 12 的权限不只是“扫描”用“连接”和“配对”同样被独立管控。配对失败时EXTRA_REASON会带一个错误码。常见的几个AUTH_FAILED8表示认证失败可能是 PIN 码不一致或对端安全策略拒绝AUTH_REJECTED9表示对方主动取消REMOTE_DEVICE_DOWN11表示设备关机或超出范围。看到这些错误码先不用怀疑代码大概率是设备端的安全模型不匹配。6. 安全框架配对模型、加密与绑定持久化6.1 配对模型与 IO Capability蓝牙配对不是简单地“输一下密码”它内部有一套完整的密钥协商机制。经典蓝牙用 SSPSecure Simple PairingBLE 用 SMPSecurity Manager Protocol配对模型有四种Just Works、Passkey Entry、Numeric Comparison、Out of Band。选哪种模型取决于设备的 IO Capability也就是它能不能显示数字、能不能输入数字、有没有确认按钮。举个例子一块运动手环只有屏幕没有输入键盘它的 IO Capability 通常是 DisplayOnly配对时走 Just Works 模型不提供 MITM 保护智能门锁通常有数字键盘IO Capability 是 KeyboardOnly配对时走 Passkey Entry 模型一方显示密码另一方输入。如果两边配置不对比如都只能显示不能输入系统会回退到 Just Works安全性下降。Android 12 的系统设置里蓝牙配对对话框本身会根据设备类型选择交互方式。做外设开发时要提前想清楚自己设备的 IO Capability并在 GAP 层广播里正确声明否则配对体验会很别扭。6.2 LE Privacy 与地址解析BLE 的隐私保护主要靠随机地址。设备可以周期性地更换自己的 MAC 地址甚至换成不可解析的随机地址这样第三方无法持续跟踪。对开发者来说最直观的影响就是你不能把 BLE MAC 地址当作设备的唯一标识来存储因为应用重启后系统的随机地址早就变了。Android 12 的BluetoothDevice.getAddress()返回的地址在非 bonded 状态下可能是一个 RPAResolvable Private Address应用无法直接用这个地址去重。正确做法是根据广播数据里的服务 UUID、设备名称或者自定义 Manufacturer Data 做识别连接成功后再保存 bonded 状态下的地址。绑定之后设备双方会交换 IRKIdentity Resolving Key。Controller 拿到 IRK 后后续扫描到的 RPA 地址如果能用 IRK 解析出来就能还原出真实的 identity address。这个解析过程由链路层完成协议栈层感知不到。6.3 绑定数据存储与恢复绑定并不是连接完成后就结束了密钥需要持久化存储。Android 里绑定数据保存在蓝牙进程的存储区域通常用加密方式写进数据库或配置文件用户擦除蓝牙共享数据、恢复出厂设置时会清空。实际项目里我发现一个经常被忽略的问题是手机恢复出厂或清除蓝牙数据库后外设端还保留着旧的绑定密钥。这种“半绑定”状态会导致连接时认证失败外设一直报错但手机端显示的却是“连接失败”。解法通常是让外设端也清一次绑定列表或者让手机端先“忽略设备”再重新扫描。Android 12 的备份恢复机制对蓝牙绑定数据也做了处理但厂商定制时经常把它禁用掉。如果你发现用户换机后老设备连不上新手机就要考虑是不是绑定密钥没有正确迁移或者外设端还存着旧手机的身份信息。7. 全链路排查从日志到问题定位7.1 logcat 与 dumpsys 的配合进到实际问题排查环节我一直坚持从软件栈上层往下看。第一步先抓 logcat重点过滤这几个 tagBluetoothManagerService、BluetoothAdapter、bt_stack、bt_btif。其中bt_stack是协议栈的 C 日志bt_btif是蓝牙接口层的日志很多底层问题都会在这里留下现场。比如连接失败时bt_stack经常会打出btif_ble_observer或l2c_link_check_timeout之类的关键信息。这些东西光靠 logcat 可能不够直观所以我一般同时跑adb shell dumpsys bluetooth_manager拿到当前 adapter 状态、绑定的 profile 服务、甚至最近的扫描结果。两者一对照基本能判断问题出在 framework、协议栈还是应用层。有一个经验如果 logcat 里协议栈日志完全消失说明蓝牙进程可能已经崩了或正在重启。这时候不要纠结某一行代码先抓adb shell ps -A | grep bluetooth确认蓝牙进程是否还活着。7.2 btsnoop 分析实战btsnoop 在 HCI 层排查中是王炸级工具。打开开发者选项里的 HCI snoop log复现问题然后导出 log用 Wireshark 打开。Wireshark 里最常用的过滤器是bluetooth和btle能分别过滤经典蓝牙和 BLE 的包。我拿一个真实案例说明。某次设备反复出现“连接成功后 3 秒断开”宏观日志全是“connection lost”完全看不出原因。打开 btsnoop 后发现 Controller 在连接完成后立刻回了一个Disconnection Completereason code 是0x08Connection Timeout。再往上翻发现链路建立后第一次 ACL 包携带的 MTU 值过大对端不支持导致链路层被控制器判定为无响应。这个链路层问题在应用层只表现为“连接失败”但从 btsnoop 一看就清清楚楚。抓了日志以后还要养成习惯把 btsnoop 里的时间戳和 logcat 里的系统时间对齐。蓝牙协议栈和 Android framework 日志的时间基点不太一样直接按时间对比容易对不上。我一般以 logcat 时间为主线根据 HCI 事件的时间戳倒推几百毫秒寻找对应的应用层调用。7.3 高频问题速查现象可能原因排查方向BLE 扫描无结果未申请BLUETOOTH_SCAN或应用在后台受限检查运行时权限、应用前后台状态连接失败 133GATT 请求超时ATT 层无响应调大连接参数、确认对端响应速度配对老是失败双方 IO Capability 不匹配或绑定残留检查 GAP 配置清理两端的绑定蓝牙开关无法打开蓝牙进程崩溃或 HAL 异常dumpsys bluetooth_manager、重启蓝牙进程经典蓝牙频繁断连Wi-Fi 5G 干扰、固件版本老换信道、升级控制器固件这些高频问题里我最想强调的还是“先抓日志再改代码”。很多蓝牙问题根本不是应用层代码的锅而是底层状态机或射频环境的问题。你拿到一个 Bug先按 HCI-协议栈-系统服务-应用层的顺序逐层排除而不是一上来就怀疑自己的回调写错了。另外Android 12 的adb shell activity manager相关调试命令也值得用但蓝牙生态里真正可信的还是 btsnoop 和 dumpsys。两者配合能覆盖从控制器到应用层的完整链路。最后分享一个我自己的习惯每次排查蓝牙问题我都会先把 btsnoop 日志存一份副本标好日期和问题现象再开始分析。蓝牙问题往往带有偶发性过几天再复现时手上有历史日志就有了对比基准。刚接触这套框架的人我建议也别急着把整个 AOSP 蓝牙代码全读完先抓一份完整的连接过程 btsnoop在 Wireshark 里把扫描、连接、服务发现、配对这几个事件找出来再回头对照源码整个框架的脉络会清晰很多。