93 · 首屏数据:开关状态与已配对列表
系列:90-总览 | 91-启动骨架 | 92-Controller 装配 | 93-首屏数据 | 94-扫描上屏 行号基线:fuben_micarsettings dev HEAD(2026-09-13)。
0. 一句话结论
首帧数据 100% 来自进程内缓存和同步 binder 直读,与广播无关——广播只负责首帧之后的增量修正。开关约 4 跳(全主线程),已配对列表约 7 跳(主→后台→主分段)。
1. 总览:两条首帧链路
flowchart TD subgraph SW["开关首帧 (~4跳, 全主线程)"] S1["onAttach 构造 controller"] --> S2["onCreateInternal :97-101<br/>adapter.getState() (binder)"] S2 --> S3["handleStateChanged :205-266<br/>直推 setChecked/setLoading/setEnabled"] S3 --> S4["notifyChanged → bind<br/>MiCarNewSwitchPreference:181-240"] end subgraph BD["已配对列表首帧 (~7跳, 主→后台→主)"] B1["①onAttach: LBM单例+readPairedDevices<br/>(主线程同步建缓存)"] --> B2["②onResume: mIsViewInitialized=true"] B2 --> B3["③updateState: 主线程延迟200ms"] B3 --> B4["④后台线程: getCachedDevicesCopy<br/>+ bondedDeviceFilter(binder实时查)"] B4 --> B5["⑤主线程: removeAll+逐个addPreference<br/>(binarySearch排序插入)"] B5 --> B6["⑥onAttached→refreshDeviceUi<br/>定title/summary/icon"] B6 --> B7["⑦notifyHierarchyChanged→onBindViewHolder"] end
2. 开关首帧:直读 adapter,不绕弯
2.1 数据源
mBluetoothAdapter = MiBluetoothUtils.getAdapter()(BluetoothSwitchController.java:46;本体 =getSystemService(BluetoothManager).adapter,MiBluetoothUtils.kt:107-111)- 首帧读取点在
onCreateInternal(:97-101)——三个 controller 里唯一在 onCreate 就首刷的(两个列表被mIsViewInitialized挡到 onResume,见 92 章 §5) - 注意:没用
MiBluetoothUtils.isEnabled(),直接adapter.getState()(binder 到蓝牙服务);STATE_ON/OFF之外一律映射BLUETOOTH_UI_STATE_SWITCHING - 前置干扰项:父类
BaseVehicleNewSwitchPrefController.onCreateInternal会先用车辆信号CAR_MODE_STATUSsetChecked 一次(BaseVehicleNewSwitchPrefController.kt:212-215),随后被 adapter 真实状态覆盖——首帧以 adapter 为准
2.2 状态映射(handleStateChanged :205-266)
stateDiagram-v2 [*] --> 已开启: STATE_ON<br/>checked=true/loading=false/enabled=看限制 :228-234 [*] --> 已关闭: STATE_OFF<br/>checked=false/loading=false :256-263 [*] --> 切换中: TURNING_ON/TURNING_OFF<br/>loading=true+提前拨动checked :222-255 切换中 --> 已开启: 广播回流 handleStateChanged 切换中 --> 已关闭: 广播回流
冷知识:开关整条继承链无人重写 updateState(已 grep 核实)→ refreshUi() 对开关是空操作,开关状态全由 handleStateChanged 直推 preference。这就是为什么开关能在 onCreate 就出状态,不受 mIsViewInitialized 影响。
2.3 渲染与点击
- 布局
connection_switch_layout.xml(PreCheckSwitch + title + summary);setChecked → persistBoolean + mUiHandler.post { notifyChanged() }(MiCarNewSwitchPreference.kt:148-156) - summary 在 onStart 设:
"可被发现为"<设备名>"(:117-118;DeviceUtil.getDeviceName同步读 Settings.Global) - 点击:
onCreateInternal:94用setOnPreferenceChangeListener(this)覆盖了基类装的派发器 → 直达onPreferenceChange(:152-199):互联确认框(Mirror/AA 连接中关蓝牙需确认)→setLoading(true)→Trace.beginSection+enable()/disable()(:187-194)→ 广播回流handleStateChanged校准 +Trace.endSection(:208-211,开关耗时是有 trace 埋点的) - 防跳变:页面回前台时若 SWITCHING 态,不按 adapter state 刷新(:121-127)
3. settingslib 数据底座:单例何时建、缓存何时填
sequenceDiagram autonumber participant F as Fragment onAttach (:210) participant U as MiBluetoothUtils participant LBM as LocalBluetoothManager participant BEM as BluetoothEventManager participant CBDM as CachedBluetoothDeviceManager F->>U: getLocalBtManager() [MiBluetoothUtils.kt:75-91] U->>LBM: getInstance(context, cb) 单首创 [:57-74] Note over LBM: 构造 :109-121 全在主线程: LBM->>CBDM: new (:113) LBM->>BEM: new (:114) → registerAdapterIntentReceiver<br/>handler=null → 回调全在主线程 LBM->>LBM: LocalBluetoothProfileManager<br/>+updateLocalProfiles() :116-119<br/>(各profile proxy 异步bind开始) LBM->>BEM: readPairedDevices() :120 BEM->>CBDM: getBondedDevices(binder)逐个addDevice :245-261 CBDM->>CBDM: new CachedBluetoothDevice→fillData :123-141 Note over CBDM: ★此时 mCallbacks 为空(controller onStart才注册)<br/>dispatchDeviceAdded 无人接收<br/>→ 首帧=纯缓存,与广播无关
另一个 readPairedDevices 触发点:每次蓝牙打开(LocalBluetoothAdapter.setBluetoothStateInt(STATE_ON) :251-257)。
4. 已配对列表首帧:缓存 × 实时过滤
4.1 innerUpdateState 分段模型(BluetoothBondedDevicesPrefController.java:352-401)
sequenceDiagram autonumber participant M as 主线程 participant BG as 后台线程(ThreadUtils) participant Sys as 蓝牙服务(binder) M->>M: updateState :332-348<br/>mIsViewInitialized门控 :333-335<br/>首开 postDelayed 200ms :337-344 M->>BG: postOnBackgroundThread :356 BG->>BG: getCachedDevicesCopy() :363-365<br/>(synchronized 拷贝) BG->>Sys: bondedDeviceFilter:<br/>getAdapter().bondedDevices.contains(device)<br/>[MiBluetoothUtils.kt:203-212] Note over BG: ★过滤器不是读缓存bondState<br/>而是后台线程上实时查系统配对表<br/>(注释自称"对齐AOSP BondedDeviceTypeFilter") BG->>M: mHandler.post :377 M->>M: removeAll + mPreferenceMap.clear :383-384 M->>Sys: adapter.isEnabled() 才逐个 addPreference :387-391 M->>M: 组可见性(count>0 && isEnabled) :393-396<br/>refreshVoiceAssistViewOrder :397
4.2 单条创建与排序插入
addPreference(:71-87)→ createDevicePreference(:142-202):新建 MiCarBluetoothBondedDevicePreference → setRightIcon(右箭头 :148)→ 点击监听(右箭头进详情 :162-164;整行走连接/断开 :167-199,isBusy 防抖忽略)→ PreferenceGroup.addPreference(micarx 库,javap 实证:Collections.binarySearch 排序插入)→ onAttached → registerCallback + refreshDeviceUi(NewBluetoothDevicePreference.java:113-118)。
排序规则(MiCarBluetoothBondedDevicePreference.compareTo:209-217):已连接排最前,其余按 title 字母序(忽略大小写)。注意:只在插入瞬间排序——运行中连接状态变化不重排,只刷单行(onProfileConnectionStateChanged:469-501);整表重排只在全量路径(STATE_ON/配对态变化/生命周期的 refreshUi)。
5. 三态摘要:首帧取值与 profile-proxy 竞态
判据(MiCarBluetoothBondedDevicePreference.getDeviceSummary:97-113):
flowchart LR A{"cachedDevice.isConnected<br/>CBD:912-923 遍历mProfiles实时问proxy"} -->|是| B["已连接"] A -->|否| C{"isBusy && mIsConnectingState"} C -->|是| D["正在连接..."] C -->|否| E["已保存"]
首帧两个确定性结论(取值时刻 = onAttached → refreshDeviceUi,主线程同步求值):
- 首帧”正在连接”不可能出现:
mIsConnectingState初值 false(NewBluetoothDevicePreference.java:66),唯一置位点在 controller 的onProfileConnectionStateChanged(:493-497)——首帧新建 preference 时必为 false。 - 首帧可能短暂”已保存”实为已连接(profile-proxy 竞态):
isConnected实时问 profile proxy,而 proxy 是异步 bind 的(§3 第 4 步);proxy 未就绪时A2dpProfile.getConnectionStatus:164-169直接返回 DISCONNECTED → 首帧”已保存”;proxy bind 完成后onServiceConnected → device.refresh() → dispatchAttributesChanged(CachedBluetoothDevice.java:1169-1173)→ preference 回调refreshDeviceUi纠正为”已连接”。这是首帧固有的自愈窗口,不是 bug;但如果纠正迟迟不来,查 proxy bind 链。
6. 装饰元素首帧填充时刻表
| 元素 | 首帧时刻 | 说明 |
|---|---|---|
| 右箭头 | 创建时 setRightIcon(:148)→ 首次 bind 画出;已连接换蓝色(kt:186-190) | 本地资源 |
| 右侧文字 | 首帧为空(bonded 从未 setRightText) | — |
| 电量/信号 | 主页布局里不存在(layout 已核实,无此 view) | 非未核实,是没有 |
| 语音序号角标 | 首帧 GONE;refreshVoiceAssistViewOrder(:117-140)在 innerUpdateState 末尾与 onPreferenceUpdateFinish 时补 | 见 91 章 §4 / 36 篇 |
| 连接动画条 | 首帧 GONE(mIsConnecting=false) | §5 |
7. 线程模型速查
| 链路 | 线程 | 锚点 |
|---|---|---|
| LBM 创建 + readPairedDevices + fillData | 主线程(onAttach) | Fragment:210;LBM:109-121 |
| 开关 getState / enable/disable(binder) | 主线程 | SwitchController:97,190-193 |
| settingslib 全部广播 dispatch | 主线程(handler=null) | BEM:227-237 |
| 已配对数据读取 + bondedDeviceFilter(binder) | 后台线程池(显式设计) | Bonded:356-374 |
| 已配对 UI 变更(removeAll/addPreference/排序) | 主线程(mHandler.post) | Bonded:377-398 |
CachedBluetoothDevice.refresh()(设备元数据) | 后台→主线程两段 | CBD:793-809 |
8. 排障要点
- 首帧数据与广播无关——“主页打开列表是空的”先查
readPairedDevices是否跑过(LBM 是否本进程首创)和bondedDeviceFilter的 binder 查询结果,别去查广播丢没丢。 - 开关状态不走 updateState——开关显示异常直接查
handleStateChanged和自有 receiver(:104-109),别在 refreshUi 链上浪费时间。 - “已保存”一闪而过变”已连接” = profile proxy 竞态自愈(§5),属固有设计;一直停在”已保存” 才是真问题(proxy bind 失败/34 篇状态机缺陷)。
- 首开 200ms 延迟每次进页都有(onPause 重置 mIsFirstOpen)——做启动性能测量时别把延迟当回归。