Glide 车模图冷启动优化技术方案(N801PROB-126046 第一波#4)
来源:N801PROB-126046 Settings 冷启动 review 第一波优化项 #4 Trace 证据:
glide-source-th线程在 activityResume 前后(offset ~1018ms)执行decodeBitmap151.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.webp— 1365×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.NONE → DiskCacheStrategy.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 缓存占用磁盘 | 低:单张降采样后 <1MB | Glide 默认 250MB 池自管理 |
| 配置字变化导致图不对 | 无:缓存 key=适配后 resId,配置变→key 变→重新解码 | 语义已验证 |
| OneShotPreDrawListener 不触发 | 无:decorView 首帧必绘制 | 与 Fragment view 解耦 |
四、验证计划
:base:settingsBaseUi:assembleXcdDebug编译通过- 装车冷启动 5 次:确认车模图正常显示、无空白残留、无闪图
- 重采 perfetto 同口径对比:glide-source-th 的 decodeBitmap 应移出 resume→frame 窗口;第二次启动起 decodeBitmap 应 <15ms
- 深浅色切换、换页导航、配置字相关页面回归