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 种合法组合,每种你都会在真实日志里遇到:

bondACLprofile现实场景
正常使用中(听歌/通话)
配对刚完成,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 是给这张身份证配的厚档案,多出来的东西分三类:

  1. 事件痕迹(大部分字段):onXxxChanged 系列回调到达后留下的印记——mJustDiscoveredmIsActiveDevice*、ACL 双标志、失败标志……(62 §0 总纲:字段 = 缓存的事件痕迹)
  2. 算好的缓存mProfiles(远端 UUID × 本地支持 的交集,由 LocalBluetoothProfileManager.updateProfiles 算好塞回来)、mDrawableCache(图标)
  3. 包装的操作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

三个要点:

  1. 交集逻辑:远端支持 × 本端支持,缺一不可。车机不支持的服务(本地 supportedList 没有)永不实例化——profile 注册本身就是动态的(updateLocalProfilesgetSupportedProfiles() 决定,LBPM:142-288)。
  2. SDP 需要时间:这就是配对刚完成时 mProfiles 为空的原因,也是 MAX_*_DELAY_FOR_AUTO_CONNECT 一族耐心常量存在的理由(66 章)。
  3. 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 个实例字段。