RV 菜单 item 布局瘦身技术方案

来源:N801PROB-126046 Settings 冷启动 review(perf_review_v2.md)第一波优化项 #1 Trace 证据:首帧 doFrame 369ms 中,RV OnLayout ×2 = 227.92ms(inflate×25=107.56ms / CreateView×18=85.79ms / OnBindView×18=59.30ms),ConstraintLayout solver 占 P6 采样 ~4% 影响范围:base/settingsBaseUi(main sourceSet)→ DCD cn / XCD cn / XCD global 全变体生效;不动 region/dcddif/xcddif/ 差异目录


一、现状量化

1.1 首帧布局结构(双栏)

首帧 doFrame 369ms
├── 左栏菜单 RV(TopLevelMenuFragment,17 项菜单,可视 ~9 项)
│     item 布局:car_ui_preference_menu.xml
└── 右栏内容 RV(VehicleControlSettingsFragment,可视 ~9 项)
      item 布局:micar_ui_pref_switch / micar_ui_pref_basic /
                micar_ui_pref_new_tab / category / divider ...

两个 RecyclerView 在同一个 doFrame 内 layout(RV OnLayout ×2 = 227.92ms),合计创建 ~18 个 item。RV 本身已只创建可视区 item,优化空间在单项成本,不在数量

1.2 item 布局问题清单(源码实测)

左栏 car_ui_preference_menu.xml(4 View,已较简,但仍有冗余):

LinearLayout (root, 仅 1 个 child —— 冗余层)
└── LinearLayout @id/menu_background
    ├── ImageView @android:id/icon (初始 GONE)
    └── TextView  @android:id/title

右栏 micar_ui_pref_switch.xml(9 View,2 层嵌套 CL):

ConstraintLayout (root, 仅 1 个 child —— 冗余层)
└── ConstraintLayout @id/micar_ui_switch
    └── <include micar_ui_pref_switch_core(merge)>
        ├── PreCheckSwitch / title / summary(GONE) / icon_title(GONE)
        ├── title_right(GONE) / icon_right(GONE) / widget_frame(GONE)

右栏 micar_ui_pref_basic.xml(10 View,2 层嵌套 CL + GONE widget_frame): root CL 只有 micar_ui_basic_main_root(内层 CL)+ GONE 的 widget_frame,同样是 wrapper-on-wrapper。

右栏 micar_ui_pref_new_tab.xml:同构(root CL 套 inner CL + widget_frame + summary)。

1.3 每项成本构成(由 trace 反推)

成本项来源性质
XML 解析 + 反射构造inflate 107ms/25 次 ≈ 4.3ms/次与 View 数、嵌套层数成正比
ConstraintLayout solverapplyConstraints/addEquality 等 ~4% 采样与 CL 层数×约束数成正比
首次类加载PreCheckSwitch/SwitchCompat 等首个 item 付税(max 25.4ms)Baseline Profile 范畴(另行)
OnBindView 3.3ms/项日志 + 无障碍描述构建 + AutoLayout + TouchDelegateJava 绑定逻辑

二、改动方案(第一波:零风险扁平化 + 绑定瘦身)

改动 1:car_ui_preference_menu.xml 去掉冗余外层

  • 外层 LinearLayout 唯一 child 就是 menu_background,将 padding/orientation 合并到内层并提升为 root。
  • 省 1 个 ViewGroup × 左栏全部可视项(~9 个)
  • id 全部保留,Java 侧 findViewById(R.id.menu_background) 不受影响。

改动 2:micar_ui_pref_switch.xml 去掉冗余 root CL

  • root CL 唯一 child 是 @id/micar_ui_switch(内层 CL),直接把内层提升为 root。
  • 省 1 个 ConstraintLayout × 每个 switch item(右栏首屏 switch 项占比高)
  • <include> 的 merge 目标不变(parentTag 本就是 ConstraintLayout)。

改动 3(已推迟到第二波):micar_ui_pref_basic.xml 扁平化

  • 推迟原因:调研发现 @id/micar_ui_basic_main_root 被 4 处代码引用:
    • BasicWithSummaryPreference.kt:52 — 给 main_root 设置独立背景(功能依赖容器边界)
    • ButtonIconAdapter.kt:38NewBluetoothDevicePreference.java:150MiCarBluetoothBondedDevicePreference.kt:122
  • 扁平化后 findViewById 命中 root 本身,背景会错误覆盖 widget_frame 区域 → 需改用 Barrier 方案重做,伴随视觉回归,不属”零风险第一波”。

改动 4(已推迟到第二波):micar_ui_pref_new_tab.xml 扁平化

  • 同改动 3,且该布局 include 的 basic_main_for_new_tab 结构不同,需单独验证。

改动 5:VoiceAssistMenuPreference.onBindViewHolder 删除每次绑定的 MLog.i

// 删除:每次绑定都执行字符串模板 + 日志写盘
MLog.tag(TAG).i("currentTextColor=${mTitleTv?.currentTextColor}, ...")
  • 菜单 17 项每次绑定/刷新都触发,trace 中 logger(PrettyFormatStrategy/SplitByLogdChunks)在启动期为主线程显著开销项。
  • 顺带保留 setMenuDescription(无障碍功能,不能删)。

预期收益

机制预估
改动 1(左栏菜单 item)每项少 1 个 LinearLayout × 可视 ~9 项inflate 微降
改动 2(右栏 switch item)每项少 1 个 ConstraintLayout + 一轮 solver 求解10-25ms
改动 5(绑定日志)消除 17+ 次日志格式化+写盘5-10ms
合计(第一波实际执行范围)15-35ms

说明:第一波只执行”零视觉风险”部分(改动 1/2/5)。更大的收益在第二波(改动 3/4 的 Barrier 扁平化、GONE View → ViewStub、CL→静态布局重写、OnBind 的 DescriptionBuilder/AutoLayout 延迟化),需视觉回归验证,另立方案。


三、实际执行 diff(第一波)

文件改动
base/settingsBaseUi/src/main/res/layout/car_ui_preference_menu.xml外层 LinearLayout(唯一 child=menu_background)合并为一层;总高 88+32 不变,id/tag 保留
base/settingsBaseUi/src/main/res/layout/micar_ui_pref_switch.xmlroot CL(唯一 child=micar_ui_switch)合并为一层,@id/micar_ui_switch 保留
base/settingsBaseUi/.../preferences/VoiceAssistMenuPreference.kt删除 onBindViewHolder 中每次绑定的 MLog.i(及未用 import)

三、不改什么 & 风险控制

决策理由
不合并 <include>(switch_core/basic_main 被 5/17 个布局复用)避免 17 处内容复制,维护风险 > 收益
不把 GONE View 改 ViewStub(本波)icon_right(箭头)、title_right 等高频使用,需逐项核对;留下一波
不动 PreCheckSwitch 内部(21ms 资源加载)自定义 View 内部逻辑,单独评估
不动 region/cnregion/global 差异 XML菜单条目定义按区域分化,本方案只改 base 共享布局
绑定代码均判空(?.let / as?)已核实 BasicPreference/MenuPreference 空安全,扁平化不引发 NPE

验证计划

  1. ./gradlew 编译 xcd cn 变体(micarsettings-build skill 流程)
  2. 装车(fermi 测试机)对比:设置首页首屏截图 diff(改动前后应像素级一致)
  3. 重采 perfetto trace,同口径(fork→首帧 doFrame 结束)对比 RV OnLayout 耗时
  4. 回归:左右栏滚动、switch 点击、深浅色切换、无障碍(TalkBack 读屏)