Files
mkdocs/docs/学习笔记/看应用卡顿问题.md
2026-02-11 00:40:25 +08:00

20 KiB
Raw Blame History

Perfetto看应用卡顿问题

一、卡顿问题概述

1. 什么是卡顿

卡顿是指应用在运行过程中出现画面不流畅、响应延迟、操作不跟手等现象。从技术角度来说,卡顿通常表现为:

  • 丢帧:60Hz设备上,1秒内显示的帧数少于60帧
  • 帧延迟:单帧渲染时间超过16.67ms(60Hz)或8.33ms(120Hz)
  • 响应延迟:用户操作到界面响应的时间过长

2. 卡顿的分类

2.1 按场景分类

  • 滑动卡顿:列表滑动、页面滚动时出现卡顿
  • 动画卡顿:页面跳转、转场动画、UI动画不流畅
  • 点击卡顿:点击操作后响应延迟
  • 启动卡顿:应用启动过程缓慢
  • 交互卡顿:界面交互操作不流畅

2.2 按原因分类

  • 主线程阻塞:主线程执行耗时操作
  • 渲染瓶颈:RenderThread或GPU性能不足
  • 系统资源竞争:CPU、内存、I/O资源不足
  • 锁竞争:多线程竞争导致阻塞
  • 内存压力:GC频繁、内存不足

3. 卡顿分析的目标

  • 定位卡顿时间点:确定卡顿发生的具体时间
  • 识别卡顿原因:找到导致卡顿的根本原因
  • 量化卡顿程度:测量卡顿的严重程度
  • 提供优化建议:给出针对性的优化方案

二、Perfetto录制卡顿场景

1. 录制前的准备

1.1 明确问题场景

在录制前需要明确:

  • 卡顿场景:什么操作导致卡顿
  • 复现步骤:如何复现卡顿问题
  • 卡顿表现:卡顿的具体表现(丢帧、延迟等)
  • 预期时长:卡顿持续的时间

1.2 环境准备

  • 设备状态:确保设备电量充足、温度正常
  • 后台应用:关闭不必要的后台应用
  • 网络状态:确保网络稳定(如需要)
  • 应用版本:使用待分析的应用版本

2. 卡顿分析配置文件

2.1 基础卡顿分析配置

jank_analysis_config.txt:

buffers: {
    size_kb: 63488
    fill_policy: DISCARD
}

data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            # CPU调度事件
            ftrace_events: "sched/sched_switch"
            ftrace_events: "sched/sched_waking"
            
            # 电源管理事件
            ftrace_events: "power/cpu_frequency"
            ftrace_events: "power/cpu_idle"
            
            # GPU事件(根据GPU型号选择)
            ftrace_events: "gfx/mali_gpu_total"
            ftrace_events: "gpu_mem_total/gpu_mem_total"
            
            # ATrace类别
            atrace_categories: "gfx"          # 图形渲染
            atrace_categories: "view"         # 视图系统
            atrace_categories: "sched"       # CPU调度
            atrace_categories: "freq"        # CPU频率
            atrace_categories: "idle"        # CPU空闲
            atrace_categories: "input"        # 输入事件
            atrace_categories: "dalvik"      # Java执行
            atrace_categories: "binder_driver" # Binder通信
            
            buffer_size_kb: 8192
            drain_period_ms: 250
        }
    }
}

data_sources: {
    config {
        name: "android.surfaceflinger.frame"
    }
}

duration_ms: 30000

2.2 滑动卡顿分析配置

scroll_jank_config.txt:

buffers: {
    size_kb: 63488
    fill_policy: DISCARD
}

data_sources: {
    config {
        name: "linux.ftrace"
        ftrace_config {
            ftrace_events: "sched/sched_switch"
            ftrace_events: "sched/sched_waking"
            ftrace_events: "power/cpu_frequency"
            ftrace_events: "power/cpu_idle"
            
            atrace_categories: "gfx"
            atrace_categories: "view"
            atrace_categories: "input"
            atrace_categories: "sched"
            atrace_categories: "freq"
            atrace_categories: "idle"
            atrace_categories: "dalvik"
            
            buffer_size_kb: 8192
            drain_period_ms: 250
        }
    }
}

data_sources: {
    config {
        name: "android.surfaceflinger.frame"
    }
}

duration_ms: 60000

3. 录制命令

3.1 基础录制

# 录制30秒的Trace
adb shell perfetto -c - --out /data/misc/perfetto-traces/trace -t 30s

# 指定应用录制
adb shell perfetto -c - --out /data/misc/perfetto-traces/trace \
    -a com.example.app -t 30s

3.2 使用配置文件录制

# 1. 推送配置文件
adb push jank_analysis_config.txt /data/local/tmp/

# 2. 开始录制(在复现卡顿前)
adb shell perfetto -c /data/local/tmp/jank_analysis_config.txt \
    --out /data/local/tmp/trace.pb

# 3. 复现卡顿问题
# (在设备上执行导致卡顿的操作)

# 4. 停止录制(Ctrl+C 或等待duration_ms时间)

# 5. 拉取Trace文件
adb pull /data/local/tmp/trace.pb jank_trace.pb

3.3 后台录制

# 后台录制,适合长时间监控
adb shell perfetto -c /data/local/tmp/jank_analysis_config.txt \
    --out /data/local/tmp/trace.pb --background

# 检查录制状态
adb shell ps | grep perfetto

# 停止录制
adb shell perfetto --stop

三、卡顿分析定位流程

1. 分析流程概览

1. 打开Trace文件
   ↓
2. 定位卡顿时间点
   ↓
3. 查看帧率信息
   ↓
4. 分析主线程状态
   ↓
5. 分析渲染线程状态
   ↓
6. 分析系统资源
   ↓
7. 定位根本原因
   ↓
8. 提供优化建议

2. 步骤1:打开Trace文件

2.1 使用Web UI打开

  1. 访问 https://ui.perfetto.dev/
  2. 点击 "Open trace file"
  3. 选择 trace.pb 文件
  4. 等待加载完成

2.2 初始检查

  • 时间范围:确认Trace覆盖了卡顿时间段
  • 进程列表:找到目标应用进程
  • 线程列表:确认主线程和RenderThread存在

3. 步骤2:定位卡顿时间点

3.1 方法1:查看Frame Timeline

1. 在顶部选择 "Frame Timeline"
2. 查看帧的颜色:
   - 绿色:正常帧(< 16.67ms)
   - 黄色:轻微掉帧(16.67-33.33ms)
   - 红色:严重掉帧(> 33.33ms)
3. 找到红色或黄色的帧
4. 点击帧,查看详细信息
5. 标记卡顿时间点(按M键)

3.2 方法2:查看VSync信息

1. 在SurfaceFlinger进程中查找 "VSync"
2. 查看VSync周期是否正常(60Hz = 16.67ms)
3. 检查是否有VSync丢失
4. 找到VSync异常的时间点

3.3 方法3:使用SQL查询

-- 查询所有掉帧
SELECT 
    ts,
    dur / 1000000.0 AS duration_ms
FROM slice 
WHERE name = 'Choreographer#doFrame' 
AND dur > 16666667
ORDER BY ts;

3.4 方法4:查看用户操作

1. 在Input进程中查找输入事件
2. 找到用户操作的时间点
3. 查看操作后的响应时间
4. 识别响应延迟的时间点

4. 步骤3:查看帧率信息

4.1 帧率统计

使用SQL查询帧率:

-- 计算平均帧率
SELECT 
    COUNT(*) * 1000000000.0 / (MAX(ts) - MIN(ts)) AS fps
FROM slice 
WHERE name = 'Choreographer#doFrame';

查看帧率分布:

-- 统计帧耗时分布
SELECT 
    CASE 
        WHEN dur < 16666667 THEN '正常'
        WHEN dur < 33333333 THEN '轻微掉帧'
        ELSE '严重掉帧'
    END AS frame_type,
    COUNT(*) AS count
FROM slice 
WHERE name = 'Choreographer#doFrame'
GROUP BY frame_type;

4.2 连续掉帧分析

-- 查询连续掉帧的情况
SELECT 
    ts,
    dur / 1000000.0 AS duration_ms,
    LAG(ts) OVER (ORDER BY ts) AS prev_ts,
    ts - LAG(ts) OVER (ORDER BY ts) AS gap_ns
FROM slice 
WHERE name = 'Choreographer#doFrame' 
AND dur > 16666667
ORDER BY ts;

5. 步骤4:分析主线程状态

5.1 查看主线程时间线

1. 在进程列表中找到目标应用
2. 展开进程,找到主线程(通常是包名或main)
3. 点击主线程,查看时间线
4. 查看线程状态:
   - Running(绿色):正在执行
   - Runnable(黄色):等待CPU
   - Sleep(红色):等待资源
   - UninterruptibleSleep(紫色):不可中断等待

5.2 分析主线程阻塞

查找Sleep状态:

1. 在主线程时间线中查找红色(Sleep)片段
2. 查看Sleep的持续时间
3. 分析Sleep的原因:
   - 等待Binder调用返回
   - 等待锁释放
   - 等待IO操作完成
   - 等待子线程返回

使用SQL查询主线程阻塞:

-- 查询主线程Sleep时间
SELECT 
    SUM(dur) / 1000000.0 AS total_sleep_ms,
    COUNT(*) AS sleep_count
FROM sched 
WHERE utid = (SELECT utid FROM thread WHERE name LIKE '%main%' LIMIT 1)
AND state = 'S';

5.3 分析主线程耗时操作

查找耗时操作:

1. 在主线程时间线中按耗时排序
2. 查看耗时最长的操作
3. 分析操作是否可以优化:
   - 是否可以移到后台线程
   - 是否可以异步执行
   - 是否可以优化算法

使用SQL查询耗时操作:

-- 查询主线程耗时最长的操作
SELECT 
    name,
    dur / 1000000.0 AS duration_ms,
    ts
FROM slice 
WHERE utid = (SELECT utid FROM thread WHERE name LIKE '%main%' LIMIT 1)
ORDER BY dur DESC
LIMIT 20;

5.4 常见主线程阻塞场景

场景1:等待Binder调用

  • 特征:主线程处于Sleep状态,等待Binder返回
  • 查找:查看wakeup信息,找到对应的Binder调用
  • 分析:检查SystemServer中的Binder处理耗时
  • 解决:优化Binder服务端处理,或使用异步调用

场景2:等待锁

  • 特征:主线程处于Sleep状态,等待锁释放
  • 查找:查看wakeup信息,找到锁持有者
  • 分析:检查锁持有时间是否过长
  • 解决:减少锁持有时间,使用细粒度锁

场景3:等待IO

  • 特征:主线程处于UninterruptibleSleep-IO状态
  • 查找:查看IO操作(文件读写、数据库操作)
  • 分析:检查IO操作是否在主线程执行
  • 解决:使用异步IO操作

场景4:等待网络

  • 特征:主线程等待网络响应
  • 查找:查看网络请求相关操作
  • 分析:检查网络请求是否同步执行
  • 解决:使用异步网络请求

场景5:等待子线程

  • 特征:主线程等待子线程返回数据
  • 查找:查看wakeup信息,找到依赖的子线程
  • 分析:检查子线程执行耗时
  • 解决:优化子线程执行,或使用回调机制

6. 步骤5:分析渲染线程状态

6.1 查看RenderThread时间线

1. 在应用进程中找到RenderThread线程
2. 查看线程状态和执行情况
3. 检查是否有阻塞或延迟

6.2 分析渲染耗时

关键操作分析:

1. 在RenderThread中查找关键操作:
   - drawFrame:绘制帧
   - dequeueBuffer:获取Buffer
   - queueBuffer:提交Buffer
2. 分析每个操作的耗时
3. 识别渲染瓶颈

使用SQL查询渲染耗时:

-- 查询RenderThread耗时操作
SELECT 
    name,
    dur / 1000000.0 AS duration_ms,
    ts
FROM slice 
WHERE utid = (SELECT utid FROM thread WHERE name LIKE '%RenderThread%' LIMIT 1)
ORDER BY dur DESC
LIMIT 20;

6.3 常见渲染问题

问题1:dequeueBuffer耗时

  • 特征:RenderThread等待Buffer分配
  • 原因:SurfaceFlinger繁忙或Buffer不足
  • 分析:查看SurfaceFlinger进程状态
  • 解决:优化SurfaceFlinger,增加Buffer数量

问题2:queueBuffer耗时

  • 特征:RenderThread等待Buffer提交
  • 原因:SurfaceFlinger处理延迟
  • 分析:查看SurfaceFlinger合成耗时
  • 解决:优化SurfaceFlinger合成流程

问题3:drawFrame耗时

  • 特征:RenderThread绘制耗时过长
  • 原因:绘制内容复杂、GPU性能不足
  • 分析:查看GPU使用情况和绘制命令
  • 解决:优化绘制内容,使用GPU加速

7. 步骤6:分析系统资源

7.1 CPU性能分析

查看CPU频率:

1. 在顶部选择 "CPU frequency"
2. 查看卡顿期间CPU频率变化
3. 检查是否有降频或频率不足

查看CPU使用率:

1. 在顶部选择 "CPU usage"
2. 查看卡顿期间CPU使用情况
3. 检查是否有CPU瓶颈

查看CPU调度:

1. 在CPU核心时间线中查看任务调度
2. 检查关键任务是否跑在小核
3. 查看调度延迟

使用SQL查询CPU使用率:

-- 查询CPU使用率
SELECT 
    cpu,
    COUNT(*) * 100.0 / (SELECT COUNT(*) FROM sched WHERE cpu = s.cpu) AS usage_percent
FROM sched s
WHERE ts BETWEEN ? AND ?  -- 卡顿时间段
GROUP BY cpu;

7.2 内存性能分析

查看GC活动:

1. 在主线程中查找 "HeapTaskDaemon"
2. 查看GC频率和耗时
3. 检查是否在关键帧期间发生GC

使用SQL查询GC频率:

-- 查询GC频率
SELECT 
    COUNT(*) AS gc_count,
    (MAX(ts) - MIN(ts)) / 1000000000.0 AS duration_sec,
    COUNT(*) * 1.0 / ((MAX(ts) - MIN(ts)) / 1000000000.0) AS gc_per_sec
FROM slice 
WHERE name LIKE '%GC%'
AND ts BETWEEN ? AND ?;  -- 卡顿时间段

查看内存使用:

1. 在顶部选择 "Memory"
2. 查看卡顿期间内存使用趋势
3. 检查是否有内存泄漏

7.3 GPU性能分析

查看GPU使用率:

1. 在顶部选择 "GPU frequency" 或 "GPU usage"
2. 查看GPU频率和使用率
3. 检查是否有GPU瓶颈

查看GPU渲染时间:

1. 在RenderThread中查找GPU相关操作
2. 查看GPU渲染耗时
3. 检查是否有GPU过载

8. 步骤7:定位根本原因

8.1 综合分析

根据前面的分析结果,综合判断卡顿的根本原因:

  1. 主线程阻塞:如果主线程长时间Sleep,分析阻塞原因
  2. 渲染瓶颈:如果RenderThread耗时过长,分析渲染瓶颈
  3. 系统资源不足:如果CPU/GPU/内存不足,分析资源竞争
  4. 锁竞争:如果多线程竞争锁,分析锁竞争情况
  5. 内存压力:如果GC频繁,分析内存使用情况

8.2 问题优先级

根据卡顿的严重程度和影响范围,确定问题优先级:

  • P0:严重卡顿,影响核心功能
  • P1:明显卡顿,影响用户体验
  • P2:轻微卡顿,可接受但需优化
  • P3:偶发卡顿,影响较小

9. 步骤8:提供优化建议

9.1 主线程优化

  • 异步化:将耗时操作移到后台线程
  • 优化算法:减少主线程计算量
  • 减少锁竞争:优化锁的使用
  • 优化IO:使用异步IO操作

9.2 渲染优化

  • 减少绘制内容:优化视图层级
  • 使用硬件加速:启用GPU加速
  • 优化布局:减少布局复杂度
  • 减少过度绘制:优化绘制区域

9.3 系统资源优化

  • CPU优化:优化任务调度,绑定关键任务到高性能核心
  • 内存优化:减少内存分配,优化GC
  • GPU优化:优化GPU使用,减少GPU负载

四、常见卡顿场景分析

1. 滑动卡顿分析

1.1 问题特征

  • 列表滑动时出现卡顿
  • 滑动不跟手
  • 滑动后页面继续滑动(惯性滑动)时卡顿

1.2 分析步骤

1. 定位滑动时间段
   - 查找Input事件(触摸事件)
   - 找到滑动开始和结束时间
   
2. 查看帧率
   - 查看滑动期间的帧率
   - 识别掉帧的时间点
   
3. 分析主线程
   - 查看主线程在滑动期间的状态
   - 查找耗时操作
   
4. 分析渲染线程
   - 查看RenderThread在滑动期间的状态
   - 查找渲染瓶颈
   
5. 分析系统资源
   - 查看CPU使用情况
   - 查看内存和GC情况

1.3 常见原因

  • 主线程阻塞:滑动时主线程执行耗时操作
  • 布局复杂:列表项布局过于复杂
  • 图片加载:滑动时加载大量图片
  • 数据绑定:滑动时频繁绑定数据
  • 系统资源不足:CPU或内存不足

2. 动画卡顿分析

2.1 问题特征

  • 页面跳转动画不流畅
  • UI动画卡顿
  • 转场动画掉帧

2.2 分析步骤

1. 定位动画时间段
   - 查找动画开始和结束时间
   - 查看动画相关的Trace标记
   
2. 查看帧率
   - 查看动画期间的帧率
   - 识别掉帧的时间点
   
3. 分析动画线程
   - 查看动画线程状态
   - 查找动画计算耗时
   
4. 分析渲染
   - 查看渲染线程状态
   - 查找渲染瓶颈

2.3 常见原因

  • 动画计算耗时:动画计算过于复杂
  • 渲染瓶颈:渲染性能不足
  • 系统资源不足:CPU或GPU不足
  • 动画冲突:多个动画同时执行

3. 点击卡顿分析

3.1 问题特征

  • 点击后响应延迟
  • 点击后界面不更新
  • 点击后操作不执行

3.2 分析步骤

1. 定位点击事件
   - 查找Input事件(点击事件)
   - 找到点击时间点
   
2. 查看响应时间
   - 从点击到界面响应的时间
   - 识别响应延迟
   
3. 分析主线程
   - 查看主线程在点击后的状态
   - 查找耗时操作
   
4. 分析系统服务
   - 查看SystemServer处理耗时
   - 查看Binder调用耗时

3.3 常见原因

  • 主线程阻塞:点击后主线程执行耗时操作
  • Binder调用延迟:跨进程调用耗时
  • 系统服务繁忙:SystemServer处理延迟
  • 资源加载:点击后加载大量资源

五、分析技巧与最佳实践

1. 分析技巧

1.1 时间对齐

  • 使用书签(M键)标记关键时间点
  • 使用时间选择(Shift+拖拽)选择时间范围
  • 使用缩放功能(W/S键)调整时间范围

1.2 多维度分析

  • 同时查看主线程、RenderThread、系统资源
  • 对比正常和异常时间段
  • 使用SQL查询进行统计分析

1.3 对比分析

  • 对比优化前后的Trace
  • 对比不同版本的性能
  • 对比竞品的性能

2. 最佳实践

2.1 录制Trace

  • 明确问题场景和复现步骤
  • 选择合适的录制时长
  • 保持环境一致
  • 多次录制确保结果一致

2.2 分析Trace

  • 系统化分析,按照固定流程
  • 关注关键指标
  • 深入分析,找到根本原因
  • 记录分析过程和结果

2.3 优化验证

  • 优化后再次录制Trace
  • 对比优化前后的性能
  • 验证优化效果
  • 确保没有引入新问题

3. 常见误区

3.1 只看主线程

  • 误区:只关注主线程,忽略其他线程
  • 正确:综合分析主线程、RenderThread、系统资源

3.2 只看表面现象

  • 误区:只看掉帧,不分析原因
  • 正确:深入分析,找到根本原因

3.3 忽略系统因素

  • 误区:只关注应用代码,忽略系统因素
  • 正确:综合考虑应用和系统因素

六、实际案例分析

案例1:列表滑动卡顿

问题描述:

  • 列表滑动时出现明显卡顿
  • 滑动不跟手
  • 掉帧严重

分析过程:

  1. 录制滑动场景的Trace
  2. 定位滑动时间段
  3. 发现主线程在滑动期间执行了大量布局计算
  4. 发现RenderThread绘制耗时过长
  5. 发现CPU使用率过高

根本原因:

  • 列表项布局过于复杂
  • 滑动时频繁重新计算布局
  • 绘制内容过多

优化方案:

  • 优化列表项布局,减少层级
  • 使用ViewHolder复用视图
  • 减少绘制内容
  • 使用硬件加速

优化效果:

  • 滑动流畅度提升80%
  • 掉帧减少90%

案例2:页面跳转卡顿

问题描述:

  • 页面跳转动画不流畅
  • 跳转后界面响应延迟

分析过程:

  1. 录制页面跳转的Trace
  2. 定位跳转时间段
  3. 发现主线程在跳转时执行了大量初始化操作
  4. 发现Binder调用耗时过长
  5. 发现系统服务处理延迟

根本原因:

  • 页面跳转时同步执行初始化
  • 跨进程调用过多
  • 系统服务繁忙

优化方案:

  • 延迟非关键初始化
  • 优化跨进程调用
  • 使用异步加载
  • 优化系统服务调用

优化效果:

  • 跳转时间减少60%
  • 动画流畅度提升

七、总结

使用Perfetto分析应用卡顿问题的关键步骤:

  1. 明确问题场景:了解卡顿的具体表现和复现步骤
  2. 录制Trace:选择合适的配置和录制方法
  3. 定位卡顿时间点:使用多种方法找到卡顿发生的时间
  4. 分析主线程:查看主线程状态,找到阻塞原因
  5. 分析渲染线程:查看RenderThread状态,找到渲染瓶颈
  6. 分析系统资源:查看CPU、内存、GPU使用情况
  7. 定位根本原因:综合分析,找到卡顿的根本原因
  8. 提供优化建议:给出针对性的优化方案

通过系统化的分析流程,可以高效地定位和解决应用卡顿问题。


最后更新:2024年