第24章 性能分析工具:Systrace与Perfetto、SimplePerf与FlameGraph、Heap Profiling

性能分析,说白了就是回答三个问题:慢在哪?为什么慢?怎么改?

我在Android Runtime领域摸爬滚打这些年,见过太多“我觉得这里慢”的猜测式优化。嗯,结果往往是把不慢的地方改快了,真正慢的地方纹丝不动。所以这一章,我想跟你聊聊我平时最常用的三套工具链——从宏观的Trace到微观的CPU采样,再到内存的深度剖析。

核心思路:先看全局(Systrace/Perfetto),再定点爆破(SimplePerf),最后深挖内存(Heap Profiling)。别跳步骤,跳了容易白忙活。

24.1 宏观Trace:Systrace与Perfetto

先说说Systrace。这工具我用了快十年,它本质上是把Android系统里各个进程的“时间线”画出来。你想想看,一个App卡顿,可能是主线程在等Binder调用,也可能是SurfaceFlinger在等GPU。Systrace能让你一眼看出谁在等谁。

我个人习惯是这么用的:

# 抓取10秒的trace,重点关注应用包名和sched、freq
python systrace.py -t 10 -o mytrace.html sched freq gfx view am wm

抓出来的HTML文件,用Chrome打开,按W放大、S缩小、M标记区间。我一般先看SurfaceFlinger的vsync信号,再看App主线程的doFrame标记。如果doFrame间隔超过16ms,那肯定掉帧了。

我的小技巧:在代码里手动加Trace标记,能帮你快速定位具体函数。

Trace.beginSection("mySlowFunction");
// 你的代码
Trace.endSection();

这样在Systrace里就能看到自定义的区间,比猜快多了。

但Systrace有个硬伤——它只能抓几秒的数据,而且对多核CPU的调度信息展示不够细。所以Google后来推出了Perfetto,算是Systrace的全面升级版。

Perfetto能抓更长时间,支持SQL查询,还能看内存、I/O、网络等更多维度。我最近的项目里,基本都切到Perfetto了。抓取命令也很简单:

# 使用Perfetto命令行抓取
adb shell perfetto -o /data/misc/perfetto-traces/trace.perfetto-trace -t 10s sched freq gfx view

抓完拉到 ui.perfetto.dev 打开,界面比Systrace清爽很多。我最喜欢的是它的SQL分析模式——你可以直接写SQL查某个进程的CPU占用率,比如:

SELECT process.name, SUM(dur) / 1e9 AS cpu_seconds
FROM sched_slice
JOIN process USING(upid)
WHERE process.name LIKE 'com.example%'
GROUP BY process.name

嗯,这比肉眼盯着时间线找快多了。

避坑指南:我曾经在抓取Perfetto trace时忘了加--no-compress,结果抓出来的文件在UI上解析失败。建议生产环境抓取时加上-c /data/local/tmp/config.pbtx用配置文件,避免命令行参数太长出错。

24.2 微观采样:SimplePerf与FlameGraph

Trace工具能告诉你“哪个时间段慢”,但说不清“哪行代码最耗CPU”。这时候就需要SimplePerf出场了。

SimplePerf是Android自带的CPU性能采样工具,原理就是每隔一段时间打断CPU,记录当前正在执行的函数调用栈。采样频率越高,结果越精确,但开销也越大。

我一般这么用:

# 对指定进程采样10秒,频率4000Hz
adb shell simpleperf record -p $(pidof com.example.app) -f 4000 --duration 10 -o perf.data

采样完生成报告:

adb shell simpleperf report -i perf.data --sort comm,dso,symbol -o report.txt

但光看文本报告不够直观。我习惯把数据转成FlameGraph(火焰图)。火焰图这东西,说白了就是把函数调用栈按占比画成矩形,越宽的矩形表示这个函数占CPU越多。

生成火焰图的步骤:

# 1. 把perf.data转成脚本可读的格式
adb shell simpleperf report -i perf.data --full-callgraph -o report.folded

# 2. 用FlameGraph脚本生成SVG
./stackcollapse-perf.pl report.folded > out.folded
./flamegraph.pl out.folded > flame.svg

打开生成的SVG,你会看到类似这样的结构:

SimplePerf 火焰图示例 android::Looper::pollOnce com.example::MainActivity.onCreate android::ViewRootImpl::doTraversal initViews loadDataFromNetwork performMeasure performLayout JSON解析 (占CPU 45%) ↑ 越往上越具体,越宽越耗CPU 系统调用 应用代码 热点函数

你看这个图,最宽的是JSON解析那块,占了45%的CPU。我当时在一个项目里就遇到过类似情况——一个列表页卡顿,火焰图一看,全是Gson在parse。后来换成手动解析,流畅度直接翻倍。

我的经验:火焰图里如果看到__libc_lockfutex这类函数很宽,说明有严重的锁竞争。这时候别急着优化业务代码,先看看锁的粒度能不能拆细。

24.3 内存深潜:Heap Profiling

CPU问题搞定了,接下来是内存。Android的内存问题,说白了就两种:泄漏抖动

泄漏是对象该回收没回收,抖动是频繁创建临时对象导致GC频繁。Perfetto的Heap Profiling功能,能帮你精确找到问题所在。

启动Heap Profiling:

# 对指定进程进行堆采样,每分配512KB记录一次
adb shell perfetto -c - --txt -o /data/misc/perfetto-traces/heap.profiling \
  <

抓完的数据,在Perfetto UI里打开,选择“Heap Profile”标签页。你会看到类似这样的调用栈分配统计:

调用栈 分配次数 总大小 占比
BitmapFactory.decodeStream 1,234 48.2 MB 62%
Handler.post -> new Runnable 8,765 12.1 MB 16%
StringBuilder.toString 22,345 8.5 MB 11%
其他 8.2 MB 11%

你看这个表,Bitmap占了62%。我当时在一个图片浏览App里就遇到过——用户滑动时,每张图片都重新解码,导致内存暴涨。后来加了LRU缓存和复用Bitmap池,内存直接降了70%。

避坑指南:我曾经在调试一个内存泄漏时,Heap Profiling显示某个Activity实例一直没释放。但代码里明明在onDestroy里置空了所有引用。后来发现是内部类持有外部类引用——匿名Runnable里用了Activity的context。嗯,改成弱引用就解决了。

除了Perfetto,你也可以用Android Studio自带的Memory Profiler。我个人习惯是:

  • 先看Java Heap:如果总大小持续增长,大概率有泄漏
  • 再看Native Heap:如果Native内存涨得快,可能是Bitmap或OpenGL资源没释放
  • 最后看GC频率:如果GC log里出现大量ConcurrentMarkSweep,说明堆抖动严重

对于GC抖动,我有个百试百灵的方法——对象池。比如频繁创建的点、矩形这类小对象,用池子复用:

public class RectPool {
    private static final int POOL_SIZE = 50;
    private final Rect[] pool = new Rect[POOL_SIZE];
    private int index = 0;

    public Rect obtain() {
        if (index > 0) {
            return pool[--index];
        }
        return new Rect();
    }

    public void recycle(Rect rect) {
        if (index < POOL_SIZE) {
            pool[index++] = rect;
        }
    }
}

这样GC压力能小很多。你想想看,一个列表页每秒创建几百个Rect,如果没有池子,GC就得频繁回收,卡顿就这么来的。

总结一下我的分析流程:

  1. 遇到卡顿,先用Perfetto抓Trace,看是CPU、GPU还是I/O瓶颈
  2. 如果是CPU问题,用SimplePerf采样,生成FlameGraph找热点函数
  3. 如果是内存问题,用Heap Profiling看分配栈,找泄漏或抖动点
  4. 修复后,再抓一次Trace验证效果

这套流程我用了好几年,基本没失手过。

最后说一句,工具只是辅助,真正重要的是你对系统运行机制的理解。比如你知道Binder线程池满了会怎样,知道GC的STW(Stop-The-World)会卡主线程,那你看到Trace里的异常区间,自然就能猜到问题在哪。嗯,这就是经验的价值。


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

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