一、车载性能优化全景:CPU/GPU 的协同战场
车载系统的性能优化,说白了就是一场与资源瓶颈的博弈。我做了这么多年车载 Android 定制,最深的体会是:车机不像手机,手机卡了你可以重启,车机卡了——嗯,用户可能正在倒车入库,或者导航到一半突然掉帧。所以 CPU 调频、GPU 渲染、帧率监控这些,不是锦上添花,是底线。
这一章我会把 CPU 和 GPU 的优化手段拆开讲,但你要记住:它们不是孤立的。CPU 负责调度、逻辑、任务分发,GPU 负责渲染、合成、显示。两者配合不好,就会出现「CPU 在等 GPU 画完,GPU 在等 CPU 喂数据」的死锁局面。
二、CPU 调频与调度优化:让每一 Hz 都花在刀刃上
2.1 cpufreq 调频策略:别让 CPU 乱跳频
车载 Android 的 CPU 调频,我建议你直接抛弃 ondemand 和 conservative。这两个策略在车机上表现很差——ondemand 响应太激进,一有负载就冲高频,功耗飙升;conservative 又太保守,界面滑动都卡。
我个人习惯用 schedutil。它是调度器驱动的调频策略,能根据任务的负载情况动态调整频率。说白了,就是让调度器告诉你「这个任务需要多快」,然后调频器跟着走。
// 查看当前调频策略
adb shell cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
// 设置为 schedutil(需要 root)
adb shell echo "schedutil" > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor
// 调整调频速率(单位:ms)
adb shell echo "4" > /sys/devices/system/cpu/cpu0/cpufreq/schedutil/up_rate_limit_ms
adb shell echo "4" > /sys/devices/system/cpu/cpu0/cpufreq/schedutil/down_rate_limit_ms
2.2 调度优化:把关键线程绑到大核上
车机上最常见的卡顿原因是什么?不是 CPU 不够快,而是关键线程被调度到了小核上。你想想看,UI 渲染线程跑到 A55 小核上,那画面能流畅才怪。
我建议的做法是:
- SurfaceFlinger 主线程:绑定到 CPU4-7(大核集群)
- Choreographer 回调:设置 cpuset 为 top-app
- HAL 层音频/视频线程:使用 RT 调度策略,优先级 90+
// 示例:将 SurfaceFlinger 绑定到大核
// 找到 SurfaceFlinger 的 PID
adb shell ps -A | grep surfaceflinger
// 查看当前 CPU 亲和性
adb shell taskset -p <PID>
// 绑定到 CPU4-7(二进制 11110000 = 0xF0)
adb shell taskset -p 0xF0 <PID>
2.3 SchedTune 与 cpuset:Android 的调度黑盒
Android 的调度框架里,SchedTune 是个好东西。它允许你给不同优先级的任务设置不同的「boost」值。比如,前台应用的 boost 值可以设高一点,后台服务设低一点。
在车机上,我通常这样配置:
| 场景 | cpuset | SchedTune Boost | 说明 |
|---|---|---|---|
| 导航前台 | 0-7(所有核) | 20 | 保证地图渲染流畅 |
| 音乐后台 | 0-3(小核) | 0 | 省电,不影响前台 |
| 语音交互 | 4-7(大核) | 30 | 低延迟,快速响应 |
| 系统服务 | 0-7 | 5 | 保证基础服务不饿死 |
三、GPU 渲染优化:别让 GPU 做无用功
3.1 Overdraw 优化:少画一层,快一帧
Overdraw 是 GPU 渲染的头号杀手。说白了,就是 GPU 画了被遮挡的像素,白费力气。在车机上,这个问题尤其严重——很多车载 Launcher 喜欢用多层背景、半透明遮罩,Overdraw 轻松到 3x 甚至 4x。
我建议你用 GPU 调试工具 来检测:
- 在开发者选项中开启「显示 GPU 过度绘制」
- 颜色越红,Overdraw 越严重
- 目标:全屏蓝色(1x Overdraw)以下
// 通过 adb 开启 GPU Overdraw 调试
adb shell setprop debug.hwui.overdraw show
// 关闭
adb shell setprop debug.hwui.overdraw false
3.2 合成策略:HWC vs GPU Composer
Android 的显示合成有两种方式:HWC(硬件合成器)和 GPU Composer。HWC 是硬件加速的,功耗低、性能好;GPU Composer 是用 GPU 去合成,灵活但费电。
在车机上,我强烈建议优先使用 HWC。但有个坑——HWC 对图层数量有限制。我记得在某个项目里,HWC 只支持 4 个图层,结果系统有 5 个图层,第 5 个就 fallback 到 GPU 合成了,帧率直接掉了一半。
3.3 纹理压缩:ASTC 是车机的最佳选择
纹理带宽是 GPU 的另一个瓶颈。车机屏幕分辨率越来越高(1920x1080 起步,4K 也不少见),纹理数据量巨大。如果不压缩,GPU 的带宽会被吃满。
我推荐使用 ASTC(自适应可伸缩纹理压缩)。它比 ETC2 压缩率更高,而且支持透明通道。车机上的 GPU(Adreno、Mali)基本都支持 ASTC。
| 纹理格式 | 压缩比 | 质量 | 适用场景 |
|---|---|---|---|
| RGBA8888 | 1:1(无压缩) | 最高 | 图标、小尺寸 UI 元素 |
| ETC2 | 6:1 | 中 | 不支持透明通道 |
| ASTC 4x4 | 4:1 | 高 | 高质量 UI 背景 |
| ASTC 8x8 | 8:1 | 中高 | 大尺寸图片、地图瓦片 |
四、帧率监控与 Jank 检测:用数据说话
4.1 Choreographer 帧率监控:最直接的指标
帧率是性能优化的「体温计」。我习惯用 Choreographer 的 FrameCallback 来监控每一帧的耗时。说白了,就是让系统在每一帧绘制完成后回调我,我记录时间戳,然后算帧率。
// 在应用层监控帧率
Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
long lastFrameTime = 0;
@Override
public void doFrame(long frameTimeNanos) {
if (lastFrameTime != 0) {
long delta = (frameTimeNanos - lastFrameTime) / 1000000; // 转毫秒
float fps = 1000f / delta;
Log.d("FrameMonitor", "当前帧率: " + fps + " fps, 帧耗时: " + delta + "ms");
if (delta > 16.67f) {
Log.w("FrameMonitor", "检测到 Jank! 耗时: " + delta + "ms");
}
}
lastFrameTime = frameTimeNanos;
Choreographer.getInstance().postFrameCallback(this);
}
});
4.2 Jank 检测:不只是掉帧那么简单
Jank 和掉帧不是一回事。掉帧是帧率低于 60fps,Jank 是帧与帧之间的间隔突然变大。举个例子:连续 10 帧都是 30fps,这不叫 Jank,这叫稳定卡顿。但如果前一帧 16ms,下一帧突然 50ms,这就是 Jank。
Android 的 FrameMetrics 可以帮你检测 Jank。它记录了每一帧的各个阶段耗时:
- Input:输入事件处理时间
- Animation:动画计算时间
- Layout:布局测量时间
- Draw:绘制时间
- Sync:同步到 GPU 的时间
- Total:总耗时
五、Trace 分析与 Systrace 使用:找到真凶
5.1 Systrace 基础:抓取一次完整的 Trace
Systrace 是性能优化的「CT 扫描仪」。它能告诉你系统里每个线程在干什么,哪个函数耗时最长。我每次遇到疑难杂症,第一件事就是抓 Systrace。
// 抓取 10 秒的 Systrace,包含所有标签
python systrace.py -t 10 -o trace.html
// 抓取特定标签(车机常用)
python systrace.py -t 10 -o trace.html \
-a com.android.systemui \
sched freq idle gfx view input
5.2 如何读懂 Systrace:三个关键区域
打开生成的 trace.html,你可能会被密密麻麻的色块吓到。别慌,我教你三个重点看:
- SurfaceFlinger 线程:看它是不是在等 Buffer。如果经常出现「waitForBuffer」或「acquireBuffer」,说明 GPU 来不及渲染。
- 应用主线程:看有没有长时间运行的函数。比如 inflate 布局、decodeBitmap 这些,都是卡顿元凶。
- CPU 频率曲线:看频率是不是在频繁跳变。如果频率曲线像心电图一样,说明调频策略有问题。
5.3 Perfetto:Systrace 的下一代替代
Android 12 之后,Google 主推 Perfetto 替代 Systrace。Perfetto 的优点是:
- 支持更长时间的抓取(几分钟甚至几小时)
- 支持 SQL 查询分析(可以写 SQL 查特定事件)
- UI 界面更现代,交互更好
// 使用 Perfetto 抓取 Trace
adb shell perfetto -o /data/misc/perfetto-traces/trace.perfetto-trace \
-t 10s \
sched freq idle gfx view
// 拉取到本地
adb pull /data/misc/perfetto-traces/trace.perfetto-trace .
SELECT * FROM slice WHERE dur > 10000000 AND name GLOB '*binder*'
六、实战案例:一次车载导航卡顿的排查过程
最后分享一个我实际遇到的案例。某款车机在导航时,地图滑动有明显的卡顿感。帧率监控显示只有 35-40fps,而且每隔几秒就出现一次 Jank。
排查过程:
- 抓 Systrace:发现 SurfaceFlinger 频繁出现「waitForBuffer」,等待时间长达 30ms。
- 看 GPU 负载:GPU 频率一直在最高频,但渲染完成时间仍然很长。
- 检查 Overdraw:开启 GPU Overdraw 调试,发现地图图层 Overdraw 达到 3.5x。
- 定位根因:地图应用使用了多层半透明遮罩,导致 GPU 需要绘制大量被遮挡的像素。
- 优化方案:去掉不必要的遮罩层,将地图背景改为不透明,Overdraw 降到 1.2x,帧率恢复到 55-60fps。
嗯,这个案例让我深刻体会到:性能优化不是堆硬件,而是找到那个「多余的图层」。很多时候,问题就藏在你觉得「应该没问题」的地方。
公众号:蓝海资料掘金营,微信deep3321