这里写目录标题
- 目录
-
-
- **一、一帧完整绘制的关键阶段**
- [**二、在 Perfetto 中搜索关键事件与函数**](#二、在 Perfetto 中搜索关键事件与函数)
-
- [**1. 应用进程(App)的绘制阶段**](#1. 应用进程(App)的绘制阶段)
- [**2. 缓冲区提交阶段**](#2. 缓冲区提交阶段)
- [**3. SurfaceFlinger 合成阶段**](#3. SurfaceFlinger 合成阶段)
- [**4. 显示刷新阶段**](#4. 显示刷新阶段)
- **三、具体分析步骤**
-
- [**1. 定位目标时间段**](#1. 定位目标时间段)
- [**2. 找到"一帧的起点":Choreographer.doFrame**](#2. 找到“一帧的起点”:Choreographer.doFrame)
- [**3. 追踪应用绘制链路**](#3. 追踪应用绘制链路)
- [**4. 追踪 SurfaceFlinger 合成链路**](#4. 追踪 SurfaceFlinger 合成链路)
- [**5. 确认帧显示完成:VSYNC 与显示事件**](#5. 确认帧显示完成:VSYNC 与显示事件)
- [**6. 分析瓶颈**](#6. 分析瓶颈)
- **四、实用工具与技巧**
- 送显的核心作用
- 送显的关键环节
- [送显在 Perfetto 中的分析关注点](#送显在 Perfetto 中的分析关注点)
- **一、先确定卡顿类型:按"帧耗时超标原因"分类**
- **二、分析切入点:从"帧链路关键节点"定位卡顿阶段**
-
- [**1. 第一步:找到卡顿帧,确定总耗时**](#1. 第一步:找到卡顿帧,确定总耗时)
- [**2. 第二步:按卡顿类型定位切入点**](#2. 第二步:按卡顿类型定位切入点)
-
- **(1)应用绘制卡顿:从应用主线程的"耗时操作"切入**
- [**(2)合成卡顿:从 SurfaceFlinger 的"图层处理"切入**](#(2)合成卡顿:从 SurfaceFlinger 的“图层处理”切入)
- [**(3)送显卡顿:从"VSYNC 对齐"和"缓冲区"切入**](#(3)送显卡顿:从“VSYNC 对齐”和“缓冲区”切入)
- [**(4)系统资源卡顿:从"线程阻塞"和"CPU 调度"切入**](#(4)系统资源卡顿:从“线程阻塞”和“CPU 调度”切入)
- **三、实战技巧:快速缩小范围**
- **总结**
-
目录
在 Perfetto 中分析"一帧完整绘制过程"时,核心是追踪从应用发起绘制请求 到屏幕最终渲染 的全链路,涉及 Android 系统的三大核心组件:应用进程(App) 、SurfaceFlinger(合成服务) 、显示硬件(如 GPU、屏幕刷新)。以下是具体分析方法:
一、一帧完整绘制的关键阶段
正常情况下,一帧的完整流程包含 4 个核心步骤(简称"渲染流水线"):
- 应用绘制(App Draw):应用进程通过 Canvas 或 OpenGL/ Vulkan 绘制 UI 元素(如 View 布局、自定义绘制)。
- 提交缓冲区(Buffer Queue):应用将绘制结果(图形缓冲区)提交到 SurfaceFlinger 的 BufferQueue。
- 合成(Compose):SurfaceFlinger 接收多个应用的缓冲区,进行图层合成(如叠加、透明度处理)。
- 显示(Present):合成结果发送到显示硬件(如通过 HWC 硬件合成),最终在屏幕上刷新显示。
这一过程需匹配屏幕刷新率(如 60Hz 对应每帧约 16.6ms),超过该时间会导致卡顿。
二、在 Perfetto 中搜索关键事件与函数
需通过过滤以下事件类型 和函数/进程名,定位一帧的完整链路:
1. 应用进程(App)的绘制阶段
- 核心进程 :目标应用进程(如
com.example.myapp
)。 - 关键事件/函数 :
Choreographer#doFrame
:系统向应用发送的帧回调(触发应用开始绘制),是一帧的起点。- 搜索关键词:
Choreographer.doFrame
或android.animation.Choreographer.doFrame
。
- 搜索关键词:
ViewRootImpl#performTraversals
:应用执行测量(measure)、布局(layout)、绘制(draw)的入口。- 搜索关键词:
ViewRootImpl.performTraversals
。
- 搜索关键词:
Canvas.draw*
:应用通过 Canvas 绘制的具体操作(如drawRect
、drawText
)。OpenGL/Vulkan 调用
:若使用硬件加速,会有glDrawArrays
、vkQueueSubmit
等 GPU 绘制函数。
2. 缓冲区提交阶段
- 核心进程 :应用进程 +
surfaceflinger
进程。 - 关键事件/函数 :
Surface#queueBuffer
:应用将绘制完成的缓冲区提交给 BufferQueue。- 搜索关键词:
Surface.queueBuffer
或android.view.Surface.queueBuffer
。
- 搜索关键词:
BufferQueueProducer#queueBuffer
:BufferQueue 接收应用提交的缓冲区(SurfaceFlinger 侧)。
3. SurfaceFlinger 合成阶段
- 核心进程 :
surfaceflinger
(系统合成服务)。 - 关键事件/函数 :
SurfaceFlinger::onMessageInvalidate
:SurfaceFlinger 收到缓冲区更新,触发合成。SurfaceFlinger::commit
:执行图层合成(软件合成或硬件合成)。HWComposer::presentDisplay
:若使用硬件合成(HWC),将合成结果提交给显示硬件。
4. 显示刷新阶段
- 核心事件 :
vsync
:垂直同步信号(屏幕刷新的时间基准),每帧一次,由硬件触发。- 搜索关键词:
vsync
或VSYNC
(通常在ftrace
事件中以vsync-app
或vsync-sf
区分应用侧和 SurfaceFlinger 侧的 VSYNC)。
- 搜索关键词:
display_hw
相关事件:显示硬件的刷新完成事件(如hwc_commit
)。
三、具体分析步骤
以"分析某应用的一帧绘制流程"为例:
1. 定位目标时间段
- 加载包含应用交互的 trace 文件(如滑动页面时的追踪)。
- 在左侧
Processes
中展开目标应用进程(如com.example.myapp
)和surfaceflinger
进程,显示其线程活动。
2. 找到"一帧的起点":Choreographer.doFrame
- 在顶部搜索框输入
Choreographer.doFrame
,主视图会高亮所有应用侧的帧回调事件。 - 点击某一个
doFrame
事件块,右侧详情面板会显示其开始时间(如t=1000ms
)和线程(通常是应用的main
线程)。
3. 追踪应用绘制链路
- 从
doFrame
开始,在应用main
线程的时间轴上,向后查找:- 若出现
ViewRootImpl.performTraversals
事件,说明进入测量、布局、绘制阶段。 - 后续若有
Canvas.draw*
或glDrawArrays
等事件,对应具体绘制操作。 - 绘制结束后,会出现
Surface.queueBuffer
事件,标志应用提交缓冲区完成(记录此时时间t1
)。
- 若出现
4. 追踪 SurfaceFlinger 合成链路
- 切换到
surfaceflinger
进程的线程(通常是main
线程),查找BufferQueueProducer.queueBuffer
事件(对应应用提交的缓冲区到达 SurfaceFlinger,时间t2
)。 - 后续会出现
SurfaceFlinger.commit
或HWComposer.presentDisplay
事件,标志合成完成(时间t3
)。
5. 确认帧显示完成:VSYNC 与显示事件
- 在左侧
CPUs
或Events
中查找vsync
事件,合成完成后下一个vsync
信号会触发屏幕刷新(时间t4
)。 - 一帧的总耗时为
t4 - t0
(t0
为上一帧的vsync
时间),若超过 16.6ms(60Hz)则可能卡顿。
6. 分析瓶颈
- 应用绘制耗时过长 :若
performTraversals
到queueBuffer
时间过长(如 >10ms),说明应用布局复杂或绘制逻辑低效(需优化 View 层级、减少过度绘制)。 - 合成耗时过长 :
surfaceflinger
中commit
到presentDisplay
时间过长,可能是图层过多或硬件合成能力不足(需减少图层、使用SurfaceControl
优化)。 - VSYNC 对齐问题 :若帧提交时间错过 VSYNC 信号,会导致多等待一帧(如本应 16ms 显示,实际 32ms),需检查应用是否及时响应
doFrame
。
四、实用工具与技巧
-
使用"Frame Timeline"视图
若 trace 包含
android.frame_timeline
数据源(需在配置中添加),左侧会出现Frame Timeline
追踪项,直接显示每帧的开始/结束时间、总耗时及各阶段分解(如App
、SF
、Present
耗时),一目了然。 -
通过 SQL 统计帧耗时
在 Perfetto 的 SQL 编辑器中执行以下查询,统计应用每帧的总耗时:
sqlSELECT ts / 1e6 AS start_ms, -- 帧开始时间(毫秒) dur / 1e6 AS frame_duration_ms, -- 帧总耗时(毫秒) package -- 应用包名 FROM android_frame_timeline_slice WHERE package = 'com.example.myapp' ORDER BY start_ms;
若
frame_duration_ms
频繁 >16.6,则存在卡顿。 -
对比正常帧与卡顿帧
找到耗时正常的帧(如 10ms)和卡顿帧(如 30ms),对比两者在应用绘制、合成阶段的事件差异,定位卡顿帧中额外的耗时操作(如突然出现的大对象绘制、锁等待)。
通过以上方法,可清晰拆解一帧的完整流程,并定位各阶段的性能瓶颈。核心是抓住 Choreographer.doFrame
(起点)、queueBuffer
(应用提交)、SurfaceFlinger
合成、VSYNC
(显示)这几个关键节点,结合时间差分析耗时。
送显
在 Android 性能分析(尤其是图形渲染和帧显示流程)中,"送显" 是指将最终合成的图像数据提交给显示硬件(如屏幕、显示屏控制器),并由硬件完成实际像素刷新的过程,是一帧画面从"系统处理完成"到"用户可见"的最后一步。
送显的核心作用
在帧渲染的全链路中,应用进程完成绘制、SurfaceFlinger 完成图层合成后,得到的是一帧完整的图像数据(通常存储在图形缓冲区中)。送显就是将这一缓冲区的数据"交付"给显示硬件,触发屏幕的物理刷新,让用户看到这一帧画面。
送显的关键环节
-
合成结果提交
SurfaceFlinger 完成图层合成后(无论是软件合成还是硬件合成 HWC),会将最终的图像缓冲区通过 HWComposer(硬件合成器) 提交给显示控制器(Display Controller)。
- 对应关键事件:
HWComposer::presentDisplay
(Perfetto 中可搜索该函数名追踪)。
- 对应关键事件:
-
显示硬件刷新
显示控制器接收到缓冲区后,会等待下一个 VSYNC(垂直同步信号),在信号触发时将缓冲区数据发送到屏幕的物理像素,完成实际显示。
- VSYNC 是屏幕刷新的时间基准(如 60Hz 屏幕每 16.6ms 一次),确保画面刷新与屏幕扫描节奏一致,避免画面撕裂。
送显在 Perfetto 中的分析关注点
- 送显时机 :通过搜索
vsync
事件和presentDisplay
事件,查看合成完成到送显的时间间隔。若间隔过长,可能导致帧显示延迟(错过 VSYNC 信号,需多等待一帧)。 - 送显成功率 :若频繁出现
hwc_commit_failed
等事件,可能是硬件合成失败或缓冲区异常,导致画面卡顿或闪烁。 - 送显耗时:正常情况下送显应在 1-2ms 内完成,若耗时过长(如超过 5ms),可能是显示硬件性能不足或驱动问题。
简言之,"送显"是连接系统图形处理与用户视觉感知的最后一步,直接影响帧显示的及时性和流畅度。
在 Perfetto 的 trace 中分析卡顿问题时,核心是先通过帧耗时判断卡顿类型,再定位卡顿发生的阶段(应用绘制、合成、送显等),最后聚焦具体函数或事件作为切入点深入分析。以下是具体方法:
一、先确定卡顿类型:按"帧耗时超标原因"分类
卡顿的本质是单帧耗时超过屏幕刷新率对应的阈值(如 60Hz 屏幕单帧应 ≤16.6ms,90Hz 应 ≤11.1ms)。根据卡顿发生的环节,可分为以下几类:
卡顿类型 | 核心特征(Perfetto 中表现) | 常见原因 |
---|---|---|
应用绘制卡顿 | 应用进程(如 com.example.app )的 main 线程耗时过长(doFrame 到 queueBuffer 阶段 >10ms) |
布局复杂、过度绘制、主线程做耗时操作(如 IO、大量计算)、自定义 View 绘制低效等。 |
合成卡顿 | SurfaceFlinger 进程(surfaceflinger )的合成阶段耗时过长(queueBuffer 到 presentDisplay 阶段 >5ms) |
图层过多(>4 层)、硬件合成(HWC)失败降级为软件合成、图层格式不兼容导致额外转换等。 |
送显卡顿 | 合成完成到屏幕显示的时间间隔过长(错过 VSYNC 信号,导致多等待一帧) | 合成完成时间晚于 VSYNC 信号、显示硬件(如 GPU、显示屏控制器)响应慢、缓冲区争夺等。 |
系统资源卡顿 | 应用或 SurfaceFlinger 线程被阻塞(如锁等待、CPU 资源被抢占),导致无法及时处理帧任务 | 其他进程占用过高 CPU(如后台进程频繁唤醒)、线程优先级低被抢占、锁竞争(如 synchronized 阻塞)等。 |
二、分析切入点:从"帧链路关键节点"定位卡顿阶段
1. 第一步:找到卡顿帧,确定总耗时
-
通过
Frame Timeline
快速定位 (推荐):若 trace 包含
android.frame_timeline
数据源(配置时需添加),左侧Frame Timeline
追踪项会直接显示每帧的:- 总耗时(
Duration
):若超过 16.6ms(60Hz),即为卡顿帧。 - 各阶段耗时分解:
App
(应用绘制)、SF
(SurfaceFlinger 合成)、Present
(送显)。
例如:某帧App
耗时 20ms,SF
耗时 3ms,Present
耗时 1ms → 确定为应用绘制卡顿。
- 总耗时(
-
手动追踪帧链路 (无
Frame Timeline
时):以
Choreographer.doFrame
(应用帧起点)为基准,到下一个VSYNC
信号(显示终点)的时间差即为单帧耗时。若超过阈值,标记为卡顿帧。
2. 第二步:按卡顿类型定位切入点
(1)应用绘制卡顿:从应用主线程的"耗时操作"切入
- 核心追踪目标 :应用
main
线程的doFrame
→performTraversals
→queueBuffer
链路。 - 切入点操作 :
- 在卡顿帧的
doFrame
事件后,查看应用main
线程的时间轴,找到持续时间长的函数块(超过 5ms 的事件)。 - 重点搜索以下关键词,定位具体耗时操作:
ViewRootImpl.performTraversals
:若耗时过长(如 >8ms),说明测量(measure)、布局(layout)、绘制(draw)阶段有问题。- 展开该事件,查看子事件:
measure
耗时高 → 布局层级过深或onMeasure
逻辑复杂;draw
耗时高 → 过度绘制或onDraw
中做耗时计算。
- 展开该事件,查看子事件:
Canvas.draw*
或glDrawArrays
:若某绘制函数(如drawBitmap
)耗时过长 → 图片过大或绘制频繁。Handler.dispatchMessage
或Runnable.run
:若主线程执行了非 UI 操作(如new Thread
意外在主线程运行)→ 阻塞绘制。Binder.call
:若主线程频繁调用Binder
(如跨进程通信)→ IPC 耗时阻塞绘制。
- 在卡顿帧的
(2)合成卡顿:从 SurfaceFlinger 的"图层处理"切入
- 核心追踪目标 :
surfaceflinger
进程的main
线程(合成主线程)或RenderEngine
线程(软件合成)。 - 切入点操作 :
- 查看卡顿帧中
surfaceflinger
的queueBuffer
(接收应用缓冲区)到presentDisplay
(提交给硬件)的时间差,若 >5ms 则为合成卡顿。 - 重点搜索以下关键词:
SurfaceFlinger::commit
:若耗时过长 → 图层合成逻辑复杂,可进一步查看是否有SoftwareComposer
相关事件(说明硬件合成失败,降级为软件合成,耗时更高)。Layer::prepare
或Layer::composite
:若某图层处理耗时高 → 该图层可能过大(如全屏图层)、格式不兼容(如 YUV 转 RGB)或有透明度过高(需混合计算)。HWComposer::validateDisplay
:若耗时高 → 硬件合成器(HWC)处理图层时出现瓶颈,可能是图层数量超过硬件限制(如超过 4 层)。
- 查看卡顿帧中
(3)送显卡顿:从"VSYNC 对齐"和"缓冲区"切入
- 核心追踪目标 :
vsync
信号、presentDisplay
事件、BufferQueue
状态。 - 切入点操作 :
- 查看合成完成时间(
presentDisplay
的ts
)与下一个vsync
信号的时间差:- 若合成完成时间在
vsync
信号之后 → 错过本次刷新,需等待下一帧(多 16.6ms),属于送显延迟。
- 若合成完成时间在
- 搜索
buffer_queue
相关事件:- 若出现
dequeueBuffer blocked
(申请缓冲区阻塞)→ 应用或 SurfaceFlinger 缓冲区不足(如未及时释放),导致送显等待。 - 若
acquireBuffer
耗时过长 → SurfaceFlinger 获取应用缓冲区延迟,可能是缓冲区被占用(如应用未及时提交)。
- 若出现
- 查看合成完成时间(
(4)系统资源卡顿:从"线程阻塞"和"CPU 调度"切入
- 核心追踪目标 :应用/
surfaceflinger
线程的状态(如Running
/Sleeping
/Uninterruptible
)、CPU 核心负载、锁事件。 - 切入点操作 :
- 查看卡顿帧期间,应用
main
线程或surfaceflinger
线程是否频繁处于Sleeping
或Uninterruptible
状态:- 若
Sleeping
时间长 → 可能被低优先级线程抢占,或等待锁(如synchronized
锁被其他线程持有)。 - 搜索
sched_switch
事件:查看线程被切换出去的原因(prev_state=S
表示休眠),以及切换进来的线程(是否被高优先级线程抢占)。
- 若
- 查看 CPU 核心负载(左侧
CPUs
展开各核心):- 若某核心长期 100% 占用(绿色块填满时间轴)→ 可能是其他进程(如后台服务)占用过多 CPU,导致应用线程无法调度。
- 搜索进程名:通过顶部搜索框查找高 CPU 占用的进程(如
com.android.systemui
异常活跃)。
- 查看卡顿帧期间,应用
三、实战技巧:快速缩小范围
-
对比正常帧与卡顿帧 :
找到同一操作下的正常帧(如 10ms)和卡顿帧(如 30ms),对比两者在应用绘制、合成、CPU 调度上的差异。例如:卡顿帧中多了一个
onDraw
耗时 20ms 的事件 → 切入点就是该onDraw
函数。 -
利用 SQL 统计耗时TOP事件 :
在 Perfetto 的 SQL 编辑器中执行查询,定位应用主线程中耗时最长的函数:
sqlSELECT name, -- 函数名 SUM(dur) / 1e6 AS total_ms, -- 总耗时(ms) COUNT(*) AS count -- 调用次数 FROM slice WHERE utid IN (SELECT utid FROM thread WHERE name = 'main' AND process.name = 'com.example.app') GROUP BY name ORDER BY total_ms DESC LIMIT 10;
结果中排名靠前的函数(如
View.draw
)即为重点分析对象。 -
结合日志定位场景 :
若 trace 包含
android.log
数据源,可通过搜索应用日志(如D/MyApp: 开始滑动
)定位卡顿发生的业务场景(如滑动列表、弹窗显示),再聚焦该场景下的帧链路。
总结
- 先通过
Frame Timeline
或帧链路确定卡顿帧的总耗时及各阶段占比,划分卡顿类型(应用/合成/送显/资源)。 - 针对不同类型,聚焦对应进程(应用/
surfaceflinger
)和线程(main
线程/合成线程)。 - 搜索关键函数(如
performTraversals
、commit
)或事件(vsync
、sched_switch
),找到耗时最长的操作作为切入点,逐步拆解具体代码或逻辑问题。
通过这种"定位类型→聚焦阶段→拆解操作"的流程,可高效定位卡顿根源。