20 KiB
20 KiB
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打开
- 访问 https://ui.perfetto.dev/
- 点击 "Open trace file"
- 选择 trace.pb 文件
- 等待加载完成
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 综合分析
根据前面的分析结果,综合判断卡顿的根本原因:
- 主线程阻塞:如果主线程长时间Sleep,分析阻塞原因
- 渲染瓶颈:如果RenderThread耗时过长,分析渲染瓶颈
- 系统资源不足:如果CPU/GPU/内存不足,分析资源竞争
- 锁竞争:如果多线程竞争锁,分析锁竞争情况
- 内存压力:如果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:列表滑动卡顿
问题描述:
- 列表滑动时出现明显卡顿
- 滑动不跟手
- 掉帧严重
分析过程:
- 录制滑动场景的Trace
- 定位滑动时间段
- 发现主线程在滑动期间执行了大量布局计算
- 发现RenderThread绘制耗时过长
- 发现CPU使用率过高
根本原因:
- 列表项布局过于复杂
- 滑动时频繁重新计算布局
- 绘制内容过多
优化方案:
- 优化列表项布局,减少层级
- 使用ViewHolder复用视图
- 减少绘制内容
- 使用硬件加速
优化效果:
- 滑动流畅度提升80%
- 掉帧减少90%
案例2:页面跳转卡顿
问题描述:
- 页面跳转动画不流畅
- 跳转后界面响应延迟
分析过程:
- 录制页面跳转的Trace
- 定位跳转时间段
- 发现主线程在跳转时执行了大量初始化操作
- 发现Binder调用耗时过长
- 发现系统服务处理延迟
根本原因:
- 页面跳转时同步执行初始化
- 跨进程调用过多
- 系统服务繁忙
优化方案:
- 延迟非关键初始化
- 优化跨进程调用
- 使用异步加载
- 优化系统服务调用
优化效果:
- 跳转时间减少60%
- 动画流畅度提升
七、总结
使用Perfetto分析应用卡顿问题的关键步骤:
- 明确问题场景:了解卡顿的具体表现和复现步骤
- 录制Trace:选择合适的配置和录制方法
- 定位卡顿时间点:使用多种方法找到卡顿发生的时间
- 分析主线程:查看主线程状态,找到阻塞原因
- 分析渲染线程:查看RenderThread状态,找到渲染瓶颈
- 分析系统资源:查看CPU、内存、GPU使用情况
- 定位根本原因:综合分析,找到卡顿的根本原因
- 提供优化建议:给出针对性的优化方案
通过系统化的分析流程,可以高效地定位和解决应用卡顿问题。
最后更新:2024年