22、性能优化与调试:Systrace 分析、CPU/GPU 负载、内存泄漏排查

性能优化,说白了就是跟硬件抢时间。在 MediaServer 这个场景里,你面对的是音视频数据流,每一帧的延迟都可能让用户感觉到卡顿或音画不同步。我做了这么多年多媒体开发,最深的体会就是:没有工具链支撑的优化,都是瞎猜

这一章,我们就来聊聊三个核心工具:Systrace、CPU/GPU 负载分析、内存泄漏排查。嗯,这三板斧用好了,大部分性能问题都能定位到根因。

22.1 Systrace:系统级性能追踪

Systrace 是 Android 平台最强大的性能分析工具之一。它能把系统各个进程的 CPU 调度、Binder 调用、SurfaceFlinger 合成、甚至内核事件都串成一条时间线。我个人习惯在 MediaServer 出现卡顿或丢帧时,第一个就抓 Systrace。

22.1.1 抓取 Systrace 的正确姿势

别用命令行一个个敲参数了。我建议直接用 Perfetto 工具,它是 Systrace 的下一代替代品。不过为了兼容老项目,我们还是先讲传统方式:

# 抓取 10 秒,包含所有常见标签
python systrace.py -t 10 -o trace.html sched freq idle am wm gfx view \
    binder_driver binder_lock input res dalvik hal camera audio video

这里有个坑:标签不要全开。全开会导致数据量爆炸,反而看不清关键路径。我一般只开 schedgfxbinder_driveraudiovideo 这几个。

我的习惯:抓取前先清空 logcat 缓存,避免干扰。抓取时让手机处于正常温度,别边充电边抓——充电会触发 CPU 降频,数据不准。

22.1.2 读懂 Systrace 的关键指标

打开生成的 HTML 文件,你会看到一堆彩色条块。别慌,重点看这几个:

指标 含义 正常范围
CPU 频率 每个核心的实时频率 不低于最低频率的 80%
SurfaceFlinger 合成 每帧的合成耗时 < 16ms(60fps)
Binder 调用 跨进程通信耗时 < 5ms
BufferQueue 状态 生产者/消费者队列深度 不超过 3

我曾经遇到过一个诡异的问题:播放 4K 视频时,每隔几秒就掉一帧。抓了 Systrace 一看,发现 audio HAL 线程每次处理完音频数据后,都会触发一次 binder transaction,而这个 transaction 居然阻塞了 30ms。原因?音频 HAL 里有个 mutex 没释放干净。嗯,这种问题不看 trace 根本想不到。

22.2 CPU/GPU 负载分析

CPU 和 GPU 的负载,直接决定了 MediaServer 能不能跑满帧率。说白了,CPU 负责解码、逻辑处理,GPU 负责渲染、合成。两者必须平衡。

22.2.1 使用 top 和 dumpsys 监控 CPU

别只盯着 top 看总 CPU 占用率。我建议用 dumpsys cpuinfo 看每个线程的负载:

adb shell dumpsys cpuinfo | grep mediaserver

# 输出示例
+ mediaserver (pid 1234):
  - MediaCodec_loop (tid 1235): 12% user, 3% kernel
  - OMXCallback (tid 1236): 2% user, 1% kernel
  - AudioTrack (tid 1237): 5% user, 2% kernel

这里有个经验值:如果 MediaCodec 线程的 CPU 占用超过 30%,说明解码器可能跑在软解模式。检查一下 MediaCodecList 是否选到了硬件解码器。

注意:CPU 占用率不是越低越好。如果 CPU 占用率很低但依然掉帧,说明瓶颈可能在 GPU 或内存带宽上。别被单一指标骗了。

22.2.2 GPU 负载怎么看?

Android 没有直接暴露 GPU 占用率的 API。但我们可以用 dumpsys gfxinfo 看渲染管线:

adb shell dumpsys gfxinfo mediaserver

# 重点关注
Pipeline: RenderThread
  - Draw: 2.3ms
  - Prepare: 1.1ms
  - Process: 0.8ms
  - Execute: 3.5ms
  - Total: 7.7ms

如果 Execute 阶段超过 10ms,说明 GPU 负载过高。这时候可以尝试降低渲染分辨率,或者减少 SurfaceView 的刷新率。

我记得有一次,客户反馈说播放 1080p 视频时画面撕裂。我一看 GPU 的 Execute 时间飙到了 18ms,但 CPU 负载才 20%。问题出在 SurfaceFlinger 的合成策略上——它用了 GPU composition,但硬件不支持。改成 HWC composition 后,Execute 直接降到 4ms。

22.3 内存泄漏排查

内存泄漏在 MediaServer 里特别隐蔽。因为音视频数据流是持续的,泄漏一点可能几个小时才爆。我见过最夸张的一次,一个 MediaCodec 实例泄漏了 200MB,播放 4 小时后直接 OOM。

22.3.1 使用 Memory Profiler 定位泄漏

Android Studio 的 Memory Profiler 是首选。但要注意:MediaServer 是 native 进程,Java heap 的泄漏只是冰山一角。你需要同时监控 native heap。

// 在代码中手动触发 GC 并 dump heap
Debug.dumpHprofData("/data/local/tmp/mediaserver.hprof");

// 然后拉取到本地分析
adb pull /data/local/tmp/mediaserver.hprof .

用 MAT 或 Android Studio 打开 hprof 文件,重点搜索这些类:

  • MediaCodec 实例数量——正常应该只有 1-2 个
  • Surface 对象——有没有被 Activity 持有
  • ByteBuffer 数组——有没有被回调引用

22.3.2 常见泄漏场景与修复

我总结了几种 MediaServer 里最常见的泄漏模式:

场景 原因 修复方式
MediaCodec 未释放 异常退出时没调 release() finally 块中确保释放
Surface 回调泄漏 匿名内部类持有外部引用 使用静态内部类 + WeakReference
ByteBuffer 未回收 DirectBuffer 没调 cleaner() 使用 allocateDirect 并手动回收
Handler 消息堆积 Looper 队列中消息未处理 退出时调用 removeCallbacksAndMessages

避坑指南:我曾经在项目里遇到一个 MediaCodec.Callback 泄漏。原因是我们在 onOutputBufferAvailable 里创建了一个匿名 Runnable 发到主线程,但主线程的 Handler 在 Activity 销毁后还在运行。这个 Runnable 持有了 MediaCodec 的引用,导致整个解码器无法释放。修复很简单:在 onStop 里调用 handler.removeCallbacksAndMessages(null)

22.3.3 Native 内存泄漏排查

Java heap 查完了,如果内存还在涨,那八成是 native 泄漏。用 malloc debug 工具:

# 开启 malloc debug
adb shell setprop libc.debug.malloc 1
# 重启 mediaserver
adb shell killall mediaserver
# 抓取 native heap dump
adb shell am dumpheap -n  /data/local/tmp/native_heap.dump

然后用 simpleperfaddr2line 解析堆栈。嗯,这个过程比较痛苦,但定位到问题后成就感很强。

22.4 知识体系总览

下面这张图总结了本章的核心逻辑。你可以把它当作排查性能问题的路线图:

MediaServer 性能优化与调试知识体系 Systrace 分析 CPU/GPU 负载 内存泄漏排查 抓取技巧 关键指标 Binder 分析 CPU 监控 GPU 渲染 负载平衡 Java Heap Native Heap 常见场景 目标:流畅播放 + 低延迟 + 无泄漏 三个维度相互关联:CPU 负载高可能导致 Systrace 显示调度延迟 内存泄漏又会间接导致 GPU 渲染变慢

你看,这三个维度其实是相互关联的。CPU 负载高可能导致 Systrace 显示调度延迟,内存泄漏又会间接导致 GPU 渲染变慢。所以排查时不要孤立地看一个指标,要结合起来分析。

我的工作流:先抓 Systrace 看整体时间线,发现异常区域后,用 dumpsys 查 CPU/GPU 负载,如果内存有异常增长,再 dump heap 分析。三步走完,90% 的问题都能定位。

好了,这一章的内容就到这里。性能优化没有银弹,但有了这些工具和方法,至少你不会再盲目地改代码了。记住:先测量,再优化


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