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)= 对外的公章(真正干活的 framework BluetoothAdapter 的包装)

本章回答四个问题:档案怎么建(§2)、柜子长什么样(§1)、档案怎么更新与排序(§4)、档案怎么销毁(§5)


1. 档案柜:CBDM 的数据结构

1.1 一个普通的 ArrayList,没有索引

// CachedBluetoothDeviceManager.java:52
@VisibleForTesting
final List<CachedBluetoothDevice> mCachedDevices = new ArrayList<>();

三个反直觉的事实:

  1. 线性表、插入序、无 HashMap。查找一律 for 循环线性扫(findDevice :95-117 里 cachedDevice.getDevice().equals(device) 逐个比对蓝牙地址),O(n)。设备多时(展厅车机扫到上百台)每次查找都是全表扫。
  2. 锁就是 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 秒级卡顿的结构性根源。
  3. 三个类共享同一个 List 引用。CBDM 构造函数(:61-67)把 mCachedDevices 原样传给 HearingAidDeviceManager(:64-65)和 CsipDeviceManager(:66-67)——组管理器可以直接改这张主表(如 CsipDeviceManager.java:383 mCachedDevices.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 才 addBEM:202-218(:212 add)
DeviceFoundHandlerACTION_FOUND(扫描发现新设备)BEM:364-388(:375 add)
BondStateChangedHandlerACTION_BOND_STATE_CHANGED(findDevice 为 null 时补建)BEM:407-472(:438 add)
AclStateChangedHandlerACTION_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 ★仍在锁内!
}

值得注意的四点:

  1. 构造函数只有三个参数 (Context, LocalBluetoothProfileManager, BluetoothDevice)(CachedBluetoothDevice.java:230-242),内部 fillData() 拉 UUID/名字/bondState、mGroupId = GROUP_ID_INVALIDinitDrawableCache()(LruCache,1/8 maxMemory)。
  2. 归组的设备不进主表:CSIP 同组已有 main → 挂 memberDevices;ASHA 同 hiSyncId → 挂 subDevice(:145-146)。也就是说组员在 UI 上”隐形”,只通过主设备露脸
  3. 已存在的设备走另一条路:DeviceFoundHandler:384-386 只是 setRssi() + setJustDiscovered(true) + setIsCoordinatedSetMember()——刷新可见性,不重复建档。
  4. :148 的 UI 回调是持 CBDM 锁执行的(dispatchDeviceAdded 在 synchronized 块内)——回调方若再碰 CBDM 的任何 synchronized 方法就是可重入,若做耗时操作就延长持锁。68 §5 134132 的锁问题的又一伏笔。

2.3 组是怎么归的(两个组管理器)

CsipDeviceManager(LE Audio 协调组)HearingAidDeviceManager(助听双耳)
组 keygroupId,来自 getBaseGroupId()(:69-87)问 CsipSetCoordinatorProfile.getGroupUuidMapByDevice() 取 value==BluetoothUuid.CAP 的 keyhiSyncId,来自 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 的构造顺序(为什么是这个顺序)

LocalBluetoothManagerstatic 单例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:75LocalBluetoothManager.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、getProfileProxystartScanning(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 一次性移除
}

三个要点:

  1. :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 滑窗 getBondState 751 次=binder 热点 No.1、findDevice 502 次。埋点分支 dev-bt-trace-134132,点位清单见 jira-data/N801PROB-134132/analysis/TRACE-INSTRUMENTATION.md。)
  2. :290 release() 很轻:只是 mHandler.removeCallbacksAndMessages(null)(CBD:245-247),清掉 pending 的消息而已。
  3. :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 广播里不删 CBDBondStateChangedHandler(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

一生总结(背诵版)

  • 诞生:四个事件入口 → addDevicenew 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 标记002(:240/:249 包住 getBluetoothClass)
addDevice 重载单参 + (device, leScanFilters) 双参(配对期带 BLE 滤器建助听标记)仅单参仅单参
clearNonBondedDevices:284-294 stream+release:189-193 裸 removeIf,无 release():250-262 同 f3dif
CSIP 组管理完整无 CsipDeviceManager 文件(无 LE Audio)完整,同构
clearNonBondedSubDevicesmember 用 removeIf,首个带 member 后 return(:303)只处理 sub,无 membertoArray 逐个删(比 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)