前面已经讲了卡顿的主要原因和主要监控手段,工具,今天开始实战案例!
1.场景RecyclerView,滚动的时候卡顿
java
public class PerformanceAdapter extends RecyclerView.Adapter<PerformanceAdapter.ViewHolder> {
private final List<String> items;
private final BindTimeListener bindTimeListener;
private long totalBindTime = 0;
private int bindCount = 0;
public interface BindTimeListener {
void onBindTime(long bindTime);
}
public PerformanceAdapter(List<String> items, BindTimeListener listener) {
this.items = items;
this.bindTimeListener = listener;
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public final TextView titleText;
public final TextView subtitleText;
public ViewHolder(View view) {
super(view);
titleText = view.findViewById(R.id.titleText);
subtitleText = view.findViewById(R.id.subtitleText);
}
}
@Override
public ViewHolder onCreateViewHolder( ViewGroup parent, int viewType) {
// 模拟耗时操作
try {
Thread.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
View view = LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_layout, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder( ViewHolder holder, int position) {
long startTime = System.currentTimeMillis();
// 模拟复杂数据绑定
holder.titleText.setText("Item " + items.get(position));
holder.subtitleText.setText("This is a long subtitle for item " + items.get(position) +
" that will cause text measurement to take more time");
// 故意制造性能问题 - 在主线程进行耗时操作
if (position % 5 == 0) {
simulateHeavyWork();
}
if (position % 3 == 0) {
holder.titleText.setTextColor(Color.RED);
} else {
holder.titleText.setTextColor(Color.BLACK);
}
// 记录绑定耗时
long bindTime = System.currentTimeMillis() - startTime;
totalBindTime += bindTime;
bindCount++;
// 回调报告绑定时间
if (bindTimeListener != null) {
bindTimeListener.onBindTime(bindTime);
}
}
@Override
public int getItemCount() {
return items.size();
}
public double getAverageBindTime() {
return bindCount > 0 ? (double) totalBindTime / bindCount : 0;
}
// 模拟耗时操作
private void simulateHeavyWork() {
// 1. 主线程耗时计算
Random random = new Random();
long sum = 0;
for (int i = 0; i <= 10000000; i++) {
sum += random.nextLong();
}
}
}
onBindViewHolder
中模拟重型计算 (simulateHeavyWork()
)
- 问题 :这是最致命的错误。
simulateHeavyWork
方法在position % 5 == 0
时执行一个高达 1000万次 的循环。这完全阻塞了主线程,导致 UI 无法响应
2.Matrix分析上面的案例
生成log
scala
public class TestPluginListener extends DefaultPluginListener {
public static final String TAG = "Matrix.TestPluginListener";
public TestPluginListener(Context context) {
super(context);
}
@Override
public void onReportIssue(Issue issue) {
super.onReportIssue(issue);
MatrixLog.e(TAG, issue.toString());
//add your code to process data
}
}
json存放
将设备上的文件拉取到当前电脑目录
adb pull /sdcard/Android/data/com.example.myapp/files/matrix/trace/canary/your_trace_file.json ./
2.1 产生的log日志和json报告:


2.2 把Matrix产生的报告json格式化
swift
{
"machine": "BEST", // 设备性能等级:顶级设备
"cpu_app": 0, // 应用CPU使用率:0%(可能是I/O阻塞)
"mem": 11901149184, // 总内存:11.9GB(充足)
"mem_free": 5092676, // 可用内存:4.9MB(内存压力较大)
"detail": "NORMAL", // 问题类型:普通慢方法
"cost": 1341, // 总耗时:1341ms(严重超标!)
"scene": "com.evenbus.myapplication.trace.TraceRecycleActivity", // 发生页面
"stack": "1,13413,1,1334\n2,13865,1,1334\n...", // 方法调用堆栈
"stackKey": "79|", // 关键方法ID:79
"tag": "Trace_EvilMethod", // 问题标签:慢方法
"process": "com.evenbus.myapplication", // 进程名
"time": 1756392370014 // 时间戳
}
2.3 根据堆栈数据,主要耗时分布:
"tag": "Trace_EvilMethod", // 问题标签:慢方法
层级 | 方法ID | 耗时(ms) | 问题严重性 |
---|---|---|---|
1-5 | 13413,13865,9039,9126,9153 | 1334 | ⚠️ 严重瓶颈 |
6-14 | 9162,8817,12812... | 315-363 | ⚠️ 次级瓶颈 |
关键方法 | 79 | 315 | 🔍 根因方法 |
耗时方法:就是79
2.4 通过映射关系,MethodMapping.txt文件,查找方法
编译的时候,会生成文件,在bulid/outputs/mapping/debug/methodMapping.txt

79对应的就是代码里面的simulateHeavyWork()方法
2.5 Matrix分析案例总结:
Matrix 的 TraceCanary
组件在这里展现了其强大的方法粒度的监控能力。
-
报告解读:
"cost": 1341
:一个操作耗时 1.3 秒,远超 16.6ms 的帧时限,用户感知绝对是"应用卡死了"。"detail": "NORMAL"
和"tag": "Trace_EvilMethod"
:明确告诉你这是一个普通的慢方法问题。"stackKey": "79"
:这是最关键的信息,它直接锁定了问题方法的唯一 ID。
-
过程还原:
- Matrix 在运行时插桩(Hack),监控所有方法的执行耗时。
- 当
onBindViewHolder
被调用时,它内部的simulateHeavyWork
方法被执行,产生了巨大的耗时。 - Matrix 捕获到这个调用栈,并计算出方法 ID
79
是导致这次卡顿的根因(Root Cause) 。 - 通过符号表文件(mapping.txt) ,可以将数字 ID
79
反混淆,还原回人类可读的方法名simulateHeavyWork
。
-
优势 :无需猜测 ,直接告诉你是哪个方法 、耗了多少时 、在哪个页面发生的。这是基于代码级别的精准打击。
3.Systrace分析前面的RecyclerView卡顿案例
3.1 采集trace
打开Profiler,点击 capture system Activites/System Trace
3.2 看工具:核心指标, janky frames

看具体的耗时,定位不出哪个方法,只能看到Rv,draw,layout, measure的哪个地方出现的问题! 需要推理分析
但是通过多次的案例,可以掌握一定的规律,比如sleep, Rv, draw,layout, measure! 然后结合Method Tracing或日志Logcat精确定位

3.3 总结: Systrace 提供了另一个维度的视角:系统资源调度。
-
报告解读:
- Alerts:Systrace 会智能地给出警告(Janky frames),告诉你哪些帧超时了。
- CPU 调度 :你可以看到在卡顿期间,主线程(通常名为
主线程
或包名
)的 CPU 占用率几乎是 100% (显示为红色的高柱),并且长时间处于Runnable
或Running
状态,这说明有代码在疯狂占用 CPU。 - 帧生命周期 :放大看一帧的生命线(
Choreographer#doFrame
),你会看到performTraversals
(measure/layout/draw) 的时间异常的长。
-
优势与局限:
- 优势 :可以看到是否是渲染问题 (Measure/Layout 耗时)、IO 问题 (线程被阻塞)还是 CPU 计算问题(本例中的循环)。可以看到系统整体状态(CPU 频率、磁盘活动等)。
- 局限 :正如你所说,它通常不能直接定位到代码行 。它告诉你
RecyclerView
的onBind
很慢,但不会告诉你到底是onBind
里的哪一行代码慢。你需要结合 Method Tracing(CPU Profiler) 或 Matrix 的日志来精确定位。
4.Pefetto分析上面卡顿案例
4.1 Pefetto采集
-
在开发者选项中启用"系统跟踪"
-
打开"系统跟踪"应用
-
配置跟踪选项:
- 跟踪类别:选择"Graphics"、"View"、"Input"、"System"等
- 缓冲区大小:建议至少 32MB
- 跟踪时长:设置为"手动停止"
-
开始录制,重现 RecyclerView 卡顿场景
-
停止录制并分享/保存生成的
.perfetto-trace
文件
4.2 Pefetto操作
打开网页: ui.perfetto.dev/
导入数据
- 打开 Perfetto UI
- 点击左上角的"Open trace file"按钮
- 选择从设备中采集到的
.perfetto-trace
文件 - 等待文件加载和解析完成
4.3 Pefetto分析卡顿原因
4.3.1 具体的操作图解
看绘制的时间和实际花费的时间: Expected Timeline 和 Actual Timeline
看主线程;mainThread

看火焰图: Flame Graph 记得把配置打开才行!火焰图里面耗时,但是不是对应的代码里面的,而是很多系统的~
BV, 更具体的就是,
performTraversals
(measure/layout/draw)、inflate
、draw
等耗时

yaml
adb shell perfetto \
-c - --txt \
-o /data/misc/perfetto-traces/trace.perfetto-trace \
<<EOF
duration_ms: 10000
buffers: {
size_kb: 63488
fill_policy: DISCARD
}
data_sources: {
config {
name: "linux.ftrace"
ftrace_config {
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_wakeup"
ftrace_events: "power/suspend_resume"
ftrace_events: "ftrace/print"
# 这是关键!启用 atrace 并指定你的应用和Java类别
atrace_categories: "view"
atrace_categories: "webview"
atrace_categories: "input"
atrace_categories: "wm"
atrace_categories: "am"
atrace_categories: "dalvik" # 重要:Dalvik/ART 运行时事件
atrace_apps: "com.evenbus.myapplication" # 你的应用包名
}
}
}
data_sources: {
config {
name: "java_hprof"
target_buffer: 0
java_hprof_config {
process_cmdline: "com.evenbus.myapplication" # 你的应用包名
# 连续采样,适合分析卡顿
continuous_dump_config {
dump_phase: CONTINUOUS
dump_interval_ms: 5000 # 每5秒采样一次
}
}
}
}
EOF
# 拉取文件到电脑
adb pull /data/misc/perfetto-traces/trace.perfetto-trace .
和log日志一起看
额外可以看的: surfaceFlinger

4.3.2 Perfetto 如何定位卡顿方法
1. 通过「Java/Kotlin 方法采样(Java Heap Profiling)」
这是最接近直接定位方法的方式。
- 操作 :在录制 Perfetto trace 时,你需要手动开启对目标应用的 "Java堆栈采样" 或类似的详细分析选项。
- 结果 :如果开启成功,在 Perfetto UI 中分析主线程时,你可以看到一条详细的火焰图(Flame Graph) 。这个火焰图会显示在卡顿的时间段内,哪些方法占用了最高的 CPU 时间。你会看到一个巨大的方法调用堆栈,其最底层、最宽的部分就是最耗时的根因方法。
- 局限性 :这个方法采样是统计性的,并非完全精确。它通过定期获取调用栈来推测热点,有时可能无法捕获到非常短暂但致命的阻塞。而且默认的系统跟踪应用可能不会默认开启最详细的Java级别跟踪。
2. 通过「系统调用 + 代码逻辑推理」(最常用)
这是 Perfetto 的标准分析流程,即使没有详细的Java采样,也能极大概率锁定问题。
-
操作 :使用标准的 atrace 分类(如
gfx
,view
,sched
)进行录制。 -
分析过程:
-
找到卡顿时间段 :首先通过 Frames 轨道或 Alerts 面板找到发生 jank(掉帧)的具体位置。
-
放大主线程 :放大时间线,查看主线程(例如
com.evenbus.myapplication
的主线程)在这段卡顿时间内在做什么。 -
识别任务 :你通常会看到一個長長的
Choreographer#doFrame
块,或者一个ListView|Recyclerview bind
之类的任务块,其执行时间远超 16ms。 -
查看子任务 :展开这个长任务块,Perfetto 会显示系统记录的更细粒度的活动。你可能会看到
performTraversals
(measure/layout/draw)、inflate
、draw
等耗时。 -
结合代码推理:
- 如果你在
onBindViewHolder
的位置看到了一个异常长的、没有明确名称的空白块或atrace
块,而你知道你自己的simulateHeavyWork()
方法就在这里被调用,那么你就可以断定就是这个方法导致的。 - 同时,你可以观察 CPU 使用率。如果在这个时间段,主线程的 CPU 使用率拉满(100%),而其他线程都很空闲,那几乎可以肯定是在执行像
simulateHeavyWork
这样的重型计算,而不是在等待I/O或锁。
- 如果你在
-
4.3.3 Perfetto 提供了强大的可视化界面来分析性能问题。针对这个 RecyclerView 卡顿案例,我们可以进行以下分析:
a. 识别卡顿帧
- 在"Timeline"面板中,找到应用进程(
com.evenbus.myapplication
) - 展开主线程(通常是"主线程"或"ui thread")
- 寻找长条的红色或黄色片段,这些表示耗时较长的操作
- 使用"Frame Timeline"面板查看具体的掉帧情况
b. 分析主线程活动
-
放大卡顿区域的时间线
-
查看主线程上的活动:
- 寻找名为
PerformanceAdapter.onBindViewHolder
的方法调用 - 特别关注其中耗时特别长的部分,这很可能是
simulateHeavyWork()
方法
- 寻找名为
p3-juejin.byteimg.com/tos-cn-i-k3...
c. 查看方法调用栈
- 如果启用了Java方法采样,可以查看火焰图(Flame Graph)
- 在火焰图中,可以清晰地看到
simulateHeavyWork
方法占据了大量的CPU时间 - 点击方法调用栈可以查看完整的调用链
d. 分析渲染性能
- 查看"SurfaceFlinger"轨道,了解帧的提交和显示情况
- 检查"GPU Completion"时间,确认是否是GPU渲染瓶颈
- 查看"Expected Timeline"和"Actual Timeline"的差异,确认掉帧的具体情况
4.4 Perfetto分析案例总结
Perfetto 是 Android 官方推荐的新一代性能跟踪工具,可以看作是 Systrace 的进化版。
-
操作 :正如你所说,在 ui.perfetto.dev 网页中导入抓取的数据文件(
.perfetto-trace
或.xtrace
)即可。 -
优势:
- 更强的交互性:UI 更现代,缩放、选择、查看详情更流畅。
- 更长的录制时间:支持录制更长时间的 trace。
- 更多的数据源:可以集成更多系统级和应用级的事件。
- CPU Profiling 集成 :可以录制同时包含系统调用和 Java 方法采样的 trace,在一定程度上弥补了 Systrace 不能直接定位代码的短板。
在 Perfetto 中分析这个案例,你会看到和 Systrace 类似的图表,但体验更好。你可以在主线程的火焰图(Flame Graph)中可能看到更详细的方法调用堆栈(如果开启了相关配置),帮助你进一步分析。
Perfetto 是 Android 官方推荐的新一代性能跟踪工具,可以看作是 Systrace 的进化版。
-
操作 :正如你所说,在 ui.perfetto.dev 网页中导入抓取的数据文件(
.perfetto-trace
或.xtrace
)即可。 -
优势:
- 更强的交互性:UI 更现代,缩放、选择、查看详情更流畅。
- 更长的录制时间:支持录制更长时间的 trace。
- 更多的数据源:可以集成更多系统级和应用级的事件。
- CPU Profiling 集成 :可以录制同时包含系统调用和 Java 方法采样的 trace,在一定程度上弥补了 Systrace 不能直接定位代码的短板。
在 Perfetto 中分析这个案例,你会看到和 Systrace 类似的图表,但体验更好。你可以在主线程的火焰图(Flame Graph)中可能看到更详细的方法调用堆栈(如果开启了相关配置),帮助你进一步分析。
trace文件和项目案例:
RecyclerView卡顿案例地址: github.com/pengcaihua1...
录制的trace文件,也在上面的项目里面: