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...

相关推荐
Hy行者勇哥15 小时前
前端代码结构详解
前端
CYRUS_STUDIO15 小时前
一步步带你移植 FART 到 Android 10,实现自动化脱壳
android·java·逆向
练习时长一年15 小时前
Spring代理的特点
java·前端·spring
CYRUS_STUDIO15 小时前
FART 主动调用组件深度解析:破解 ART 下函数抽取壳的终极武器
android·java·逆向
水星记_15 小时前
时间轴组件开发:实现灵活的时间范围选择
前端·vue
MisterZhang66615 小时前
Java使用apache.commons.math3的DBSCAN实现自动聚类
java·人工智能·机器学习·自然语言处理·nlp·聚类
2501_9301247016 小时前
Linux之Shell编程(三)流程控制
linux·前端·chrome
Swift社区16 小时前
Java 常见异常系列:ClassNotFoundException 类找不到
java·开发语言
潘小安16 小时前
『译』React useEffect:早知道这些调试技巧就好了
前端·react.js·面试
@大迁世界16 小时前
告别 React 中丑陋的导入路径,借助 Vite 的魔法
前端·javascript·react.js·前端框架·ecmascript