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
这里有个坑:标签不要全开。全开会导致数据量爆炸,反而看不清关键路径。我一般只开 sched、gfx、binder_driver、audio、video 这几个。
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 是否选到了硬件解码器。
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
然后用 simpleperf 或 addr2line 解析堆栈。嗯,这个过程比较痛苦,但定位到问题后成就感很强。
22.4 知识体系总览
下面这张图总结了本章的核心逻辑。你可以把它当作排查性能问题的路线图:
你看,这三个维度其实是相互关联的。CPU 负载高可能导致 Systrace 显示调度延迟,内存泄漏又会间接导致 GPU 渲染变慢。所以排查时不要孤立地看一个指标,要结合起来分析。
好了,这一章的内容就到这里。性能优化没有银弹,但有了这些工具和方法,至少你不会再盲目地改代码了。记住:先测量,再优化。
公众号:蓝海资料掘金营,微信deep3321