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)。

两个存储结构:

结构类型用途
mPreferenceControllersList(:135)生命周期 forEach 驱动
mPreferenceControllersLookupMap<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-117EventManager.registerCallback(this) + mIsControllerActive=true
onResumeInternal:129-133mIsViewInitialized=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. 设计要点与坑(改代码前必读)

  1. 两次解析分工:元数据解析 ≠ Preference 树构建。改 XML 加 controller 属性,两次解析路径都要验证。
  2. 构造签名是硬约束:四参 (Context, String, FragmentController, CarUxRestrictions),反射调用,编译期不检查——签名错了运行期才炸。
  3. mIsViewInitialized 闸门:蓝牙列表首刷在 onResume,别在 onCreate/onStart 里找列表数据加载。
  4. 首开创 200ms 延迟:BLUETOOTH_DATA_LAZY_TIME(MiBluetoothUtils.kt:59)是给首帧让路的设计(先出框架再填数据),onPause 重置——每次进页都重新走延迟,不是只有冷启动。
  5. 生命周期观察者模式:没有手写驱动循环,排查”为什么 controller 没收到 onXxx”先查 lifecycle.addObserver 是否执行(onAttach 时序)。
  6. use() 的 null 三种形态:context 为空 / Class 未注册 / key 不匹配——调用方必须判空。

10. 与既有篇目关系