5. Android <卡顿五>优化RecyclerView 卡顿:一套基于 Matrix 监控、Systrace/Perfetto 标准化排查流程(卡顿实战)

前面已经讲了卡顿的主要原因和主要监控手段,工具,今天开始实战案例!

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。
  • 过程还原

    1. Matrix 在运行时插桩(Hack),监控所有方法的执行耗时。
    2. onBindViewHolder 被调用时,它内部的 simulateHeavyWork 方法被执行,产生了巨大的耗时。
    3. Matrix 捕获到这个调用栈,并计算出方法 ID 79 是导致这次卡顿的根因(Root Cause)
    4. 通过符号表文件(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% (显示为红色的高柱),并且长时间处于 RunnableRunning 状态,这说明有代码在疯狂占用 CPU。
    • 帧生命周期 :放大看一帧的生命线(Choreographer#doFrame),你会看到 performTraversals (measure/layout/draw) 的时间异常的长。
  • 优势与局限

    • 优势 :可以看到是否是渲染问题 (Measure/Layout 耗时)、IO 问题 (线程被阻塞)还是 CPU 计算问题(本例中的循环)。可以看到系统整体状态(CPU 频率、磁盘活动等)。
    • 局限 :正如你所说,它通常不能直接定位到代码行 。它告诉你 RecyclerViewonBind 很慢,但不会告诉你到底是 onBind 里的哪一行代码慢。你需要结合 Method Tracing(CPU Profiler)Matrix 的日志来精确定位。

4.Pefetto分析上面卡顿案例

4.1 Pefetto采集

  1. 在开发者选项中启用"系统跟踪"

  2. 打开"系统跟踪"应用

  3. 配置跟踪选项:

    • 跟踪类别:选择"Graphics"、"View"、"Input"、"System"等
    • 缓冲区大小:建议至少 32MB
    • 跟踪时长:设置为"手动停止"
  4. 开始录制,重现 RecyclerView 卡顿场景

  5. 停止录制并分享/保存生成的 .perfetto-trace 文件

4.2 Pefetto操作

打开网页: ui.perfetto.dev/

导入数据

  1. 打开 Perfetto UI
  2. 点击左上角的"Open trace file"按钮
  3. 选择从设备中采集到的 .perfetto-trace 文件
  4. 等待文件加载和解析完成

4.3 Pefetto分析卡顿原因

4.3.1 具体的操作图解

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

看主线程;mainThread

看火焰图: Flame Graph 记得把配置打开才行!火焰图里面耗时,但是不是对应的代码里面的,而是很多系统的~

BV, 更具体的就是,

performTraversals(measure/layout/draw)、inflatedraw 等耗时

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)进行录制。

  • 分析过程

    1. 找到卡顿时间段 :首先通过 Frames 轨道或 Alerts 面板找到发生 jank(掉帧)的具体位置。

    2. 放大主线程 :放大时间线,查看主线程(例如 com.evenbus.myapplication 的主线程)在这段卡顿时间内在做什么。

    3. 识别任务 :你通常会看到一個長長的 Choreographer#doFrame 块,或者一个 ListView|Recyclerview bind 之类的任务块,其执行时间远超 16ms。

    4. 查看子任务 :展开这个长任务块,Perfetto 会显示系统记录的更细粒度的活动。你可能会看到 performTraversals(measure/layout/draw)、inflatedraw 等耗时。

    5. 结合代码推理

      • 如果你在 onBindViewHolder 的位置看到了一个异常长的、没有明确名称的空白块或 atrace 块,而你知道你自己的 simulateHeavyWork() 方法就在这里被调用,那么你就可以断定就是这个方法导致的。
      • 同时,你可以观察 CPU 使用率。如果在这个时间段,主线程的 CPU 使用率拉满(100%),而其他线程都很空闲,那几乎可以肯定是在执行像 simulateHeavyWork 这样的重型计算,而不是在等待I/O或锁。

4.3.3 Perfetto 提供了强大的可视化界面来分析性能问题。针对这个 RecyclerView 卡顿案例,我们可以进行以下分析:

a. 识别卡顿帧

  1. 在"Timeline"面板中,找到应用进程(com.evenbus.myapplication
  2. 展开主线程(通常是"主线程"或"ui thread")
  3. 寻找长条的红色或黄色片段,这些表示耗时较长的操作
  4. 使用"Frame Timeline"面板查看具体的掉帧情况

b. 分析主线程活动

  1. 放大卡顿区域的时间线

  2. 查看主线程上的活动:

    • 寻找名为PerformanceAdapter.onBindViewHolder的方法调用
    • 特别关注其中耗时特别长的部分,这很可能是simulateHeavyWork()方法

p3-juejin.byteimg.com/tos-cn-i-k3...

c. 查看方法调用栈

  1. 如果启用了Java方法采样,可以查看火焰图(Flame Graph)
  2. 在火焰图中,可以清晰地看到simulateHeavyWork方法占据了大量的CPU时间
  3. 点击方法调用栈可以查看完整的调用链

d. 分析渲染性能

  1. 查看"SurfaceFlinger"轨道,了解帧的提交和显示情况
  2. 检查"GPU Completion"时间,确认是否是GPU渲染瓶颈
  3. 查看"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文件,也在上面的项目里面:

相关推荐
Mintopia4 分钟前
🌌 Next.js 服务端组件(Server Components)与客户端组件(`"use client"`)
前端·javascript·next.js
Mintopia7 分钟前
⚔️ WebAI 推理效率优化:边缘计算 vs 云端部署的技术博弈
前端·javascript·aigc
爱学大树锯1 小时前
【Ruoyi 解密 - 09. 前端探秘2】------ 接口路径及联调实战指南
前端
老华带你飞1 小时前
校园二手书交易|基于SprinBoot+vue的校园二手书交易管理系统(源码+数据库+文档)
java·前端·数据库·vue.js·小程序·毕设·校园二手书交易管理系统
萌程序.1 小时前
创建Vue项目
前端·javascript·vue.js
VT.馒头2 小时前
【力扣】2704. 相等还是不相等
前端·javascript·算法·leetcode·udp
linweidong2 小时前
Vue前端国际化完全教程(企业内部实践教程)
前端·javascript·vue.js·多语言·vue-i18n·动态翻译·vue面经
lukeLiouu2 小时前
augment不能白嫖了?试试claude code + GLM4.5,十分钟搞定起飞🚀
前端
点正2 小时前
使用 Volta 管理 Node 版本和 chsrc 换源:提升开发效率的完整指南
前端
泉城老铁2 小时前
VUE2实现加载Unity3d
前端·vue.js·unity3d