Glide 车模图冷启动优化技术方案(N801PROB-126046 第一波#4)

来源:N801PROB-126046 Settings 冷启动 review 第一波优化项 #4 Trace 证据:glide-source-th 线程在 activityResume 前后(offset ~1018ms)执行 decodeBitmap 151.64ms(+7.18ms),后台 CPU 146ms 恰好压在 resume→首帧的关键窗口,与主线程抢核(该窗口主线程 R 饥饿 37.1ms) 影响范围:base/settingsBaseUi(main sourceSet)→ DCD cn / XCD cn / XCD global 全变体


一、现状分析

1.1 调用链

VehicleControlRightFragment(首页右栏).onViewCreated
  └─ BaseRightFragment.createCarModelLayout()            [onViewCreated 同步执行]
      └─ HomePageActivityReal.setImageResource()
          └─ CarModelSwitcher.setImageResource()
              └─ ImageView.setCarModeImageResourceIfDiff()  ← Glide 加载(MiCarSettingsExt.kt:219)
                  └─ glide-source-th: decodeBitmap 151.6ms ← 与 resume/首帧抢 CPU

1.2 资源现状

  • 车模图:micar_car_model_vehicle_control_main.webp1365×1365 VP8L(无损),649KB
  • VP8L 无损格式不支持子采样解码,必须全量解码出 1365×1365×4B ≈ 7.4MB 中间位图 → 这就是 151ms 的来源
  • 显示区域:SafeImageView(WRAP_CONTENT × MATCH_PARENT, FIT_END),实际显示远小于 1365px

1.3 Glide 配置问题

MiCarSettingsExt.kt setCarModeImageResourceIfDiff():

Glide.with(this).asBitmap().load(newResId)
    .apply(RequestOptions.diskCacheStrategyOf(DiskCacheStrategy.NONE))  // ← 问题1
    ...

DiskCacheStrategy.NONE 意味着每次冷启动都重新全量解码 VP8L,即使同一资源已解码过无数次。 缓存 key 是适配后的 resId(VehicleCarModeConfig.adapterVehicleRes 按车型/配置字映射,不同配置 → 不同 resId → 不同缓存项),改成 RESOURCE 缓存语义完全正确

1.4 已评估并否决的方案

方案结论
RGB_565 解码❌ 车模图含 alpha 通道(透明背景)与细腻渐变,565 会丢透明度并产生色带
启动早期后台预解码❌ bindApplication 期间 CPU 已饱和(7 核 100%),只会加剧主线程饥饿
缩减资源分辨率⏸ 有效但需设计出图+全车型全配置回归,第二波再评估

二、改动方案(两处,互补)

改动 1:解码结果磁盘缓存(温启动优化)

MiCarSettingsExt.kt:DiskCacheStrategy.NONEDiskCacheStrategy.RESOURCE

  • RESOURCE = 缓存解码+降采样后的位图;之后冷启动从磁盘读小图(几 ms),不再解码 VP8L
  • 首次开机仍解码一次,但为后续所有启动摊销
  • 零视觉变化,零 API 变化

改动 2:首页车模图延迟到首帧后加载(冷启动关键窗口优化)

BaseRightFragment.createCarModelLayout():仅对 setImageResource 路径(纯图片)做首帧后调度;setLayoutXml 路径(可能含交互 UI)不延迟

} else if (imgResId != Resources.ID_NULL) {
    // N801PROB-126046:车模图解码(~150ms 后台 CPU)推迟到首帧之后,
    // 避免与 resume→首帧渲染争抢 CPU;decorView 必绘制,不会漏触发
    View decorView = requireActivity().getWindow().getDecorView();
    OneShotPreDrawListener.add(decorView, () -> {
        int res = imgResId;
        decorView.post(() -> setImageResource(res, getInitCarModelTag()));
        return true;
    });
}
  • OneShotPreDrawListener 在首个 doFrame traversal 触发,内部 post 的 runnable 在该帧绘制完成后执行 → 精确落在”首帧上屏后”
  • hook 点是 Activity decorView(必绘制),不依赖 Fragment 自身 view 可见性,无漏触发风险
  • 视觉差异:车模图区域首帧为空,约 150-350ms 后图片出现(首页为过场动画场景,可接受;温导航换页时仅差一帧,无感知)

预期收益

场景收益
冷启动首帧窗口151ms 后台解码移出关键窗口,主线程 R 饥饿显著缓解(resume→frame 段 37ms 大部分)
后续冷启动RESOURCE 缓存命中后解码 151ms → ~5-10ms 磁盘读,温启动每次省 ~140ms 后台 CPU

三、风险控制

风险评估对策
首帧车模区域短暂空白低:首页有过场动画,图片 150-350ms 内出现装车目检确认可接受
RESOURCE 缓存占用磁盘低:单张降采样后 <1MBGlide 默认 250MB 池自管理
配置字变化导致图不对无:缓存 key=适配后 resId,配置变→key 变→重新解码语义已验证
OneShotPreDrawListener 不触发无:decorView 首帧必绘制与 Fragment view 解耦

四、验证计划

  1. :base:settingsBaseUi:assembleXcdDebug 编译通过
  2. 装车冷启动 5 次:确认车模图正常显示、无空白残留、无闪图
  3. 重采 perfetto 同口径对比:glide-source-th 的 decodeBitmap 应移出 resume→frame 窗口;第二次启动起 decodeBitmap 应 <15ms
  4. 深浅色切换、换页导航、配置字相关页面回归