一、车载性能优化全景:CPU/GPU 的协同战场

车载系统的性能优化,说白了就是一场与资源瓶颈的博弈。我做了这么多年车载 Android 定制,最深的体会是:车机不像手机,手机卡了你可以重启,车机卡了——嗯,用户可能正在倒车入库,或者导航到一半突然掉帧。所以 CPU 调频、GPU 渲染、帧率监控这些,不是锦上添花,是底线。

这一章我会把 CPU 和 GPU 的优化手段拆开讲,但你要记住:它们不是孤立的。CPU 负责调度、逻辑、任务分发,GPU 负责渲染、合成、显示。两者配合不好,就会出现「CPU 在等 GPU 画完,GPU 在等 CPU 喂数据」的死锁局面。

核心目标:在保证流畅度的前提下,把 CPU/GPU 的功耗压到最低。车机的散热和电池都有限,你不能像手机那样「性能全开跑分」。
车载性能优化核心框架 CPU 优化域 • 调频策略:cpufreq / schedutil • 调度优化:SchedTune / cpuset • 核心隔离:RT 线程绑定大核 GPU 优化域 • 渲染管线:Overdraw 优化 • 合成策略:HWC / GPU Composer • 纹理压缩:ASTC / ETC2 监控与诊断层 • 帧率监控:Choreographer / SurfaceFlinger • Jank 检测:FrameMetrics / JankTracker • Trace 分析:Systrace / Perfetto

二、CPU 调频与调度优化:让每一 Hz 都花在刀刃上

2.1 cpufreq 调频策略:别让 CPU 乱跳频

车载 Android 的 CPU 调频,我建议你直接抛弃 ondemand 和 conservative。这两个策略在车机上表现很差——ondemand 响应太激进,一有负载就冲高频,功耗飙升;conservative 又太保守,界面滑动都卡。

我个人习惯用 schedutil。它是调度器驱动的调频策略,能根据任务的负载情况动态调整频率。说白了,就是让调度器告诉你「这个任务需要多快」,然后调频器跟着走。

经验之谈:我在某款高通 SA8155 平台上调试时,发现 schedutil 的 up_rate_limit_ms 默认是 10ms,对于车机这种交互场景偏大。我把它改成了 4ms,帧率抖动明显减少。
// 查看当前调频策略
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 保证基础服务不饿死
避坑指南:我曾经把语音交互的 boost 设到 50,结果导致 CPU 一直处于高频,功耗高了 15%。后来发现 30 就足够了。调参这事,得一点点试。

三、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 合成了,帧率直接掉了一半。

解决方案:减少图层数量。把状态栏、导航栏、主界面合并到同一个 Surface 中,或者使用 SurfaceView 代替 TextureView。

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:总耗时
我的经验:如果 Total 超过 16ms,先看 Draw 和 Sync。Draw 长说明 UI 绘制复杂,Sync 长说明 GPU 负载高。这两个是车机上最常见的瓶颈。

五、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,你可能会被密密麻麻的色块吓到。别慌,我教你三个重点看:

  1. SurfaceFlinger 线程:看它是不是在等 Buffer。如果经常出现「waitForBuffer」或「acquireBuffer」,说明 GPU 来不及渲染。
  2. 应用主线程:看有没有长时间运行的函数。比如 inflate 布局、decodeBitmap 这些,都是卡顿元凶。
  3. CPU 频率曲线:看频率是不是在频繁跳变。如果频率曲线像心电图一样,说明调频策略有问题。
注意:Systrace 的开销不小。在车机上抓 Trace 时,建议只抓 5-10 秒,不要长时间抓取。我曾经抓了 30 秒,结果车机直接过热关机了。

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 .
小技巧:Perfetto 的 SQL 分析非常强大。比如你想查所有耗时超过 10ms 的 Binder 调用,可以写:
SELECT * FROM slice WHERE dur > 10000000 AND name GLOB '*binder*'

六、实战案例:一次车载导航卡顿的排查过程

最后分享一个我实际遇到的案例。某款车机在导航时,地图滑动有明显的卡顿感。帧率监控显示只有 35-40fps,而且每隔几秒就出现一次 Jank。

排查过程:

  1. 抓 Systrace:发现 SurfaceFlinger 频繁出现「waitForBuffer」,等待时间长达 30ms。
  2. 看 GPU 负载:GPU 频率一直在最高频,但渲染完成时间仍然很长。
  3. 检查 Overdraw:开启 GPU Overdraw 调试,发现地图图层 Overdraw 达到 3.5x。
  4. 定位根因:地图应用使用了多层半透明遮罩,导致 GPU 需要绘制大量被遮挡的像素。
  5. 优化方案:去掉不必要的遮罩层,将地图背景改为不透明,Overdraw 降到 1.2x,帧率恢复到 55-60fps。

嗯,这个案例让我深刻体会到:性能优化不是堆硬件,而是找到那个「多余的图层」。很多时候,问题就藏在你觉得「应该没问题」的地方。


公众号:蓝海资料掘金营,微信deep3321