61 · 一台蓝牙设备的 Android 身份 — 读 CachedBluetoothDevice 前的地基
本篇是整个精讲系列的地基:不读懂”一台蓝牙设备在 Android 里有几层身份”,后面所有字段和方法都会看成一锅粥。 协议层细节(BR/EDR/BLE/SSP/UUID 的规范含义)已由 10-蓝牙协议速成、11-配对与SSP安全机制、12-Profile体系与车机角色 覆盖,本篇不重复,只讲 CachedBluetoothDevice 视角下的状态模型。
0. 一个生活类比:微信交友三步
一台蓝牙设备从”陌生”到”能用”,要走三步,每一步独立存在、互不隐含:
| 步骤 | 微信类比 | 蓝牙术语 | Android API |
|---|---|---|---|
| ① 看见 | 搜到附近的人 | 发现(discovery) | ACTION_FOUND 广播 |
| ② 加好友 | 互加好友(交换密钥) | 配对(bond/pair) | createBond() → BOND_BONDED |
| ③ 开业务 | 发消息/打语音/传文件 | 连接 profile(connect) | BluetoothDevice.connect() |
新手最大的误区就是把 ②③ 混为一谈:“配对成功 = 连上了”。错。配对只是拿到钥匙(Link Key),连接才是开门进去用服务。你可以和一台设备保持 bonded(好友关系)但从不连接(从不开聊);反过来,没配对基本没法连 profile(除了一些 LE 场景)。
而在 ② 和 ③ 之间,还有一个隐形的底层步骤:
② 配对(bond) ──→ 【物理链路(ACL)】 ──→ ③ 服务连接(profile)
交换密钥 架起无线电路 在电路上开业务通道
ACL(Asynchronous Connection-Less)是承载一切 profile 的物理链路。它对 App 层基本透明,但 CachedBluetoothDevice 用两个字段盯着它(mIsAclConnectedBrEdr / mIsAclConnectedLe,:153-154)——因为自动回连的时间窗是从 ACL 建立时刻起算的(见 66 章)。
1. 三层状态独立:这个类的字段就是按这个骨架长的
把 62 章 的字段分组往这三层上一贴,骨架立刻清晰:
flowchart TD subgraph L1["第①层 关系层 bond"] B1["mBondTimestamp :118<br/>配对成功时刻"] B2["getBondState() :908<br/>↔ binder 实时查询"] end subgraph L2["第②层 链路层 ACL"] A1["mIsAclConnectedBrEdr :153<br/>经典蓝牙物理链路"] A2["mIsAclConnectedLe :154<br/>低功耗物理链路"] A3["mConnectAttempted :150<br/>首个 ACL 建立时刻<br/>(自动回连窗口起点)"] end subgraph L3["第③层 业务层 profile"] C1["mProfiles :127<br/>这台设备支持哪些服务"] C2["mIsActiveDevice* :157-160<br/>是不是当前输出目标"] C3["mIs*ProfileConnectedFail :162-165<br/>服务连接失败标志"] end L1 --> L2 --> L3
三层独立 = 8 种合法组合,每种你都会在真实日志里遇到:
| bond | ACL | profile | 现实场景 |
|---|---|---|---|
| ✅ | ✅ | ✅ | 正常使用中(听歌/通话) |
| ✅ | ✅ | ❌ | 配对刚完成,SDP 还没跑完——mProfiles 还是空的,connectDevice() 会直接放弃等 UUID(:599-609) |
| ✅ | ❌ | ❌ | “已配对未连接”——列表里显示”已保存”的常态 |
| ❌ | ✅ | ❌ | 对端主动发起的临时链路(OPP 传文件前置),f3dif 只在 ACL_CONNECTED 时为此建档(BEM:590) |
| ✅ | 🔄 | 🔄 | 正在连接:ACL 已通、profile CONNECTING 中——看门狗已上弦(62 §8) |
| ✅ | ✅ | ⚠️ | 连接失败:60s 看门狗到期,mIs*ProfileConnectedFail 置位 |
| ❌ | ❌ | ❌ | 只在扫描结果里见过的陌生设备(mJustDiscovered=true) |
| 🔄 | — | — | 正在配对:BOND_BONDING 中——isBusy() 为 true 的来源之一(:1043-1054) |
排障时先定位”卡在哪一层”,再看对应字段/日志——这是本系列反复使用的方法论。
2. BluetoothDevice vs CachedBluetoothDevice:瘦身份证 vs 厚档案
framework 的 android.bluetooth.BluetoothDevice 是一张瘦身份证:本质就是 MAC 地址的封装 + 一堆在线查询方法(每次调用都是同步 binder IPC,跨进程问 BluetoothManagerService)。
CachedBluetoothDevice 是给这张身份证配的厚档案,多出来的东西分三类:
- 事件痕迹(大部分字段):
onXxxChanged系列回调到达后留下的印记——mJustDiscovered、mIsActiveDevice*、ACL 双标志、失败标志……(62 §0 总纲:字段 = 缓存的事件痕迹) - 算好的缓存:
mProfiles(远端 UUID × 本地支持 的交集,由LocalBluetoothProfileManager.updateProfiles算好塞回来)、mDrawableCache(图标) - 包装的操作:
connect()/disconnect()/unpair()/setActive()把多步 framework 操作组合成一个语义动作
⚠️ 关键认知:哪些是”缓存”,哪些是”透传”
不是所有 getXxx 都读缓存。分两类(这是 68 §1 MANNPROB-2403 的根因):
| 类型 | 方法 | 行为 |
|---|---|---|
| 透传(在线查询) | getBondState()(:908)、getBtClass()(:1276)、getBatteryLevel()(:836)、getName()(:753,实时 getAlias()) | 每次调用 → binder → framework。adapter 已 OFF 时返回 BOND_NONE/null——在错误的时机调用就拿到”骗人”的值 |
| 读缓存 | mProfiles 系(getProfiles :1283)、mIsActiveDevice*(isActiveDevice :974)、ACL 双标志、失败标志 | 读上次事件留下的值。事件错过就永久陈旧(68 §4 MANNPROB-3492 的温床) |
两种都能”骗人”,骗法不同:透传在时机不对时骗你,缓存在事件丢了时骗你。排障第一步永远是判断你读的是哪一类。
3. 设备的”能力清单”从哪来:UUID → profile
mProfiles 是这台设备能干什么的清单(音乐?通话?通讯录?)。它的生成链:
配对完成后,framework 做 SDP 服务发现(问对方:"你支持哪些服务?")
→ 对端回答一串 UUID(每个 UUID = 一种服务的编号,如 HFP、A2DP)
→ ACTION_UUID 广播 → onUuidChanged()(:1109)
→ updateProfiles()(:1056):远端 UUID × 本地 adapter 支持的 profile
→ LocalBluetoothProfileManager.updateProfiles(LBPM:634)重建 mProfiles
三个要点:
- 交集逻辑:远端支持 × 本端支持,缺一不可。车机不支持的服务(本地 supportedList 没有)永不实例化——profile 注册本身就是动态的(
updateLocalProfiles按getSupportedProfiles()决定,LBPM:142-288)。 - SDP 需要时间:这就是配对刚完成时
mProfiles为空的原因,也是MAX_*_DELAY_FOR_AUTO_CONNECT一族耐心常量存在的理由(66 章)。 - UUID 会迟到甚至不来:所以
connectDevice()对空列表的处理是”先放弃、等广播”,而不是硬连(:599-609 注释:防与 carkit 配对期 UUID 竞态)。
profile 家族与车机角色(车机当 HF 客户端连手机、当 A2DP 音源输出到耳机……)详见 12-Profile体系与车机角色。
4. 状态查询方法速查(这个类对外的主流读法)
| 想知道什么 | 方法(行号) | 实现要点 |
|---|---|---|
| 配对了吗 | getBondState()(:908) | binder 直通,无缓存 |
| 连上了吗(任一 profile) | isConnected()(:1003) | 锁内遍历 mProfiles,任一 STATE_CONNECTED |
| 某 profile 连上了吗 | isConnectedProfile(profile)(:1022/:1034) | 委托 profile 查询 |
| 正忙吗 | isBusy()(:1043-1054) | 存在 CONNECTING/DISCONNECTING 或 BOND_BONDING——注意含断开中(34 篇 摘要状态的派生语义就靠它) |
| 全部 profile 里最高的连接态 | getMaxConnectionState()(:1423) | 给 UI 排序/摘要用 |
| 是当前输出设备吗 | isActiveDevice(profile)(:974) | 读 4 个 active 位 |
| 连接失败了吗 | isProfileConnectedFail()(:2210) | 4 失败标志 + isEnabled 综合(注意常驻 Log.d,62 §8) |
| 设备叫什么 | getName()(:753) | 实时 getAlias()(读时取,不缓存) |
| 是什么类型的设备 | getBtClass()(:1276) | BluetoothClass 直读(CoD,设备类) |
| 信号多强 | mRssi(:123) | 上次扫描广播的值,会过期 |
| 调试对设备 | toString()(:1356-1368) | 匿名地址/名字/groupId/member——日志取证神器 |
| 排序 | compareTo(:1386-1406) | 已连接 > 已配对 > justDiscovered > RSSI > 名字 |
5. 自测
- 配对和连接的区别是什么?(钥匙 vs 开门;bond ≠ connected)
- “配对成功但列表显示还没连上、图标却已经能显示手机形状”——哪层数据已经就位?(bond 层 + CoD;profile 层还没好)
-
getBondState()和isConnected()哪个是缓存?(都不是——前者 binder 透传,后者遍历 mProfiles 算) - ACL 通了但没有任何 profile 连接,合法吗?(合法——SDP/连接进行中的中间态)
-
mProfiles的内容由谁计算?(远端 UUID × 本地支持的交集,LBPM:634 重建)
下一篇 62-字段逐个精讲:带着这张三层地图,去逐个认识那 41 个实例字段。