Android 性能优化利器 —— 火焰图(FlameGraph)
介绍
火焰图(Flamegraph)是一种用于可视化软件程序性能剖析数据的强大工具,主要用于分析程序的 CPU 使用情况。它由性能工程专家 Brendan Gregg 开发,现已成为系统性能分析的标准工具之一。
Note
提到火焰图(FlameGraph)时,很难不提它的发明者 Brendan Gregg。Brendan Gregg 是性能优化领域的权威专家,曾在 Netflix、Sun Microsystems 等公司从事系统性能分析工作。他不仅开发了火焰图,还贡献了大量性能分析工具和方法论,如 DTrace(动态跟踪工具)的使用和优化。
Brendan Gregg 也是一位乐于分享技术的大牛,这是他的博客地址:https://www.brendangregg.com/
核心原理
火焰图的核心原理是通过将时间轴与堆栈跟踪信息巧妙结合,以直观的可视化方式展示程序在执行过程中的活动情况。其核心流程大致可以分成如下三个步骤:
- 通过采样获取程序运行时的调用栈信息;
- 统计各函数在调用栈中出现的频率;
- 将统计结果转化为层级化的可视化图表。
下图是一个火焰图可视化图表示例:
火焰图是对 CPU 进行周期性的采样。通过设置一定的采样频率,让工具频繁地访问 CPU 上执行的指令的地址,并记录当前调用栈。每次访问 CPU 运行情况的瞬间,可以理解为采样点。在一定周期内采集到的点就可以汇集成“面”,这个“面”的精细程度由采样频率决定。 采样的频率越高,收集到的数据就越详细,“面”的也就越精细,反之亦然。 最后,再把这个“面”进行数据处理,比如符号解析、栈帧整理等操作,就得到了火焰图。 当用户调用脚本后,将这些原始数据通过可视化的界面(比如网页、SVG)展示出来。
Danger
初学者经常会把火焰图的长度看成是函数的执行时间,这是错误的!
或者换个思路,其实很好理解。把 CPU 想象成池塘,把每次采集到的点想象成做了标记的鱼。 当我们想对一个池塘的鱼的总数进行估算时,会先捞出一部分鱼做标记,丢回池塘,过一段时间,再捞起等同数量的鱼,计算被标记鱼的占比, 从而大概估算整个池塘鱼的数量。
火焰图也一样,只不过其是对 CPU 运行状态进行“标记”。假设 CPU 在 T0 时间下,采集到的采样点 P0 正在执行方法为 M0。当下一次采集时,CPU 在 T1 时间下,采集到的采样点 P1 正在执行方法为 M1。对比 P0 和 P1 函数和栈帧情况:
- 若 P0 和 P1 完全相等,则 M0 和 M1 完全相等。则可以认为从 T0 时间到 T1 时间段,CPU 都在执行 M0 方法,或者 CPU 都在执行 M1 方法;
- 如 P0 和 P1 不相等,则 M0 和 M1 不相等。则可以认为从 T0 时间到 T1 时间段,CPU 至少执行了 2 个及其以上的方法,其中包含 M0 和 M1。因为可能在 T0 到 T1 过程中,有某些方法瞬间执行,但是采样频率设置过长,导致没有采集到。
最后,将 P0、P1、P2、P3...... 采样点进行整理和归类,就可以得到多条连续的线段,将线段高度拉高,就成了火焰图上看到的矩形(可以理解成火焰图的 X 轴)。再根据函数调用栈的情况,将这些矩形进行上下排列,得到了形似火焰的阶梯形状(可以理解成火焰图的 Y 轴)。
为了方便理解,我们可以举个实际的代码的例子。以下是一段伪代码1:
main() {
// some business logic
func3() {
// some business logic
func7();
}
// some business logic
func4();
// some business logic
func1() {
// some business logic
func5();
}
// some business logic
func2() {
// some business logic
func6() {
// some business logic
func8(); // cpu intensive work here
}
}
性能分析启动时以每秒 X 次的频率进行采样。每次采集样本时,系统会保存当前的调用栈信息。下图展示了在排序与聚合处理前,未经整理的原始采样数据视图。
这是每个函数被采样到的样本数:
func3()->func7()
: 3 samplesfunc4()
: 1 samplefunc1()->func5()
: 2 samplesfunc2()->func8()
: 4 samplesfunc2()->func6()
: 1 sample
样本随后会在应用程序的根节点(或主方法)之后的基底层按字母顺序进行排序。
注意,X轴不表示时间维度。火焰图不会保留特定调用栈的采样时间信息,仅反映在性能分析期间各调用栈出现的频率统计。 系统将各调用栈层级中的相同函数块进行拼接整合,最终形成聚合的火焰图可视化视图。
本示例中,除 func4()
外,其他函数在调用栈基底层均未实际占用资源。实际资源消耗来自func5()
、func6()
、func7()
和
func8()
函数,其中 func8()
是性能优化的重点候选对象。
火焰图最典型的应用场景是分析 CPU 使用率,但同时它也支持多种分析模式:内存分配分析(Allocation Profiling)可用于监测堆内存使用状况,而实时耗时分析(Wall-clock Profiling)则能有效诊断系统延迟问题。
Note
Q:是不是采样频率越高越好?
不是。采样的频率越高,收集到的数据就越详细,但也会对程序性能产生更大的影响。通常会选择一个折衷的采样频率,既能获得足够的数据分析程序行为,又不会过分干扰程序运行。
平台工具
火焰图可以通过多种工具和方式生成,主要取决于目标平台、编程语言和性能分析需求。以下是常见的生成方式分类:
分类 | 工具/方法 | 适用场景 | 典型命令/工具链 |
---|---|---|---|
Linux系统 | perf + FlameGraph | Linux系统级分析 | perf record → perf script → FlameGraph脚本 |
eBPF + BCC | 现代Linux内核分析 | profile /offcputime 工具 → 导出数据生成 |
|
SystemTap | 深度内核分析 | SystemTap脚本采集 → 数据转换 | |
Java/JVM | Async-Profiler | 低开销CPU/内存/锁分析 | 直接生成火焰图 |
JFR (Java Flight Recorder) | 官方性能记录工具 | 记录文件 → 转换为火焰图格式 | |
VisualVM | 内置采样分析 | 导出调用栈 → 生成火焰图 | |
Android平台 | Simpleperf + Inferno | Android Native代码分析 | simpleperf record → inferno 生成 |
Android Studio Profiler | 官方IDE集成工具 | CPU分析 → 导出调用栈 → 转换 | |
Perfetto | 系统级跟踪 | 跟踪数据 → trace_processor 转换 |
|
其他语言 | Python (py-spy/scalene) | Python应用分析 | py-spy record → 生成SVG |
Node.js (0x/clinic.js) | JavaScript性能分析 | 0x 或--cpu-prof 生成 |
|
Go (pprof) | Go应用分析 | go tool pprof --web 生成 |
|
Rust (flamegraph) | Rust应用分析 | cargo flamegraph 生成 |
|
.NET (dotnet-trace) | .NET应用分析 | dotnet-trace collect → PerfView分析 |
|
通用/跨平台工具 | Speedscope | 在线可视化 | 支持多种输入格式直接可视化 |
Hotspot | perf数据可视化 | 加载perf.data自动生成 | |
VTune | Intel处理器深度分析 | 内置火焰图生成功能 |
配色方案
默认的火焰图的配色方案以暖色调为主,反应的是 CPU 的繁忙程度(热程度)。除此之外,还有其他的的配色方案,比如用于 Java 混合模式的黄绿红橙配色方案:

上图中颜色的含义如下:
生成方式
以 Android 为例,生成火焰图的方式如下几种:
- Simpleperf:Android 官方优化版 perf,支持 ARM 架构,性能开销低。无需修改代码,支持符号解析(需带调试信息的 .so)。
- Android Studio Profiler:图形化界面,集成在 Android Studio 中,易用性强。支持采样分析(Sampling 和插桩分析(Instrumentation)。
- Perfetto:高性能、低开销,支持 CPU、GPU、内存等多维度分析。可导出为 perfetto-trace 并用 UI 或脚本生成火焰图。
下表是这些方法的对比情况:
方法 | 适用层 | Root 需求 | 精度 | 易用性 | 适用场景 |
---|---|---|---|---|---|
Simpleperf | Native/JNI | 部分需 Root | ⭐⭐⭐⭐ | ⭐⭐⭐ | NDK 开发、系统优化 |
Android Studio Profiler | Java/Kotlin | 无需 | ⭐⭐ | ⭐⭐⭐⭐ | 应用层性能调试 |
Systrace + Flame Graph | 系统/线程 | 无需 | ⭐⭐ | ⭐⭐ | 卡顿分析、锁竞争 |
Perfetto | 系统/应用 | 无需 | ⭐⭐⭐ | ⭐⭐⭐ | 多维度系统级跟踪 |
eBPF | 内核/Native | 需 Root | ⭐⭐⭐⭐ | ⭐ | 底层性能分析 |
Async-Profiler | Java/Native | 部分需 Root | ⭐⭐⭐⭐ | ⭐⭐⭐ | 混合语言应用(如游戏、音视频) |
Simpleperf
Warning
若对 Simpleperf 的命令不熟悉,建议先阅读《Android 性能优化利器 —— Simpleperf》。
以 Settings 为例,先使用 Simpleperf 生成火焰图的核心是获取到 perf.data
文件,然后再把此文件通过 report.html
转换成火焰图。
Step 1:获取 perf.data
文件。
# 方式 1:使用内置的 Simpleperf
$ adb shell ps -A | grep settings
system 7711 590 5405764 116988 do_epoll_wait 0 S com.android.setting
$ adb shell "simpleperf record -p 7711 --duration 10 -o /data/local/tmp/perf.data"
simpleperf I cmd_record.cpp:729] Recorded for 10.0152 seconds. Start post processing.
simpleperf I cmd_record.cpp:809] Samples recorded: 0. Samples lost: 0.
$ adb pull /data/local/tmp/perf.data D:/Downloads
# 方式 2:使用 Android NDK 下的脚本
python app_profiler.py -p com.android.settings
17:10:52,908 [INFO] (app_profiler.py:206) prepare profiling
...
17:11:09,340 [INFO] (app_profiler.py:213) profiling is finished.
方式 2 其实和方式 1 一样,在执行方式 2 的时候会显示执行的是如下的命令:
adb shell "simpleperf record -o /data/local/tmp/perf.data -e task-clock:u -f 1000 -g --duration 10 --log info --app com.android.settings"
Step 2:将 perf.data
转换成火焰图,推荐使用 Android NDK 下的 report.py
脚本完成:
python report.py -i <your_perf_data_path>
在参数 -i
后传递 perf.data
在 PC 的路径即可。
Danger
注意:生成的 Html 可能会加载失败,这是无法访问 JavaScript 源,解决办法参考 生成火焰图无法加载。
Android Studio Profiler
不同的 Android Studio 的 Profiler 的入口和界面可能不同。在 Android Studio Ladybug(2024.2.1 Patch 2)的版本上, Profiler 的入口在:View > Tool Windows > Profiler。打开后,可以看到如下的界面:
- 提供了多种性能的分析方式,因为火焰图是对 CPU 执行的函数的采集,所以可以选择:Find CPU Hotspots;
- 选择录制的方式,可以选择 Tracing 或者 Sampling;
- 确认后,点击 Start profiler task。
等待录制完成,就可以获取到如下的火焰图:
Perfetto
在 Perfetto 的 Record new trace
选项卡中,在 Stack Sampling
开启 Callstack sampling
,并设置采集的频率:
然后抓取到的 Perfetto trace 中可以看到函数的调用情况:
Note
通过 Perfetto 抓取到更像是函数的堆栈图,相比 Simpleperf 生成的图,总感觉这种方式缺少了点东西。
差分火焰图
差分火焰图(也称为红蓝差分图)是一种通过对比两个时间点的火焰图来分析性能变化的可视化图。它可以帮助开发者了解程序在不同时间段内的性能特征, 并识别出在两个时间点之间发生的性能变化。差分火焰图的重点在于分析函数的样本被采集到的数量以及两个时间点之间样本的变化情况。
在 Android 中,生成差分火焰图相对生成火焰图稍微复杂一些,需要使用 Simpleperf 和 FlameGraph 工具共同完成。大致的步骤可以总结如下:
- 通过 Android NDK 提供的脚本抓取数据,得到
perf.data
。因为差分火焰图是对两份数据进行对比,通常需要在测试机和对比机上分别抓取; - 使用
stackcollapse.py
将perf.data
转成 FlameGraph 能够识别的 folded 格式; - 最后使用
difffolded.pl
脚本做差分,得到差分火焰图。
以生成通讯录(Contacts)应用在冷启动过程中的差分火焰图为例,具体步骤如下:
Step 1:在测试机和对比机上分别抓取两份 perf.data
数据。
python app_profiler.py -p com.google.android.contacts -o perf_first_cold_start.data
python app_profiler.py -p com.google.android.contacts -o perf_second_cold_start.data
Step 2:下载 FlameGraph 源码,然后配置 Perl 环境:
配置 Perl 环境。网上有很多教程,自行搜索。检查配置是否成功:
# 检查 Perl 语言环境可用
$ perl -v
# 输出 Perl 的信息
This is perl 5, version 36, subversion 0 (v5.36.0) built for MSWin32-x64-multi-thread
Step 3:将 perf.data
数据文件转 folded 文件:
python stackcollapse.py -i perf_first_cold_start.data > perf_first_cold_start.folded
python stackcollapse.py -i perf_second_cold_start.data > perf_second_cold_start.folded
若转换后的 folded 文件大小为 0 字节,说明数据抓取或者转换错误。返回之前的步骤,重新抓取。
Step 4:生成差分火焰图
perl difffolded.pl perf_first_cold_start.folded perf_second_cold_start.folded | perl flamegraph.pl --negate > D:\Temp\diff.svg
Step 5:得到 diff.svg
的差分火焰图,使用浏览器访问。

上图中颜色的含义如下:
- 蓝色:表示在后一时间点相比于前一时间点,函数调用次数减少了。这可能意味着在优化或者修复了某些问题后,相关函数的调用次数减少了;
- 红色:表示在后一时间点相比于前一时间点,函数调用次数增加了。这可能意味着在代码中引入了新的性能问题或者程序流程发生了变化,导致某些函数被更频繁地调用。
- 白色或灰色:表示未变化的部分。