6. Android <卡顿六>Android渲染性能攻坚:基于Matrix+Systrace+Perfetto的过度绘制卡顿标准化排查(卡顿实战)

前面讲了卡顿的原理,原因,监控手段和分析手段,各种工具

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在频繁处理绘制请求

推导过程

证据链分析:

  1. COMMAND_ISSUE异常高 → 大量绘制命令
  2. DRAW正常 → 不是绘制内容复杂
  3. GPU处理时间长 → 频繁的绘制请求
  4. 综合判断 → 频繁的invalidate()调用

invalidate的独特特征

COMMAND_ISSUE独占鳌头

  • 只有频繁的invalidate()会导致这个指标异常突出
  • 其他问题都会在其他指标上有体现

Matrix,如果是tag[Trace_FPS], 说明是掉帧,不一定能扫描出慢方法,注意排除绘制,draw,invalidate

如果有慢方法,基本是这样, 指向handler,你大概率分析不出来! 所以这种情况就要看FPS

2.4 在BadAnimationView中重点检查:invalidate

  1. onDraw中的invalidate()调用

    scss 复制代码
    // 这行代码是罪魁祸首!
    invalidate(); // 形成无限循环,每帧都在调用
  2. Handler的频繁post

    kotlin 复制代码
    handler.postDelayed(this, 0); // 0延迟,疯狂调用
  3. 复杂的绘制操作

    arduino 复制代码
    private 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...

相关推荐
RaidenLiu7 分钟前
告别陷阱:精通Flutter Signals的生命周期、高级API与调试之道
前端·flutter·前端框架
非凡ghost7 分钟前
HWiNFO(专业系统信息检测工具)
前端·javascript·后端
非凡ghost9 分钟前
FireAlpaca(免费数字绘图软件)
前端·javascript·后端
ekkcole11 分钟前
java把word转pdf使用jar包maven依赖
java·pdf·word
非凡ghost16 分钟前
Sucrose Wallpaper Engine(动态壁纸管理工具)
前端·javascript·后端
拉不动的猪17 分钟前
为什么不建议项目里用延时器作为规定时间内的业务操作
前端·javascript·vue.js
Java小王子呀19 分钟前
Java实现Excel转PDF
java·pdf·excel
该用户已不存在24 分钟前
Gemini CLI 扩展,把Nano Banana 搬到终端
前端·后端·ai编程
地方地方26 分钟前
前端踩坑记:解决图片与 Div 换行间隙的隐藏元凶
前端·javascript
小猫由里香32 分钟前
小程序打开文件(文件流、地址链接)封装
前端