63 · 对象的一生 — 谁创建 CachedBluetoothDevice、谁持有、何时消失
源码基线:f3dif(A17 主线)
CachedBluetoothDeviceManager.java(下称 CBDM,615 行)/LocalBluetoothManager.java(LBM,183 行)/LocalBluetoothAdapter.java(LBA,281 行) 前置阅读:62-字段逐个精讲 · 交叉引用:21-settingslib架构与事件流、68 实战案例(1922/134132/2960 三案全在本章的机制上)
0. 先建立一个心智模型:户籍系统
把蓝牙设置模块想成一个户籍科:
CachedBluetoothDevice(CBD)= 一个人的档案CachedBluetoothDeviceManager(CBDM)= 档案柜(所有档案放一个柜子里)BluetoothEventManager(BEM)= 收发室(所有广播先到这,再分拣)LocalBluetoothManager(LBM)= 科长(把上面三个部门组建起来,对外只有一个窗口)LocalBluetoothAdapter(LBA)= 对外的公章(真正干活的 frameworkBluetoothAdapter的包装)
本章回答四个问题:档案怎么建(§2)、柜子长什么样(§1)、档案怎么更新与排序(§4)、档案怎么销毁(§5)。
1. 档案柜:CBDM 的数据结构
1.1 一个普通的 ArrayList,没有索引
// CachedBluetoothDeviceManager.java:52
@VisibleForTesting
final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<>();三个反直觉的事实:
- 线性表、插入序、无 HashMap。查找一律
for循环线性扫(findDevice:95-117 里cachedDevice.getDevice().equals(device)逐个比对蓝牙地址),O(n)。设备多时(展厅车机扫到上百台)每次查找都是全表扫。 - 锁就是 CBDM 实例自己:全类 20 个
synchronized方法 + addDevice 内 1 个synchronized块(:139),互斥都以 CBDM 实例为锁——行号 :69/:95/:139(块)/:163/:186/:202/:217/:244/:252/:284/:315/:336/:368/:382/:399/:411/:460/:474/:501/:519/:567,互相之间全互斥。这是 68 §5 N801PROB-134132 秒级卡顿的结构性根源。 - 三个类共享同一个 List 引用。CBDM 构造函数(:61-67)把
mCachedDevices原样传给HearingAidDeviceManager(:64-65)和CsipDeviceManager(:66-67)——组管理器可以直接改这张主表(如 CsipDeviceManager.java:383mCachedDevices.remove(deviceItem))。没有”封装边界”可言,读代码时必须意识到这一点。
1.2 附带状态(不在容器里)
:57-59 的三件套 mOngoingSetMemberPair / mIsLateBonding / mGroupIdOfLateBonding 服务于 CSIP 组成员自动配对,蓝牙关闭时重置(:362-364)。
2. 档案怎么建:四个诞生入口 → addDevice 全流程
2.1 谁会触发建档(全部在 BluetoothEventManager 里;全 BEM 仅 4 处 mDeviceManager.addDevice)
| 入口 | 触发广播/时机 | 行号 |
|---|---|---|
readPairedDevices() | LBM 构造收尾(进程启动/单例首建),遍历 getBondedDevices(),findDevice 为 null 才 add | BEM:202-218(:212 add) |
DeviceFoundHandler | ACTION_FOUND(扫描发现新设备) | BEM:364-388(:375 add) |
BondStateChangedHandler | ACTION_BOND_STATE_CHANGED(findDevice 为 null 时补建) | BEM:407-472(:438 add) |
AclStateChangedHandler | ACTION_ACL_CONNECTED(对端发起 OPP/MAP/来电时对端先建链);DISCONNECTED 分支已被 MANNPROB-4005 修补为不建缓存(:580-588) | BEM:565+(:590 add) |
教学点:CBD 的诞生是事件驱动的,四个入口各对应一种”这台设备该被知道”的场景。这个设计的代价:任何一条事件链断掉,设备就从 UI 上消失——68 §6 MANNPROB-2960(双蓝牙芯片 ext adapter 广播不扇出 → 已配对列表全空)就是 ①② 两条链同时失效的实例。
反面澄清(防踩坑):
ActiveDeviceChangedHandler(BEM:135-140 注册)不是诞生入口——它把 findDevice 结果(可能为 null)直接交给 dispatch 做音频路由(HearingAidDeviceManager:485 只做路由/麦克风配置),全程无 addDevice,“findDevice==null 兜底建档”的说法是错的。另有一条”准入口”:CSIP 组成员配对时CBDM.onBondStateChangedIfProcess(:584)会为组员建档——但它仍由 ACTION_BOND_STATE_CHANGED 触发,算第 ③ 条链的延伸,不单列。
2.2 addDevice 内部(:136-154,含双参重载)
// 简化伪码(保留行号语义)
synchronized (this) { // 块范围 :139-151
if (findDevice(device) != null) return null; // :140 查重
CachedBluetoothDevice newDevice =
new CachedBluetoothDevice(mContext, profileManager, device); // :142 建档
mCsipDeviceManager.initCsipDeviceIfNeeded(newDevice); // :143 查 CSIS 组 → groupId
mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(
newDevice, leScanFilters); // :144 ASHA/LE 助听信息
if (setMemberDeviceIfNeeded() || setSubDeviceIfNeeded()) // :145-146 归组判定
return newDevice; // 挂进组里,不进主表、不发 UI 回调
mCachedDevices.add(newDevice); // :147 进主表
mBtManager.getEventManager().dispatchDeviceAdded(newDevice); // :148 通知 UI ★仍在锁内!
}值得注意的四点:
- 构造函数只有三个参数
(Context, LocalBluetoothProfileManager, BluetoothDevice)(CachedBluetoothDevice.java:230-242),内部fillData()拉 UUID/名字/bondState、mGroupId = GROUP_ID_INVALID、initDrawableCache()(LruCache,1/8 maxMemory)。 - 归组的设备不进主表:CSIP 同组已有 main → 挂
memberDevices;ASHA 同 hiSyncId → 挂subDevice(:145-146)。也就是说组员在 UI 上”隐形”,只通过主设备露脸。 - 已存在的设备走另一条路:DeviceFoundHandler:384-386 只是
setRssi()+setJustDiscovered(true)+setIsCoordinatedSetMember()——刷新可见性,不重复建档。 - :148 的 UI 回调是持 CBDM 锁执行的(dispatchDeviceAdded 在 synchronized 块内)——回调方若再碰 CBDM 的任何 synchronized 方法就是可重入,若做耗时操作就延长持锁。68 §5 134132 的锁问题的又一伏笔。
2.3 组是怎么归的(两个组管理器)
| CsipDeviceManager(LE Audio 协调组) | HearingAidDeviceManager(助听双耳) | |
|---|---|---|
| 组 key | groupId,来自 getBaseGroupId()(:69-87)问 CsipSetCoordinatorProfile.getGroupUuidMapByDevice() 取 value==BluetoothUuid.CAP 的 key | hiSyncId,来自 generateHearingAidInfo()(:634-676):ASHA 路线 HearingAidProfile.getHiSyncId();LE 路线 HapClient+LeAudio 组合 |
| 挂靠时机 | addDevice 时(:89-104)/ CSIS 连上后 updateCsipDevices()(:131-149)/ 连接态变化(:169-178) | addDevice 时(init :274-297 / setSub :300-322)/ 服务连上后 onHiSyncIdChanged()(:365-414) |
| main 选举 | getPreferredMainDevice(:259-320):已连接 DUAL > LeAudio 组 lead > 任一已连接 > 未连接 DUAL > 组内第一个 | 先连上的是 main,后来的被 mCachedDevices.remove + 挂为已连接者的 subDevice(:397-412) |
| 两通道关系 | 互斥:HearingAidDeviceManager.setSubDeviceIfNeeded(:300-309)见设备带 CsipSetCoordinatorProfile 直接 return false(“Skip ASHA grouping since this device supports CSIP”)——支持 CSIP 的设备组关系只走 member 通道 | 同左 |
车机上手机类设备基本不涉及组管理(那是 TWS 耳机/助听器的事),但读删除/清缓存代码时必须记得主表外还挂着组员——
clearNonBondedSubDevices()(:296-313)就是专门清它们的。
3. 科长:LBM 的构造顺序(为什么是这个顺序)
LocalBluetoothManager 是 static 单例(getInstance(context, onInitCallback) :57-76;无蓝牙硬件返回 null,调用方必须处理 :31-33)。私有构造(:110-129)按依赖顺序搭台:
:112 mContext = context.getApplicationContext()
:113 mLocalAdapter = adapter ← 依赖外部已建好的 LBA(getInstance 时先建)
:114 mCachedDeviceManager = new CBDM(...) ← 档案柜(此时是空的)
:115 mEventManager = new BEM(...) ← 收发室:构造里注册全部广播 receiver
:123 mProfileManager = new LocalBluetoothProfileManager(...)
:127 mProfileManager.updateLocalProfiles() ← 按本机支持情况填 profile 列表
:128 mEventManager.readPairedDevices() ← 全量回灌:从 getBondedDevices() 重建已配对 CBD
顺序教学点::128 的全量回灌必须排在 ProfileManager 就绪(:123/:127)之后——因为 addDevice → new CachedBluetoothDevice(..., profileManager, ...) 构造时要 fillData(),而 profile 相关查询依赖 ProfileManager。依赖倒挂 = 启动崩溃,这个顺序不是随便排的。
App 层入口在 MiBluetoothUtils.kt:75(LocalBluetoothManager.getInstance(...))。
3.1 单例永不销毁(重要!)
- 全仓库 grep
sInstance = null零命中——LBM 没有任何 teardown 代码。 - 注释原话(:66):“This will be around as long as this process is”。
- 关蓝牙不等于销毁:蓝牙 OFF 只走
AdapterStateChangedHandler(BEM:334-347)→CBDM.onBluetoothStateChanged(STATE_TURNING_OFF)(:336-366)——清掉非 bonded 的缓存设备、重置 CSIP 三状态(:362-364),单例和 bonded 设备的档案都活着。
另有两个非单例工厂 create(context, handler)(:83-89)与跨用户版 create(context, handler, userHandle)(:101-108,需 INTERACT_ACROSS_USERS_FULL 权限)。
3.2 LBA:公章的定位
LocalBluetoothAdapter 头上顶着 @Deprecated(:47),类注释(:36-44)自己说清了分工:adapter 自身状态迁移归它,具体设备的连接/bond 变化归 CBDM+BEM+ProfileManager。它不把 BluetoothAdapter 直接引用交给外部(:51 注释)。
| 纯 passthrough(无逻辑) | 带自有逻辑(与裸用 BluetoothAdapter 的区别) |
|---|---|
| enable/disable(:110-116)、getAddress/getName/setName、getScanMode/setScanMode、getBondedDevices(:127,纯默认 adapter——2960 案的缺口)、getUuids、getState/isDiscovering/isEnabled、cancelDiscovery、getBluetoothLeScanner、getProfileProxy | startScanning(force)(:188-213):5 分钟节流(SCAN_EXPIRATION_MS :60)且 A2dp 正在播放则不扫(:199-206);setBluetoothEnabled(:254-272):成功后乐观置 TURNING_ON/OFF,失败才回查真实态;mState 影子(:58)+ syncBluetoothState() 对账(:245-252) |
事件回流不经过它:adapter 状态变化是 BEM 直接收 ACTION_STATE_CHANGED 广播(BEM:112),AdapterStateChangedHandler 里反向调 mLocalAdapter.setBluetoothStateInt(state) 同步影子。即 LBA = 命令出口 + 状态影子;广播入口在 BEM。
4. 档案的日常:刷新与排序
4.1 “消失”的真实语义(一个死代码警告)
AOSP 的 CBDM 有 onDeviceDisappeared(CachedBluetoothDevice)(:73-76,static 方法):仅 setJustDiscovered(false),返回 bondState == BOND_NONE 供调用者决定是否从 UI 移除。f3dif 与 dcddif 全仓库内均无调用者(grep 仅定义处);f3dif 的 BEM 注册表(:112-152)也没有 ACTION_APPEARANCE_CHANGED——车机版未接入 AOSP appearance 通道。
所以车机上”设备从可用列表消失”不是一个事件,而是:ACTION_DISCOVERY_FINISHED 到来 + UI 层用 clearNonBondedDevices/过滤条件重建列表的推论结果。
onScanningStateChanged(started)(:315-334)只在新一轮扫描开始时逆序遍历,把主设备/member/sub 的 mJustDiscovered 全清 false——清的是”本轮可见性”,不删对象。mJustDiscovered 写 true 的唯一入口在 BEM DeviceFoundHandler:385;setJustDiscovered(CBD:901-903)值变才 refresh()。
4.2 排序:compareTo 五级权重(CBD:1386-1405)
CBDM 自己不排序(ArrayList 插入序),排序在 CBD 的 compareTo:
已连接 > 已配对(BOND_BONDED) > mJustDiscovered > RSSI 强 > 名称字典序
注释明说:排序依赖非 final 字段,属性变化时 Settings 端要全量刷新列表。(UI 层还有自己的置顶规则,见 67 与 34 篇。)
5. 档案怎么销毁:两条路 + 一个不通知
5.1 clearNonBondedDevices 逐行(:284-294)
public synchronized void clearNonBondedDevices() {
clearNonBondedSubDevices(); // :285 先清组内从属
final List<CachedBluetoothDevice> removedCachedDevice = new ArrayList<>();
mCachedDevices.stream()
.filter(c -> c.getBondState() == BOND_NONE) // :288 ← 每次 getBondState 都是 binder!
.forEach(c -> { c.release(); removedCachedDevice.add(c); }); // :290-291
mCachedDevices.removeAll(removedCachedDevice); // :293 一次性移除
}三个要点:
- :288 是性能炸点:
getBondState()是 binder 直通(CBD:908-910 →mDevice.getBondState(),跨进程问 BluetoothManagerService)。N 个缓存设备 = N 次串行 binder RPC,全程持有 CBDM 实例锁。扫描后设备可达几十上百台 → 持锁 0.7~2.4s(134132 实测 3.136s),主线程再调任何 synchronized 方法(getCachedDevicesCopy():69 /findDevice():95)就被 dvm_lock_sample 命中。(2026-09-10 补:BtTrace 埋点复测刷新峰值——fermi 台架 ~195 台缓存,onPause 后台清表持锁 4329ms、主线程 innerUpdateState 整段被顶 4141ms;60s 滑窗getBondState751 次=binder 热点 No.1、findDevice502 次。埋点分支 dev-bt-trace-134132,点位清单见 jira-data/N801PROB-134132/analysis/TRACE-INSTRUMENTATION.md。) - :290
release()很轻:只是mHandler.removeCallbacksAndMessages(null)(CBD:245-247),清掉 pending 的消息而已。 - :293 之前没有任何
dispatchDeviceRemoved——UI 不会收到逐设备删除回调,列表刷新全靠调用方自己重建。对比:正常移除路径(组员合并 CsipDeviceManager:346-351/:369-387)是 dispatch → 改表 → dispatch 的完整闭环。
clearNonBondedSubDevices()(:296-313)还有个 AOSP 遗留缺陷:member 清理只处理第一个带 member 的设备就 return(:303)——多个 CSIP 组时后面的组不会被清(f3dif/xcddif 均如此;dcddif 压根没有 member 概念)。(2026-09-06 第五轮补:onScanningStateChanged 也有同款首组 return——xcddif :305 / f3dif :327,扫描可见性重置同样只处理到第一个组,详见 42 §7.2。)
5.2 谁调用它(全部在 app 层,settingsLib 内零调用)
BluetoothUnbondedDevicesPrefController.java:
| 位置 | 时机 |
|---|---|
:169(innerUpdateState 内) | 每次 updateState(列表刷新;首开还有 200ms postDelayed 延迟) |
:226(onPauseInternal) | 页面 onPause,postOnBackgroundThread 里——134132 的”反向持锁源” |
:245(onDestroyInternal) | 页面销毁 |
:330(onDeviceAdded 拦截分支) | 手动解绑设备再广播回来时,拦截一次后清缓存防再触发(2026-09-10 补:嵌套持锁——onDeviceAdded 本身在 addDevice 的 synchronized 块内被回调,见 §2.2 第 4 点,清表的 N 次 binder 全程延长同一把锁的持锁时间;367819 未删此调用,改为复位标记后清表移后台) |
settingsLib 内部另有一条自动通道:onBluetoothStateChanged(STATE_TURNING_OFF)(CBDM:336-366)。(2026-09-10 补:同类病灶未修——该方法 STATE_TURNING_OFF 分支同样在锁内逐台 getBondState(xcddif :314-341 三处已验证,f3dif 同构),仅关蓝牙时触发、不在 134132 场景,367764/367819 未覆盖,建议单独立项。)
5.3 取消配对后,档案其实还躺着
Bond 广播里不删 CBD。BondStateChangedHandler(BEM:407-472)对 BOND_NONE,只有组设备(groupId/hiSyncId 有效,:460-464 条件门)才回调 onDeviceUnpaired(级联解配对,CBDM:411-447;2026-09-06 补:此为 f3dif 基线行为——xcddif 版已被 MIUI MOD 改弱,主设备解配不向 CSIP 组员下发 unpair(组员解配仍连带主设备),不对称分叉详见 42 §7.1);无论组否,CBD 都不从主表移除——残留到下一次 clearNonBondedDevices 或 BT off 才真正出表。这个”残留窗口”正是 68 §4 MANNPROB-3492(cached+BOND_NONE 分发盲区)的温床。
6. 一生总图
flowchart TD A["进程启动 / 单例首建<br/>MiBluetoothUtils.kt:75 getInstance"] --> B["LBM 构造 :110-129<br/>CBDM → BEM → ProfileManager → updateLocalProfiles"] B --> C["readPairedDevices BEM:202-218<br/>getBondedDevices 逐台 findDevice==null → addDevice"] D["扫描中 ACTION_FOUND"] --> E["DeviceFoundHandler BEM:364-388"] E -->|findDevice==null| F["addDevice :136-154<br/>new CachedBluetoothDevice :142<br/>initCsip :143 / initHearingAid :144"] E -->|已存在| I["setRssi + setJustDiscovered(true) :384-386"] F -->|普通设备| G["进主表 + dispatchDeviceAdded :147-148"] F -->|CSIP同组 / ASHA同hiSyncId| H["挂 member / sub,不进主表 :145-146"] C --> F2["同 F"] --> G G --> J["配对 createBond<br/>ACTION_BOND_STATE_CHANGED"] M["蓝牙 OFF (TURNING_OFF)"] --> N["onBluetoothStateChanged CBDM:336-366<br/>清非bonded + 重置CSIP三状态;单例不销毁"] O["UI 刷新 / onPause / onDestroy / 解绑拦截<br/>Controller :169/:226/:245/:331"] --> P["clearNonBondedDevices :284-294<br/>持锁 + 每台 binder getBondState<br/>→ 1922 / 134132 病灶"] Q["取消配对 bondState→BOND_NONE"] --> R["CBD 残留,等下次 clear 或 BT off"] P -.清掉后重扫到同一台.-> E
一生总结(背诵版):
- 诞生:四个事件入口 →
addDevice→new CBD(三参)→ (组员挂靠 / 主表+dispatch,dispatch 在锁内) - 重建:进程重启靠 LBM 构造 :128
readPairedDevices全量回灌;关开蓝牙靠 STATE_ON 重灌 - 日常:广播到达 → BEM handler → 改 CBD 字段 →
refresh()→ UI 回调 - 消失:没有”删除事件”——靠
clearNonBondedDevices连坐全清(且不通知 UI),或 BT off 自动清 - 终点:
release()只清 handler 队列,对象由 GC 收尸
7. 三 flavor 的 CBDM 差异速览
| 维度 | f3dif(615 行,A17/F3) | dcddif(约 270 行,DCD 老基线) | xcddif(约 580 行,XCD) |
|---|---|---|---|
| MICAR-PORTING 标记 | 0 | 0 | 2(:240/:249 包住 getBluetoothClass) |
| addDevice 重载 | 单参 + (device, leScanFilters) 双参(配对期带 BLE 滤器建助听标记) | 仅单参 | 仅单参 |
| clearNonBondedDevices | :284-294 stream+release | :189-193 裸 removeIf,无 release() | :250-262 同 f3dif |
| CSIP 组管理 | 完整 | 无 CsipDeviceManager 文件(无 LE Audio) | 完整,同构 |
| clearNonBondedSubDevices | member 用 removeIf,首个带 member 后 return(:303) | 只处理 sub,无 member | toArray 逐个删(比 f3dif 完整),同样首个后 return :274 |
| 配套 CBD | 无 isCloseAutoConnectDevice | 有(:821) | 有(:1032) |
(flavor 机制详见 22-双flavor差异;改 settingslib 必须三栏对照——见 50 速查卡 §C。)
8. 自测清单
- CBD 的四个诞生入口分别对应什么广播/时机?哪两个属于”回灌”、哪个属于”扫描”?
ActiveDeviceChangedHandler算不算入口?(不算——它只做路由,无 addDevice) - 为什么
readPairedDevices()必须排在 ProfileManager 构造之后? - 关蓝牙后单例销毁了吗?已配对设备的档案还在吗?(答:不销毁;bonded 档案活着,只清非 bonded)
-
clearNonBondedDevices临界区里最贵的一行是哪行?为什么?(:288 filter 里的 getBondState = binder) - 取消配对后 CBD 立即从主表移除了吗?(没有——残留到下次 clear)
- 归组的设备(TWS 副耳机)会出现在主表吗?(不会,挂 member/sub)