92 · XML 解析与 Controller 装配机制
系列:90-总览 | 91-启动骨架 | 92-Controller 装配 | 93-首屏数据 | 94-扫描上屏 行号基线:fuben_micarsettings dev HEAD(2026-09-13)。核心类:
BaseXmlParserSettingsFragment、PreferenceController、PreferenceControllerListHelper、PreferenceXmlParser(均在base/settingsBaseUi/base/settingsBaseLib)。
0. 一句话结论
蓝牙主页不是手写 findViewById 搭出来的,而是:XML 声明”有哪些 Preference、各归谁管” → 框架两次解析(一次拿元数据反射建 Controller,一次真正 inflate 出 Preference 树)→ Controller 注册成 Fragment 的生命周期观察者 → 生命周期事件自动驱动 onCreateInternal → … → refreshUi → updateState。理解这套机制,就理解了所有车机设置页。
1. 全景图:从 XML 到可交互页面
flowchart TD subgraph XML["bluetooth_mainlist_fragment.xml"] X1["MiCarConnectSwitchPreference<br/>controller=BluetoothSwitchController"] X2["MiCarPreferenceCategory pk_bluetooth_paired_devices<br/>controller=BluetoothBondedDevicesPrefController"] X3["CarUIPreferenceCategory pk_bluetooth_available_devices_e2<br/>controller=BluetoothUnbondedDevicesPrefController"] end subgraph Parse1["第①次解析: 元数据 (onAttach)"] M1["PreferenceXmlParser.extractMetadata<br/>一次解析拿齐 4 类 flag<br/>BaseXmlParserSettingsFragment.java:318-330"] M2["getController() 读 settings:controller<br/>PreferenceXmlParser.java:174-176"] end subgraph Build["反射建 Controller"] B1["PreferenceControllerListHelper<br/>.getPreferenceControllersFromMetadata :81-85"] B2["Class.forName + 构造签名<br/>(Context, key, FragmentController, CarUxRestrictions)<br/>:110-129"] B3["Router.holdController 登记 :119-121"] end subgraph Parse2["第②次解析: Preference 树 (onCreatePreferences)"] P1["addPreferencesFromResource :654<br/>micarx 框架 inflate 成对象树 :647-667"] P2["screen.findPreference(key)<br/>→ controller.setPreference(pref) :655-660"] end subgraph Life["生命周期绑定"] L1["lifecycle.addObserver(controller) :335-336<br/>controller 实现 DefaultLifecycleObserver"] L2["mPreferenceControllers (List, 驱动用)<br/>mPreferenceControllersLookup (Map, use() 用)<br/>:134-135, :335-339"] end XML --> Parse1 --> Build --> L2 XML --> Parse2 B2 --> B3 Parse2 --> Life Build --> Life
2. 两次解析:各取所需
第①次(元数据):onAttach() 里 extractMetadata(...)(BaseXmlParserSettingsFragment.java:318-330),一次拿齐 FLAG_NEED_KEY | FLAG_NEED_PREF_CONTROLLER | FLAG_NEED_DEPEND_KEY | FLAG_NEED_HIDDE_FEATURES——2026-06-04 的优化把原先 3 次解析合并成 1 次(注释 :314-317)。目的只有一个:在 Preference 树存在之前,先把 Controller 建出来。
第②次(视图树):onCreatePreferences()(:647-667)里 addPreferencesFromResource(resId)(:654),由 micarx Preference 框架真正 inflate XML 成 Preference 对象树。
sequenceDiagram participant F as BaseXmlParserSettingsFragment participant XP as PreferenceXmlParser participant H as PreferenceControllerListHelper participant C as Controller(反射新建) participant S as PreferenceScreen Note over F: onAttach() F->>XP: extractMetadata(resId, 4类flag) [:318-330] XP-->>F: Bundle 列表(含 METADATA_CONTROLLER) F->>H: getPreferenceControllersFromMetadata [:332-334] H->>C: Class.forName+newInstance [:110-129] H->>H: Router.holdController [:119-121] F->>F: lifecycle.addObserver(C) [:335-336] Note over F: onCreatePreferences() F->>S: addPreferencesFromResource [:654] F->>S: findPreference(controller.key) [:655-660] S-->>C: setPreference(pref)<br/>类型校验+挂点击/变更监听 [:329-338]
3. 反射实例化的精确细节
PreferenceControllerListHelper.createInstance()(:110-129):
Class.forName(className) // :114
.getConstructor(Context.class, String.class,
FragmentController.class, CarUxRestrictions.class) // :115-116 固定构造签名
.newInstance(context, key, fragmentController, uxRestrictions) // :118约束:所有 Controller 必须遵守这个四参构造签名——这就是为什么 BluetoothUnbondedDevicesPrefController 的构造函数(:134-141)长得千篇一律。FragmentController 传的是 Fragment 自己(见 §6)。
两个存储结构:
| 结构 | 类型 | 用途 |
|---|---|---|
mPreferenceControllers | List(:135) | 生命周期 forEach 驱动 |
mPreferenceControllersLookup | Map<Class, List<Controller>>(:134) | use() 按键查找 |
use(Class, keyResId)(:242-259):先按 Class 取列表,再按 getString(keyResId) 匹配 controller.getPreferenceKey();getContext()==null 时返回 null——这就是 36 篇里 notifyVoiceActive 必须先判 isAdded() 的原因。
4. 生命周期契约:不是手写循环,是观察者模式
PreferenceController 实现 DefaultLifecycleObserver(PreferenceController.java:106-108),onAttach 时注册、onDetach 时移除(:734-735)。没有手写的 for-each onStart/onResume 循环——生命周期事件由 Fragment 的 Lifecycle 自动分发。
flowchart LR subgraph FragLife["Fragment 生命周期"] FL1[onCreate] --> FL2[onViewCreated] --> FL3[onStart] --> FL4[onResume] FL4 --> FL5[onPause] --> FL6[onStop] --> FL7[onDestroy] end subgraph CtrlLife["PreferenceController final 模板 (PreferenceController.java)"] C1["onCreate :400-412<br/>UNSUPPORTED短路→onCreateInternal<br/>→mIsCreated=true→refreshUi()"] C2["onViewCreated :414-426<br/>→onViewCreatedInternal"] C3["onStart :433-441<br/>→onStartInternal→mIsStarted=true<br/>→refreshUi()"] C4["onResume :447-453<br/>→onResumeInternal"] C5["onPause :459-465"] C6["onStop :471-478<br/>→mIsStarted=false"] C7["onDestroy :485-493<br/>→mIsCreated=false→dismissAutoDismissDialog"] end FL1 -.Lifecycle 分发.-> C1 FL2 -.唯一手写forEach :597.-> C2 FL3 -.-> C3 FL4 -.-> C4 FL5 -.-> C5 FL6 -.-> C6 FL7 -.-> C7
要点:
- 每个入口先做
getAvailabilityStatus() == UNSUPPORTED_ON_DEVICE短路(不可用设备上直接隐藏 preference)。 - 唯二手写驱动点:
onViewCreated的 forEach(BaseXmlParserSettingsFragment.java:597)和 UxRestrictions 变化时的 forEach(:774-781)。 - 基类空实现集中在
PreferenceController.java:567-623;onCreateInternal/onResumeInternal默认会调openSecondPageIfNeed()(:818-830,处理”二级页直达”intent extra)。
5. refreshUi / updateState:首帧数据的闸门
flowchart TD R["refreshUi() PreferenceController.java:359-375 (final)"] --> G1{"mIsCreated?"} G1 -->|否| X[直接 return] G1 -->|是| G2{"isAvailable()?<br/>:388-391"} G2 -->|否| H["preference.setVisible(false) :373"] G2 -->|是| V["setVisible(true)<br/>受 defaultVisibleToUserWhenRefreshUi 控制 :384-386"] V --> U["updateState(mPreference) :639 基类空实现,子类重写"] U --> UX["onApplyUxRestrictions(mUxRestrictions)"]
updateState 的触发时机(全部经过 refreshUi 这道闸门):
| 时机 | 链路 |
|---|---|
| onCreate 末尾 | onCreate → refreshUi |
| onStart 末尾 | onStart → refreshUi |
| UxRestrictions 变化 | onUxRestrictionsChanged(:346-349) → refreshUi |
| 子类主动 | 任意时刻 refreshUi()(如 SwitchController 状态变化 :265) |
关键结论:蓝牙三 Controller 的首屏数据都被 mIsViewInitialized 挡到 onResume——Bonded(:334)/Unbonded(:147)的 updateState 开头都有 if (!mIsViewInitialized) return;,而置位点在 BluetoothPreferenceController.onResumeInternal()(:129-133,置 true 后当场调一次 updateState)。所以 onCreate/onStart 里的 refreshUi 对这两个列表是空跑,真正的首刷发生在 Fragment onResume。
6. 三 Controller 初始化清单(首屏数据何时开拉)
共同底座 BluetoothPreferenceController:
| 方法 | 行号 | 动作 |
|---|---|---|
onCreateInternal | :70-73 | 拿 LocalBluetoothManager(MiBluetoothUtils.getLocalBtManager()) |
onStartInternal | :113-117 | EventManager.registerCallback(this) + mIsControllerActive=true |
onResumeInternal | :129-133 | mIsViewInitialized=true + updateState(getPreference()) |
onStopInternal | :120-124 | 注销 callback + active=false |
BluetoothSwitchController:
onCreateInternal(:90-112):onCreate 阶段就mBluetoothAdapter.getState()首刷开关 UI(:97-101)、注册ACTION_STATE_CHANGED广播(:104-109,API33+ 带 EXPORTED)。onStartInternal(:115-128):设 summary(本机蓝牙名)+setForegroundActivity。onResumeInternal(:131-134):syncDeviceName()(车机名同步给 adapter)。- 车辆属性:订阅
Driving.CAR_MODE_STATUS(:72-76),运输模式禁用开关(:83-87)。
BluetoothBondedDevicesPrefController:不重写 onCreate/onStart/onResume(全走底座)。首开 updateState(:332-348)延迟 BLUETOOTH_DATA_LAZY_TIME=200ms(MiBluetoothUtils.kt:59)→ innerUpdateState(:352-400):后台线程取 getCachedDevicesCopy() → 过滤 → 回主线程 removeAll+逐个 addPreference。onPauseInternal(:464-466)重置首开标志。
BluetoothUnbondedDevicesPrefController:
onCreateInternal(:197-206):设标题色 + 注册BluetoothScanManager扫描监听器。onResumeInternal(:209-213):清 pause 标志 +startLoading()(:600-605 开启动画)。- 首开同样 200ms 延迟 →
innerUpdateState(:164-194):清缓存 → 重建 → 后台线程 triggerScan(“update_state”)(:193)。 onPauseInternal(:216-232):停动画 + 后台清缓存 + stopDiscovery。
7. FragmentController:Controller 反向操作宿主的接口
FragmentController(base/settingsBaseUi/.../FragmentController.java:36)是接口,实现者就是 BaseXmlParserSettingsFragment 自己(:98-101,构造 controller 时传 this)。能力子集:
| 方法 | 行号 | 用途 |
|---|---|---|
launchFragment / goBack | :43 / :57 | 页面跳转 |
findPreferenceByKey / findControllerByKey | :84-88 | 跨 controller 互访 |
onHandleCmd | :90 | 数据请求总线(PreferenceController.requestDataFromHost :764-784) |
listViewGoToPosition | :92-93 → 实现 :868-877 | 滚动(例:配对成功滚到顶部,Bonded:422) |
getSettingsLifecycle / ViewModel 获取 | :110 | 生命周期/数据共享 |
8. CarUxRestrictions(驾驶限制)参与方式
flowchart LR CS[CarService] --> H["CarUxRestrictionsHelper<br/>registerListener 立即回调一次<br/>CarUxRestrictionsHelper.java:124-140"] H --> A["BaseCarSettingsActivity.onUxRestrictionsChanged :475-499"] A --> F["Fragment.onUxRestrictionsChanged :773-781<br/>forEach controller"] F --> C["PreferenceController.onUxRestrictionsChanged :345-349<br/>→ refreshUi → onApplyUxRestrictions :649-658"] C -->|"命中限制名单且非 AVAILABLE_FOR_VIEWING"| RP["restrictPreference :667-680<br/>置 uxRestricted + 点击 toast<br/>递归应用到子项"]
⚠️ 注意 restrictExperienceCustomize() 默认 true(:503-505)会短路基类的整条分发——具体页面是否受限取决于各 Activity 重写,排查”行车中某开关不可点”时先看这个开关。
9. 设计要点与坑(改代码前必读)
- 两次解析分工:元数据解析 ≠ Preference 树构建。改 XML 加 controller 属性,两次解析路径都要验证。
- 构造签名是硬约束:四参
(Context, String, FragmentController, CarUxRestrictions),反射调用,编译期不检查——签名错了运行期才炸。 mIsViewInitialized闸门:蓝牙列表首刷在 onResume,别在 onCreate/onStart 里找列表数据加载。- 首开创 200ms 延迟:
BLUETOOTH_DATA_LAZY_TIME(MiBluetoothUtils.kt:59)是给首帧让路的设计(先出框架再填数据),onPause重置——每次进页都重新走延迟,不是只有冷启动。 - 生命周期观察者模式:没有手写驱动循环,排查”为什么 controller 没收到 onXxx”先查
lifecycle.addObserver是否执行(onAttach 时序)。 - use() 的 null 三种形态:context 为空 / Class 未注册 / key 不匹配——调用方必须判空。
10. 与既有篇目关系
- 蓝牙三 Controller 的业务细节 → 01 发现流程 / 03 设备详情 / 34 摘要状态机
- EventManager 回调契约 → 23-BluetoothCallback契约
- 语音角标链路 → 36-可见即可说