前面讲了卡顿的原理,原因,监控手段和分析手段,各种工具
Android <卡顿一> 深入理解Android 卡顿Choreographer:从VSYNC到掉帧(卡顿原理)
Android <卡顿二> 突破性性能监控方案Matrix---揭开微信亿级用户背后的流畅秘密 (卡顿监控工具集成)
Android <卡顿三> 卡顿性能分析工具 SystemTrace 精准定位 Android 性能瓶颈 (工具使用)
Android <卡顿四> 卡顿性能第二代工具 Perfetto 精准定位 Android 性能瓶颈 (工具使用)
卡顿:不仅仅是只有主线程卡顿有问题,还有过渡绘制导致,在Matrix 和Systrace/Perfetto中的表现都不一样! 所以要把案例单独列举出来。
Matrix在FPS的时候,并不是很严格,流程也会报出来! 后面说原理的时候会讲到

1.过渡绘制案例
typescript
package com.evenbus.myapplication.trace;
import android.os.Bundle;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import com.evenbus.myapplication.R;
import java.util.ArrayList;
import java.util.List;
/**
* 卡顿追踪测试Activity - 演示频繁invalidate()导致的性能问题
* 该Activity通过BadAnimationView展示错误的动画实现方式导致的卡顿现象
*/
public class TraceInvalideActivity extends AppCompatActivity implements BadAnimationView.OnAnimationUpdateListener {
// UI组件引用
private BadAnimationView badAnimationView; // 展示错误动画的自定义View
private TextView tvPerformance; // 性能状态显示TextView
private TextView tvInvalidateCount; // invalidate调用次数显示TextView
private TextView tvFPS; // 帧率显示TextView
private ListView listView; // 用于测试卡顿的列表视图
/**
* Activity创建生命周期回调
* @param savedInstanceState 保存的实例状态
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置布局文件
setContentView(R.layout.activity_trace_invalide_layout);
setTitle("invalide过渡绘制导致的卡顿");
// 初始化视图组件
initViews();
// 设置按钮点击监听器
setupClickListeners();
// 设置列表视图数据
setupListView();
}
/**
* 初始化所有视图组件
*/
private void initViews() {
// 获取自定义动画View引用
badAnimationView = findViewById(R.id.bad_animation_view);
// 获取性能显示TextView引用
tvPerformance = findViewById(R.id.tv_performance);
tvInvalidateCount = findViewById(R.id.tv_invalidate_count);
tvFPS = findViewById(R.id.tv_fps);
// 获取测试用列表视图引用
listView = findViewById(R.id.list_view);
// 设置动画更新监听器,用于接收性能数据回调
badAnimationView.setUpdateListener(this);
}
/**
* 设置按钮点击事件监听器
*/
private void setupClickListeners() {
// 启动错误动画按钮
findViewById(R.id.btn_start_bad_animation).setOnClickListener(v -> startBadAnimation());
// 启动正确动画按钮(预留功能)
findViewById(R.id.btn_start_good_animation).setOnClickListener(v -> startGoodAnimation());
// 停止动画按钮
findViewById(R.id.btn_stop_animation).setOnClickListener(v -> stopAnimation());
// 清空性能日志按钮
findViewById(R.id.btn_clear_log).setOnClickListener(v -> clearPerformanceLog());
}
/**
* 设置列表视图数据和适配器
* 该列表用于在动画运行时测试滚动卡顿情况
*/
private void setupListView() {
// 创建测试数据
List<String> items = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
items.add("列表项 " + i + " - 尝试滚动我感受卡顿");
}
// 创建并设置数组适配器
ArrayAdapter<String> adapter = new ArrayAdapter<>(
this,
android.R.layout.simple_list_item_1, // 使用系统简单列表项布局
items);
listView.setAdapter(adapter);
}
/**
* 启动错误动画演示
* 该方法会启动BadAnimationView中的错误动画实现
*/
private void startBadAnimation() {
// 先停止现有动画
stopAnimation();
// 更新性能状态显示
updatePerformanceStatus("启动错误动画 - Handler频繁invalidate");
// 启动错误动画
badAnimationView.startBadAnimation();
}
/**
* 启动正确动画(预留方法)
* 该方法用于展示正确的动画实现方式
*/
private void startGoodAnimation() {
// 先停止现有动画
stopAnimation();
// 更新性能状态显示
updatePerformanceStatus("启动正确动画 - 使用ValueAnimator");
// 预留:这里可以添加ValueAnimator的正确实现
// 用于对比错误动画和正确动画的性能差异
}
/**
* 停止所有动画
*/
private void stopAnimation() {
// 调用自定义View的停止动画方法
badAnimationView.stopAnimation();
// 更新性能状态显示
updatePerformanceStatus("动画已停止");
}
/**
* 清空性能监控日志
*/
private void clearPerformanceLog() {
// 重置invalidate调用次数显示
tvInvalidateCount.setText("invalidate()调用次数: 0");
// 重置FPS显示
tvFPS.setText("当前FPS: 0");
}
/**
* 更新性能状态显示
* @param status 要显示的性能状态文本
*/
private void updatePerformanceStatus(String status) {
tvPerformance.setText("性能状态: " + status);
}
/**
* invalidate调用次数回调方法
* 实现OnAnimationUpdateListener接口
* @param count 当前invalidate调用次数
* @param fps 当前帧率
*/
@Override
public void onInvalidateCalled(int count, int fps) {
// 在主线程更新UI
runOnUiThread(() -> {
// 更新invalidate调用次数显示
tvInvalidateCount.setText("invalidate()调用次数: " + count);
// 更新FPS显示
tvFPS.setText("当前FPS: " + fps);
// 根据FPS更新性能状态
updatePerformanceStatusBasedOnFPS(fps);
});
}
/**
* FPS更新回调方法
* 实现OnAnimationUpdateListener接口
* @param fps 新的FPS值
*/
@Override
public void onFPSUpdated(int fps) {
// 在主线程更新UI
runOnUiThread(() -> {
// 更新FPS显示
tvFPS.setText("当前FPS: " + fps);
// 根据FPS更新性能状态
updatePerformanceStatusBasedOnFPS(fps);
});
}
/**
* 根据FPS值更新性能状态显示和颜色
* @param fps 当前帧率值
*/
private void updatePerformanceStatusBasedOnFPS(int fps) {
// 根据FPS范围设置不同的颜色和状态文本
if (fps < 10) {
// 严重卡顿:红色显示
tvPerformance.setTextColor(0xFFFF0000); // ARGB: 红色
updatePerformanceStatus("严重卡顿 - FPS: " + fps);
} else if (fps < 30) {
// 明显卡顿:橙色显示
tvPerformance.setTextColor(0xFFFFA500); // ARGB: 橙色
updatePerformanceStatus("明显卡顿 - FPS: " + fps);
} else if (fps < 50) {
// 轻微卡顿:黄色显示
tvPerformance.setTextColor(0xFFFFFF00); // ARGB: 黄色
updatePerformanceStatus("轻微卡顿 - FPS: " + fps);
} else {
// 流畅:绿色显示
tvPerformance.setTextColor(0xFF00FF00); // ARGB: 绿色
updatePerformanceStatus("流畅 - FPS: " + fps);
}
}
/**
* Activity销毁生命周期回调
* 进行资源清理工作
*/
@Override
protected void onDestroy() {
super.onDestroy();
// 确保动画停止,避免内存泄漏
badAnimationView.stopAnimation();
}
}
java
package com.evenbus.myapplication.trace;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.view.View;
import java.util.Random;
/**
* 错误动画演示View - 展示频繁invalidate()导致的卡顿问题
* 这个类 intentionally 实现了错误的动画模式,用于性能问题演示
* 警告:不要在生产代码中使用这种实现方式
*/
public class BadAnimationView extends View {
// 绘制工具
private Paint paint;
// 随机数生成器
private Random random;
// UI线程Handler
private Handler handler;
// 动画状态标志
private boolean isAnimating = false;
// 动画参数 - 控制圆球的运动
private int circleX = 0; // 圆球X坐标
private int circleY = 0; // 圆球Y坐标
private int circleRadius = 30; // 圆球半径
private int dx = 5; // X方向速度
private int dy = 3; // Y方向速度
// 性能统计相关变量
private int invalidateCount = 0; // invalidate()调用次数
private long lastFrameTime = 0; // 上一帧时间戳
private int frameCount = 0; // 帧计数器
private int currentFPS = 0; // 当前FPS值
// 性能回调接口
private OnAnimationUpdateListener updateListener;
/**
* 构造方法1 - 代码创建View时调用
* @param context 上下文对象
*/
public BadAnimationView(Context context) {
super(context);
init();
}
/**
* 构造方法2 - XML布局中声明View时调用
* @param context 上下文对象
* @param attrs 属性集合
*/
public BadAnimationView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
/**
* 初始化方法 - 设置画笔和工具
* 问题:在初始化时创建Handler,可能引起内存泄漏
*/
private void init() {
// 初始化画笔
paint = new Paint();
paint.setColor(Color.RED); // 设置红色
paint.setStyle(Paint.Style.FILL); // 填充样式
paint.setAntiAlias(true); // 抗锯齿
// 初始化随机数生成器
random = new Random();
// 问题:直接使用主线程Handler,可能导致内存泄漏
// 建议:使用View的post方法或者弱引用Handler
handler = new Handler(Looper.getMainLooper());
}
/**
* View尺寸变化回调
* @param w 新宽度
* @param h 新高度
* @param oldw 旧宽度
* @param oldh 旧高度
*/
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
// 初始化位置在视图中心
circleX = w / 2;
circleY = h / 2;
}
/**
* 绘制方法 - 核心绘制逻辑
* 问题:在onDraw中调用invalidate()形成无限循环
* @param canvas 画布对象
*/
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 计算帧率
calculateFPS();
// 绘制淡灰色背景
canvas.drawColor(Color.parseColor("#f8f8f8"));
// 绘制弹跳的圆球
canvas.drawCircle(circleX, circleY, circleRadius, paint);
// 绘制运动轨迹
drawTrail(canvas);
// 绘制性能信息
drawPerformanceInfo(canvas);
// 严重问题:在onDraw中无条件调用invalidate()
// 这会形成无限重绘循环,每一帧结束后立即请求下一帧
if (isAnimating) {
invalidateCount++; // 增加调用计数
// 通知监听器
if (updateListener != null) {
updateListener.onInvalidateCalled(invalidateCount, currentFPS);
}
// 错误做法:立即请求重绘,造成疯狂的重绘循环
// 这会导致UI线程被完全占用,无法处理其他任务
invalidate();
}
}
/**
* 计算帧率(FPS)
* 基于时间间隔计算每秒帧数
*/
private void calculateFPS() {
long currentTime = System.currentTimeMillis();
// 初始化上一帧时间
if (lastFrameTime == 0) {
lastFrameTime = currentTime;
}
frameCount++; // 增加帧计数
long elapsedTime = currentTime - lastFrameTime;
// 每秒更新一次FPS显示
if (elapsedTime >= 1000) {
currentFPS = (int) (frameCount * 1000 / elapsedTime);
frameCount = 0; // 重置帧计数
lastFrameTime = currentTime; // 更新时间戳
// 通知监听器FPS更新
if (updateListener != null) {
updateListener.onFPSUpdated(currentFPS);
}
}
}
/**
* 绘制运动轨迹
* 在圆球后面绘制渐变的轨迹效果
* @param canvas 画布对象
*/
private void drawTrail(Canvas canvas) {
// 创建轨迹画笔(半透明红色)
Paint trailPaint = new Paint();
trailPaint.setColor(Color.argb(64, 255, 0, 0)); // 25%不透明度
trailPaint.setStyle(Paint.Style.FILL);
// 绘制5个逐渐变小的轨迹圆
for (int i = 0; i < 5; i++) {
// 计算轨迹位置(反向偏移)
int trailX = circleX - dx * i * 2;
int trailY = circleY - dy * i * 2;
int trailRadius = circleRadius - i * 3; // 逐渐变小
// 只绘制半径大于0的圆
if (trailRadius > 0) {
canvas.drawCircle(trailX, trailY, trailRadius, trailPaint);
}
}
}
/**
* 绘制性能信息
* 在画布左上角显示FPS和invalidate次数
* @param canvas 画布对象
*/
private void drawPerformanceInfo(Canvas canvas) {
// 创建文本画笔
Paint textPaint = new Paint();
textPaint.setColor(Color.BLACK); // 黑色文字
textPaint.setTextSize(24); // 24sp字号
textPaint.setAntiAlias(true); // 抗锯齿
// 绘制性能信息文本
String info = "FPS: " + currentFPS + " | Invalidates: " + invalidateCount;
canvas.drawText(info, 10, 30, textPaint);
}
/**
* 启动错误动画 - 使用Handler频繁调用invalidate
* 这是导致卡顿的核心方法
*/
public void startBadAnimation() {
if (isAnimating) return; // 防止重复启动
// 初始化动画状态
isAnimating = true;
invalidateCount = 0;
frameCount = 0;
lastFrameTime = 0;
// 重置圆球位置到中心
circleX = getWidth() / 2;
circleY = getHeight() / 2;
// 随机生成速度方向
dx = random.nextInt(7) + 3; // 3-9的速度
dy = random.nextInt(5) + 2; // 2-6的速度
if (random.nextBoolean()) dx = -dx; // 随机X方向
if (random.nextBoolean()) dy = -dy; // 随机Y方向
// 严重问题:使用Handler频繁触发重绘
// 设置0延迟,试图达到最大帧率,但实际上会造成严重卡顿
handler.postDelayed(new Runnable() {
@Override
public void run() {
if (!isAnimating) return; // 检查动画状态
// 问题:在UI线程执行物理计算
// 应该在工作线程计算,只在主线程更新UI
updatePhysics();
// 问题:频繁调用invalidate()
invalidateCount++;
if (updateListener != null) {
updateListener.onInvalidateCalled(invalidateCount, currentFPS);
}
invalidate(); // 请求重绘
// 问题:使用0延迟循环,占用UI线程
// 这会导致消息队列堆积,无法处理其他消息
handler.postDelayed(this, 0); // 0延迟,尽可能快地执行
}
}, 0); // 立即执行
}
/**
* 更新物理计算 - 处理圆球的运动和碰撞
* 问题:在UI线程执行物理计算
*/
private void updatePhysics() {
// 更新位置
circleX += dx;
circleY += dy;
// 左右边界碰撞检测
if (circleX - circleRadius <= 0 || circleX + circleRadius >= getWidth()) {
dx = -dx; // 反转X方向速度
// 防止超出边界
circleX = Math.max(circleRadius, Math.min(circleX, getWidth() - circleRadius));
}
// 上下边界碰撞检测
if (circleY - circleRadius <= 0 || circleY + circleRadius >= getHeight()) {
dy = -dy; // 反转Y方向速度
// 防止超出边界
circleY = Math.max(circleRadius, Math.min(circleY, getHeight() - circleRadius));
}
}
/**
* 停止动画
* 清除Handler回调,停止动画循环
*/
public void stopAnimation() {
isAnimating = false;
// 移除所有待处理的消息
handler.removeCallbacksAndMessages(null);
}
/**
* 设置动画更新监听器
* @param listener 监听器实例
*/
public void setUpdateListener(OnAnimationUpdateListener listener) {
this.updateListener = listener;
}
/**
* 动画更新监听器接口
* 用于向外传递性能数据
*/
public interface OnAnimationUpdateListener {
/**
* invalidate调用回调
* @param count 调用次数
* @param fps 当前帧率
*/
void onInvalidateCalled(int count, int fps);
/**
* FPS更新回调
* @param fps 新的FPS值
*/
void onFPSUpdated(int fps);
}
/**
* 获取invalidate调用次数
* @return 调用次数
*/
public int getInvalidateCount() {
return invalidateCount;
}
/**
* 获取当前FPS
* @return 当前帧率
*/
public int getCurrentFPS() {
return currentFPS;
}
}
极端频繁的invalidate()调用
scss
// 在BadAnimationView:
public void startExtremeBadAnimation() {
// 3个Handler同时疯狂调用invalidate()
handler1.postDelayed(this, 0); // 0延迟!
handler2.postDelayed(this, 1); // 1ms延迟!
handler3.postDelayed(this, 2); // 2ms延迟!
// 并且在onDraw中也调用invalidate()
if (isAnimating) {
invalidate(); // 形成循环!
}
}
2.用Matrix分析上面案例
2.1 log日志

2.2 json报告
json
{
"machine": "BEST",
"cpu_app": 0,
"mem": 11901149184,
"mem_free": 4968624,
"scene": "com.evenbus.myapplication.trace.TraceInvalideActivity",
"dropLevel": **{
"DROPPED_BEST": 599,
"DROPPED_NORMAL": 0,
"DROPPED_MIDDLE": 0,
"DROPPED_HIGH": 0,
"DROPPED_FROZEN": 0
},
"dropSum": **{
"DROPPED_BEST": 509,
"DROPPED_NORMAL": 0,
"DROPPED_MIDDLE": 0,
"DROPPED_HIGH": 0,
"DROPPED_FROZEN": 0
},
"fps": 38.54767608642578,
"UNKNOWN_DELAY_DURATION": 2255068,
"INPUT_HANDLING_DURATION": 4124,
"ANIMATION_DURATION": 14604,
"LAYOUT_MEASURE_DURATION": 137228,
"DRAW_DURATION": 2702776,
"SYNC_DURATION": 215923,
"COMMAND_ISSUE_DURATION": 13355978,
"SWAP_BUFFERS_DURATION": 1017575,
"TOTAL_DURATION": 24828857,
"GPU_DURATION": 3026714,
"DROP_COUNT": 1,
"REFRESH_RATE": 60,
"tag": "Trace_FPS",
"process": "com.evenbus.myapplication",
"time": 1756522780377
}
指标 | 当前 | 目标 | 改善幅度 |
---|---|---|---|
FPS | 38.5 | 55+ | +43% |
丢帧数 | 509 | <50 | -90% |
COMMAND_ISSUE | 13.36ms | <8ms | -40% |
2.3 这个性能报告清楚地显示了频繁invalidate调用导致的严重性能问题,需要立即进行优化。
为什么这么说:
关键证据分析
1. COMMAND_ISSUE_DURATION异常高
json
"COMMAND_ISSUE_DURATION":13355978 // 13.36ms,占总时间的53.8%
- 这是什么:向GPU提交绘制命令的耗时
- 正常值:应该小于5ms
- 问题指示 :13.36ms严重超标,说明有大量的绘制命令被提交
2. DRAW_DURATION相对正常
json
"DRAW_DURATION":2702776 // 2.70ms,正常范围
- 绘制操作本身耗时正常,说明不是绘制内容复杂导致的
- 问题不在
onDraw()
方法内部的计算
3. GPU_DURATION偏高
json
"GPU_DURATION":3026714 // 3.03ms,偏高一倍
- GPU处理时间比正常值(1-1.5ms)高一倍
- 说明GPU在频繁处理绘制请求
推导过程
证据链分析:
- COMMAND_ISSUE异常高 → 大量绘制命令
- DRAW正常 → 不是绘制内容复杂
- GPU处理时间长 → 频繁的绘制请求
- 综合判断 → 频繁的invalidate()调用
invalidate的独特特征
COMMAND_ISSUE独占鳌头
- 只有频繁的invalidate()会导致这个指标异常突出
- 其他问题都会在其他指标上有体现
Matrix,如果是tag[Trace_FPS], 说明是掉帧,不一定能扫描出慢方法,注意排除绘制,draw,invalidate
如果有慢方法,基本是这样, 指向handler,你大概率分析不出来! 所以这种情况就要看FPS


2.4 在BadAnimationView中重点检查:invalidate
-
onDraw中的invalidate()调用
scss// 这行代码是罪魁祸首! invalidate(); // 形成无限循环,每帧都在调用
-
Handler的频繁post
kotlinhandler.postDelayed(this, 0); // 0延迟,疯狂调用
-
复杂的绘制操作
arduinoprivate void drawTrail(Canvas canvas) { for (int i = 0; i < 5; i++) { // 5次绘制操作 canvas.drawCircle(...); // 每次都有开销 } }
3.用Systrace分析上面案例
看工具:核心指标, janky frames

看火焰图和Top down,具体的堆栈信息
4.用Pefetto分析上面案例
4.1 卡顿的原因

4.2 卡顿时间:看绘制的时间和实际花费的时间: Expected Timeline 和 Actual Timeline

4.3 主线程

额外看的cpu调度:

5. 总结
Matrix 监控、Systrace/Perfetto 分析过渡绘制的特点
Matrix的表现:
json
// 间接指标:
"DRAW_DURATION":3948203 // 绘制耗时异常高
"GPU_DURATION":2797737 // GPU处理耗时偏高
"COMMAND_ISSUE_DURATION":3368769 // 命令提交耗时高
// 但无法显示:
- 具体哪些视图过度绘制
- 重叠绘制的层级关系
- 实际屏幕上的Overdraw分布
Systrace/Perfetto的表现:
diff
// 直接可视化:
- 红色区域显示过度绘制严重的位置
- 不同颜色表示不同的Overdraw程度
- 能够看到视图层次结构
// 帧分析:
- 显示每帧的渲染时间 breakdown
- 可以看到哪些绘制操作最耗时
- 显示GPU的实际工作负载
trace文件和项目案例:
RecyclerView卡顿案例地址: github.com/pengcaihua1...