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 solver | applyConstraints/addEquality 等 ~4% 采样 | 与 CL 层数×约束数成正比 |
| 首次类加载 | PreCheckSwitch/SwitchCompat 等首个 item 付税(max 25.4ms) | Baseline Profile 范畴(另行) |
| OnBindView 3.3ms/项 | 日志 + 无障碍描述构建 + AutoLayout + TouchDelegate | Java 绑定逻辑 |
二、改动方案(第一波:零风险扁平化 + 绑定瘦身)
改动 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:38、NewBluetoothDevicePreference.java:150、MiCarBluetoothBondedDevicePreference.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.xml | root 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/cn、region/global 差异 XML | 菜单条目定义按区域分化,本方案只改 base 共享布局 |
绑定代码均判空(?.let / as?) | 已核实 BasicPreference/MenuPreference 空安全,扁平化不引发 NPE |
验证计划
./gradlew编译 xcd cn 变体(micarsettings-build skill 流程)- 装车(fermi 测试机)对比:设置首页首屏截图 diff(改动前后应像素级一致)
- 重采 perfetto trace,同口径(fork→首帧 doFrame 结束)对比 RV OnLayout 耗时
- 回归:左右栏滚动、switch 点击、深浅色切换、无障碍(TalkBack 读屏)