62 · 字段逐个精讲 — CachedBluetoothDevice 的 41 个实例字段
源码基线:
base/settingsLibAndroid/src/f3dif/java/com/android/settingslib/bluetooth/CachedBluetoothDevice.java(2719 行,f3dif = A17 主线) 本文回答初学者的第一个问题:“开头那一大片字段声明都是干什么的?” 读法:每组先看”是什么”(新手视角),再看”谁写谁读”(工程师视角),最后看”坑在哪”(排障视角)。 口径:本文件实例字段共 41 个(:112-:228 实测),本篇按 9 组逐讲 38 个;mHandler/mBatteryMetadataListener/mBluetoothFailureTimerScheduler在 §8/§9 已顺带覆盖,唯mBluetoothFailureFuture(失败延迟刷新任务的 Future 句柄,与 §8 看门狗机制配套)从略。
0. 全景图:先背下来这 9 组
flowchart TD CBD[CachedBluetoothDevice 一台设备的档案] --> G1[§1 常量<br/>耐心值/阈值] CBD --> G2[§2 身份与依赖<br/>mDevice 是灵魂] CBD --> G3[§3 profile 集合<br/>这台设备能干什么] CBD --> G4[§4 可见性状态<br/>mJustDiscovered 等] CBD --> G5[§5 回调注册表<br/>UI 怎么知道变了] CBD --> G6[§6 时间戳与 ACL<br/>回连窗口的账本] CBD --> G7[§7 active 设备<br/>当前输出目标] CBD --> G8[§8 失败标志+看门狗<br/>60s 超时机制] CBD --> G9[§9 组与缓存<br/>sub/member/LruCache]
一个总纲:这个类的大部分字段不是”数据仓库”,而是缓存的事件痕迹——每个布尔/时间戳对应一类广播事件到达后留下的印记(onXxxChanged 系列方法的产物)。读字段先想”哪个事件写它”,字段就活起来了。
§1 常量(:94-110)
private static final String TAG = "CachedBluetoothDevice"; // :94
private static final ParcelUuid ANDROID_AUTO_UUID = ...; // :95-96
private static final long MAX_UUID_DELAY_FOR_AUTO_CONNECT = 35000; // :99
private static final long MAX_HEARING_AIDS_DELAY_FOR_AUTO_CONNECT = 45000; // :101
private static final long MAX_HOGP_DELAY_FOR_AUTO_CONNECT = 60000; // :102
private static final long MAX_LEAUDIO_DELAY_FOR_AUTO_CONNECT = 60000; // :103
private static final long MAX_MEDIA_PROFILE_CONNECT_DELAY = 60000; // :104
private static final int DEFAULT_LOW_BATTERY_THRESHOLD = 20; // :106
private static final int SUMMARY_NO_COLOR_FOR_LOW_BATTERY = 0; // :110| 常量 | 一句话 | 深入 |
|---|---|---|
TAG | 日志标签 | bugreport 取证入口:grep CachedBluetoothDevice |
ANDROID_AUTO_UUID | 识别 Android Auto 车机的特征 UUID | 消费者 isAndroidAuto()(:2651)——UUID 命中或 UiModeManager 投影类型 |
MAX_*_DELAY_FOR_AUTO_CONNECT | 自动回连的耐心窗口(ms) | 用法见 66 章。注意选择顺序是 if-else 链(:1113-1120):HOGP(60s) > HEARING_AID(45s) > LE_AUDIO(60s) > 默认(35s)——同时含助听器与 LE Audio UUID 的设备拿 45s 而非 60s,顺序敏感,重排即改行为 |
MAX_MEDIA_PROFILE_CONNECT_DELAY | 单 profile 连接看门狗时长 | §8:Handler 60s 超时判失败 |
DEFAULT_LOW_BATTERY_THRESHOLD | 低电量阈值兜底 20% | isBatteryLow(:2155)按 metadata key 取阈值,<=0 才回落此常量;只在 TV 摘要着色链路使用 |
SUMMARY_NO_COLOR_FOR_LOW_BATTERY | ”不着色”哨兵值 0 | 魔数 0 表示低电量文字不变色(resource id 语义) |
新手常见疑问:为什么耐心值要分设备类型? 蓝牙连上后要做 SDP 服务发现(问对方”你支持哪些服务”),助听器(尤其第二只耳)做服务发现特别慢(:100 注释原话),HOGP/LE Audio 也慢。窗口太短→该连的没连上;太长→失败后傻等。
§2 身份与依赖(:112-123)
private final Context mContext; // :112
private final LocalBluetoothProfileManager mProfileManager; // :113
private final Object mProfileLock = new Object(); // :114
BluetoothDevice mDevice; // :115 ← 注意非 private!
private HearingAidInfo mHearingAidInfo; // :116
private int mGroupId; // :117
private Timestamp mBondTimestamp; // :118
private LocalBluetoothManager mBluetoothManager; // :119
private BluetoothAdapter mLocalAdapter; // :120
short mRssi; // :123 ← 包级可见逐个讲
mDevice(:115)— 灵魂字段。真正的 frameworkBluetoothDevice(本质是 MAC 地址封装)。三个关键事实:- 整个类的身份就是它:
equals(:1371-1376)只比mDevice.equals(...),hashCode(:1379-1381)=mDevice.getAddress().hashCode()。CBD = 被包装对象的身份证。 - 非 private(包级可见)——组管理器(CsipDeviceManager 等)直接改它,这是”换壳”模式(§9)能实现的前提。
- 它是同步 binder 调用的源头:
getBondState()(:908-910)直通mDevice.getBondState(),每次跨进程问 BluetoothManagerService——68 §5 N801PROB-134132 锁内秒级阻塞的代价来源。
- 整个类的身份就是它:
mProfileManager(:113) — 管各 profile 连接器的总管,connect/updateProfiles都靠它。构造函数三参之一。mProfileLock(:114) — 一把普通对象锁,保护mProfiles/mRemovedProfiles的并发访问。加锁点:connectDevice(:596)、disconnect(:456)、onProfileStateChanged(:277 起)、onBondingStateChanged的 clear(:1149)、updateProfiles(:1069)。设计铁律:临界区内不做 binder 调用(134132 的教训就是把这条踩碎了)。mHearingAidInfo(:116)/mGroupId(:117) — 助听器信息与 CSIP 组号。mGroupId构造时置GROUP_ID_INVALID(:237),后续由 CsipDeviceManager 写(:587setGroupId)。组的完整机制见 67 章。mBondTimestamp(:118) — 配对成功时刻。写点:BONDED 时new Timestamp(...)(:1190);BOND_NONE 后台线程清 null(:1159)。诊断用途(getBondTimestamp:1264)。mBluetoothManager(:119)/mLocalAdapter(:120) — 总管引用与本机适配器。注意构造函数并不初始化它们(构造只有 ctx/profileManager/device 三参,:230-242),mLocalAdapter在构造里BluetoothAdapter.getDefaultAdapter()(:232-235 一带);mBluetoothManager有@VisibleForTesting注入钩子(:2631)。mRssi(:123) — 信号强度。没有主动查询接口(:122 注释),只能扫描回调写入:setRssi(:991-996,值变才写+dispatch)。所以 RSSI 是”最近一次广播里看到的强度”,会过期。
§3 profile 集合(:125-130)
// :125-126 注释:mProfiles 和 mRemovedProfiles 不在 main/sub 设备间做 swap(),
// 因为当前副设备只用于助听器,其 profile 与主设备相同。
private final Collection<LocalBluetoothProfile> mProfiles = new CopyOnWriteArrayList<>(); // :127
private final Collection<LocalBluetoothProfile> mRemovedProfiles = new CopyOnWriteArrayList<>(); // :130这两个字段回答”这台设备能干什么”:mProfiles = 当前支持的 profile(A2DP 音乐、HFP 通话、PBAP 通讯录…);mRemovedProfiles = 曾在 mProfiles 后来被移除的(diff 用)。
三个必须知道的工程事实:
- 主写入者是外部类!CBD 自己只在
onProfileStateChanged里 add/remove(:345-364),整体重建在LocalBluetoothProfileManager.updateProfiles(LBPM:634,synchronized)——updateProfiles()(:1056-1085)把mProfiles引用交出去,那边先clear()再按 UUID 重填。也就是说:你看到的 mProfiles 内容,是远端 UUID × 本地 UUID 的交集,由别人算好塞回来。 CopyOnWriteArrayList的选型逻辑:读极多(每次 UI 刷新、isConnected判断都遍历)写极少(状态变化时),且需要在遍历中安全修改。代价是每次写复制整个数组——写频繁就是性能问题。- BOND_NONE 时
mProfiles.clear()但mRemovedProfiles不清(:1149-1151)——残留的 removed 列表是排查时的噪音源。
什么时候重建?
onUuidChanged(:1109,UUID 广播到达/SDP 完成)→updateProfiles()。这也解释了为什么配对刚完成时mProfiles可能是空的——SDP 还没跑完,空列表时connectDevice()直接放弃(:599-609,注释:防与 carkit 配对期 UUID 竞态,等 ACTION_UUID 再连)。
§4 可见性与杂项状态(:132-137)
private boolean mLocalNapRoleConnected; // :133
boolean mJustDiscovered; // :135
boolean mIsCoordinatedSetMember = false; // :137mLocalNapRoleConnected(:133) — PAN(蓝牙网络共享)角色标记:设备支持 PANU 但不支持 NAP 时,从 NAP 断开后移除 PanProfile(注释原话)。写点:onProfileStateChanged:350(连)/ :365(断);只读传参updateProfiles:1071。mJustDiscovered(:135) — “本轮扫描刚见过”。唯一写 true 入口:BEM DeviceFoundHandler:385 →setJustDiscovered(:901-903,值变才 dispatch);清 false:新扫描开始时 CBDM.onScanningStateChanged(:315-334;注意与 clearNonBondedSubDevices:303 同款早退——:327 遇首个带 member 的组即 return,其后条目本轮不清)。用途有二:compareTo排序权重(:1386-1405,已连接 > 已配对 > justDiscovered > RSSI > 名字);UI 层”附近设备”过滤。mIsCoordinatedSetMember(:137) — 是否 CSIP 协调组成员(TWS 耳机副耳)。唯一写点setIsCoordinatedSetMember(:559,外部入口 EventManager:386)。
§5 回调注册表(:139-141)
private final Collection<Callback> mCallbacks = new CopyOnWriteArrayList<>(); // :139
private final Map<Callback, Executor> mCallbackExecutorMap = new ConcurrentHashMap<>(); // :141观察者模式的两个通道,一个 deprecated 一个新:
旧 API registerCallback(Callback)(:1313) | 新 API registerCallback(Executor, Callback)(:1327) | |
|---|---|---|
| 存储 | mCallbacks | mCallbackExecutorMap |
| 派发线程 | 调用者线程直调(:1348-1350)——通常是广播线程 | 注册时指定的 executor(:1351-1352) |
| 状态 | @Deprecated | 推荐 |
dispatchAttributesChanged()(:1347-1353)先遍历旧通道直调,再遍历新通道丢给 executor。Callback 接口只有一个方法 onDeviceAttributesChanged()(:1408-1410)——属性变了这一种事件,粒度很粗(不告诉你哪个属性变了)。
两个坑:
- 双注册双发:同一 callback 若两个 API 都注册,一次属性变化收两次通知(§惊喜 7)。
- 旧通道的接收方必须自己保证线程安全——回调可能落在任意广播线程。车机 UI 层(
NewBluetoothDevicePreference:116)用的是registerCallback(UI 层自己 post 回主线程)。
unregisterCallback(:1337-1345)要两个集合都空才注销电量 metadata listener——防止多注册方互相踩。
§6 时间戳与 ACL 链路(:143-154)
private long mConnectAttempted = -1; // :150
private long mBondFailureTimeMillis = -1; // :151
private long mConnectionFailureTimeMillis = -1; // :152
private boolean mIsAclConnectedBrEdr = false; // :153
private boolean mIsAclConnectedLe = false; // :154⚠️ mConnectAttempted:注释会骗人(本组最重要的一课)
字段注释(:143-150)说它是 “Last time a bt profile auto-connect was attempted”(上次自动连接尝试时间)。实际实现(:1226-1227):
// onAclStateChanged() 内
if (!mIsAclConnectedBrEdr && !mIsAclConnectedLe) {
mConnectAttempted = SystemClock.elapsedRealtime(); // ← 写点:首个 ACL 链路建立时刻- 它记的是 ACL 物理链路建立时刻,不是”连接尝试”时刻;
- 复位 -1 的条件是双传输都断开(:1251-1252),不是注释说的”断开即复位”(半断不复位);
- 换壳时还会被搬运到新 main(
switchMemberDeviceContent:2580/:2591); - dcddif 版的
onUuidChanged判定行(DC:811)因此漏了-1守卫——开机早期mConnectAttempted=-1时-1+timeout > now可能为真,误触发连接。f3dif 补了mConnectAttempted != -1(:1138)。
教学点:字段名、注释、实现三者不一致时,以写入点代码为准。读开源代码要养成”顺着赋值语句找语义”的习惯。完整回连窗口机制见 66 章。
ACL 双标志(:153-154)
onAclStateChanged(state, transport)(:1212-1262)按 transport 分流维护:经典蓝牙(BR/EDR)和低功耗(LE)各一条 ACL 物理链路,比 profile 更底层。“ACL 通了但没有任何 profile 连上”是合法中间态(配对后 SDP/连接进行中)。DCD/XCD 老版本没有这两个标志——是 A17 基线新加的。
两个失败时间戳(:151-152)
| 字段 | 写点 | 语义 |
|---|---|---|
mBondFailureTimeMillis | BONDING→NONE 且诊断 flag 开(:1163,后台线程);BONDED 复位 -1(:1201) | 配对失败时刻(CAN_NOT_PAIR 延迟刷新配套) |
mConnectionFailureTimeMillis | onProfileStateChanged :378(isProfileConnectedFail 且 !isBusy)/ :394(清 -1) | 连接失败时刻(重试节流用) |
§7 active 设备标志(:156-160)
private boolean mIsActiveDeviceA2dp = false; // :157
private boolean mIsActiveDeviceHeadset = false; // :158
private boolean mIsActiveDeviceHearingAid = false; // :159
private boolean mIsActiveDeviceLeAudio = false; // :160“这台设备是不是当前的输出目标”。车机连两台耳机,音乐只从一台出——那台在 A2DP 维度是 active。每个 profile 维度各一个 active 位。
- 被动写:
onActiveDeviceChanged(isActive, profile)(:918-951,入口 EventManager:296-304 的四个 ACTIVE_DEVICE_CHANGED 广播,值变才 dispatch)。 - 主动对账:
fetchActiveDevices()(:1087-1104)——A2DP/HFP 用equals(getActiveDevice())(单 active 语义),HA/LeAudio 用getActiveDevices().contains(mDevice)(双 active 语义——两台助听器可以同时 active)。fillData()与onProfileStateChanged尾部(:400)都会调。 - 主动设置:
setActive()(:782-813)对四个 profile 依次setActiveDevice,已连接才设。
详见 67 章。
§8 profile 连接失败标志 + 60 秒看门狗(:161-166 + :179-202)
private boolean mIsA2dpProfileConnectedFail = false; // :162
private boolean mIsHeadsetProfileConnectedFail = false; // :163
private boolean mIsHearingAidProfileConnectedFail = false; // :164
private boolean mIsLeAudioProfileConnectedFail = false; // :165
private boolean mIsListeningBatteryChange = false; // :166这四个失败标志与一个很巧的看门狗机制配套——mHandler(:179-202)直接把 profileId 当 message.what 用:
STATE_CONNECTING → sendEmptyMessageDelayed(profileId, 60_000) // :298-301 上弦
STATE_CONNECTED → removeMessages(profileId) // :295-297 解除
STATE_DISCONNECTING→ 有则 remove // :302-306
STATE_DISCONNECTED → 有则 remove;
且 getConnectionPolicy > FORBIDDEN 时判"真失败" // :307-331
→ setProfileConnectedStatus(id, true) // :436-449 写失败标志
60 秒后消息还活着 → handleMessage 命中 :184/:187/:190/:193
→ "Connect to profile : X timeout, show error message !" // :199
→ 置失败标志 + refresh() // :200
零额外状态字的超时器设计:不用单独的 Timer/状态机,Handler 消息本身就是”看门狗已上弦”的证据。消费者 isProfileConnectedFail()(:2210-2242)综合 4 标志 + profile isEnabled 判定,UI 据此显示”连接失败”。
排障提示:
isProfileConnectedFail里有一条跨 6 行的 Log.d 常驻(:2211-2216,单条语句打印 4 个失败标志 + SAP 状态),正常路径每次调用都打——bugreport 里大量出现不是异常,别误判。
另外 mIsListeningBatteryChange(:166)是电量 metadata listener 的注册守卫(防重复注册,:2672/:2683/:2698/:2706),详见 67 章 §电量。
§9 组管理与资源缓存(:167-177)
private boolean mUnpairing; // :167
@Nullable private InputDevice mInputDevice; // :168-169
private boolean mIsDeviceStylus; // :170
private CachedBluetoothDevice mSubDevice; // :173
private Set<CachedBluetoothDevice> mMemberDevices = new HashSet<>(); // :175
@VisibleForTesting LruCache<String, BitmapDrawable> mDrawableCache; // :176-177mUnpairing(:167)— 单向旗。只有两个写点:构造置 false(:239)、unpair()置 true(:666)——永不复位。能这么写是因为 unpair 后对象即被丢弃;消费者HearingAidDeviceManager:439靠它在删键期间吞掉副设备的 STATE_DISCONNECTED 事件。若对象被复用就是 bug(教学点:生命周期假设要写进注释——这里恰好没写)。mInputDevice/mIsDeviceStylus(:168-170) — 手写笔场景(构造 :240-241BluetoothUtils.getInputDevice判定)。mSubDevice(:173) — 助听器”另一只耳”。唯一挂 sub 赋值入口setSubDevice(:2490,调用方 HearingAidDeviceManager:316/:407;另有 CBDM 4 处setSubDevice(null)清除调用 :310/:350/:433/:437)。换壳switchSubDeviceContent(:2494-2520):备份 → 双方release()→ 互换 mDevice/mRssi/mJustDiscovered/mHearingAidInfo 四字段(不换 mProfiles,原因见 :125-126 注释:副设备只有助听器场景、profile 必同)。mMemberDevices(:175)— 裸 HashSet,唯一没有并发保护的集合!mProfiles/mCallbacks都是 CopyOnWriteArrayList、mCallbackExecutorMap是 ConcurrentHashMap,唯独它是普通 HashSet——组操作全靠调用方 CBDM 的 synchronized 方法(addDevice 等)持锁串行化(组管理器自身方法并无锁保护:CsipDeviceManager 全类 0 个 synchronized,HearingAidDeviceManager 仅 1 处且与组挂靠无关)。在 CBDM 锁外碰它就是数据竞争。mDrawableCache(:176-177) — 设备图标 LruCache。initDrawableCache(:249-258)容量 = 进程最大内存的 1/8(KB);refresh()(:877-899)后台线程预取METADATA_MAIN_ICON塞缓存;releaseLruCache(:2622,unpair :679 调用)evictAll。每个 CBD 对象一份 → 设备多时隐性内存占用可观。68 §1 MANNPROB-2403 的图标问题就发生在这条链上。- 同族资源还有
mBluetoothFailureTimerScheduler = Executors.newSingleThreadScheduledExecutor()(:226-227)——无 shutdown 路径,release()(:245-247)只清 Handler 消息。泄漏面:每设备一个永不关的单线程调度器。
换壳的身份陷阱(高级话题)
switchMemberDeviceContent(:2553-2598)互换 7 个字段(比 sub 版多 mConnectAttempted/mIsAclConnectedBrEdr/mIsAclConnectedLe,:2563-2582)。因为 hashCode 以地址计算(:1380),换壳会改变对象在 HashSet 里的 hash——所以 :2554-2556 必须先 removeMemberDevice 再事后 addMemberDevice,注释原话 “prevent hash mismatch problem due to mDevice switch”。这是”包装器身份 = 被包装对象身份”设计的反面教材。
§10 惊喜点集锦(读懂这个类的暗面)
- 死代码对:
connectInt(:621-632)与onBondingDockConnect(:590-594)全仓库无调用者(grep 实证)——旧 API 迁移化石。 TURNING_OFF吞事件:onProfileStateChanged开头 STATE_TURNING_OFF 直接 return(:282-288),日志原文 “BT Turninig Off…”(Turninig 拼错)——关蓝牙期间的 profile 断开不进失败统计,排”关蓝牙后状态残留”必看。- compareTo 自认不稳定:头注释(:1383-1385)明说排序键非 final、属性变化会变序,依赖 Settings 整表刷新兜底——排序实现与 UI 刷新协议是隐式契约。
toString(:1356-1368)是免费 debug 工具:输出匿名化地址/名字/groupId/member(+助听器信息)。日志里看到它就能对上设备。- f3dif 无
MICAR-PORTING标记 ≠ 无小米改动::1152-1182 有MIUI MOD: BT_MIUIBluetoothFrame块(BOND_NONE 后台重置三权限)——小米改动换了标记体系。 - 名字是”读时取”不是缓存:
getName()(:753)每次实时mDevice.getAlias();refreshName()(:815)只 dispatch 不存值。(老版本 AOSP 的fetchNameAndImages在 f3dif 已不存在——搜不到这个方法不是你眼花。)
自测清单
-
mProfiles的整体重建者是谁?(不是 CBD 自己——是LocalBluetoothProfileManager.updateProfilesLBPM:634) -
mConnectAttempted注释说记什么、实际记什么?(“连接尝试时间” vs ACL 链路建立时刻) - 60 秒看门狗用什么当 message.what?(profileId 本身)
- 哪个集合没有并发保护?(
mMemberDevices裸 HashSet) -
mUnpairing会被写回 false 吗?(不会,单向旗) - 换壳时为什么必须先 remove 再 add member?(hashCode 随 mDevice 变化,防 hash 错配)
-
equals比什么?(只比 mDevice——蓝牙地址)
下一章 63-对象的一生:这些字段所在的对象,从哪里来、到哪里去。