本篇是 Perfetto 系列文章的第八篇,主要深入介绍 Android 中的 Vsync 机制及其在 Perfetto 中的表现形式。文章将从 Perfetto 的角度来分析 Android 系统如何基于 Vsync 信号进行帧渲染和合成,涵盖 Vsync、Vsync-app、Vsync-sf、VsyncWorkDuration 等核心概念。
随着高刷新率屏幕的普及,理解 Vsync 机制变得更加重要。本文将以 120Hz 刷新率为主要叙事线,帮助开发者理解现代 Android 设备中 Vsync 的工作原理,以及如何在 Perfetto 中观察和分析 Vsync 相关的性能问题。
注:本文内容基于 Android 16 的最新架构和实现
系列文章目录
- Android Perfetto 系列目录
- Android Perfetto 系列 1:Perfetto 工具简介
- Android Perfetto 系列 2:Perfetto Trace 抓取
- Android Perfetto 系列 3:熟悉 Perfetto View
- Android Perfetto 系列 4:使用命令行在本地打开超大 Trace
- Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
- Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
- Android Perfetto 系列 7 - MainThread 和 RenderThread 解读
- Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析
- 视频(B站) - Android Perfetto 基础和案例分享
如果大家还没看过 Systrace 系列,下面是传送门:
- Systrace 系列目录 : 系统介绍了 Perfetto 的前身 Systrace 的使用,并通过 Systrace 来学习和了解 Android 性能优化和 Android 系统运行的基本规则。
- 个人博客 :个人博客,主要是 Android 相关的内容,也放了一些生活和工作相关的内容。
欢迎大家在 关于我 页面加入微信群或者星球,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容
什么是 Vsync
Vsync(Vertical Synchronization,垂直同步)是 Android 图形系统的核心机制,它的存在是为了解决一个根本性的问题:如何让软件的渲染节奏与硬件的显示节奏保持同步。
在没有 Vsync 机制之前,常见问题是屏幕撕裂(Screen Tearing)。当显示器读取 framebuffer 的同时,GPU 写入了下一帧,就会在同一次刷新中出现上下两部分不一致的画面。
Vsync 解决什么问题?
Vsync 机制的核心思想非常简单:让所有的渲染工作都按照显示器的刷新节拍来进行。具体来说:
- 同步信号:显示器每次开始新的刷新周期时,都会发出一个 Vsync 信号。
- 帧节拍与生产:应用侧在 Vsync 到来时由 Choreographer 驱动开始一帧的生产(Input/Animation/Traversal);CPU 提交渲染命令后,GPU 异步流水执行。SurfaceFlinger 侧在 Vsync 到来时进行 Buffer 的合成操作。
- 缓冲机制:使用双缓冲或三缓冲技术,确保显示器总是读取完整的帧数据。
这样,帧的生产与显示以 Vsync 为节拍对齐。以 120Hz 为例,每 8.333ms 会有一个显示机会;应用需要在该窗口前把可合成的 Buffer 提交给 SurfaceFlinger。关键约束是 queueBuffer
/acquire_fence
/present_fence
的时序;若未赶上本周期,会顺延到下一个周期显示。
Android 中 Vsync 的基本工作原理
Android 系统的 Vsync 实现比基本概念复杂得多,需要考虑多个不同的渲染组件,以及它们之间的协调工作。
Vsync 信号的分层架构
在 Android 系统中,并不是只有一个简单的 Vsync 信号。实际上,系统维护着多个不同用途的 Vsync 信号:
硬件 Vsync(HW Vsync):
这是最底层的 Vsync 信号,由显示硬件(HWC,Hardware Composer)产生。它的频率严格对应显示器的刷新率,比如 60Hz 的显示器会每 16.67ms 产生一次 HW Vsync,120Hz 的显示器会每 8.333ms 产生一次。(硬件 Vsync 回调由 HWC/SurfaceFlinger 管理,详见 frameworks/native/services/surfaceflinger
相关实现)
但是,HW Vsync 并不是一直开启的。由于频繁的硬件中断会消耗较多的电量,Android 系统采用了一种智能的策略:只有在需要精确同步的时候才开启 HW Vsync,大部分时间使用软件预测的方式生成 Vsync 信号。
Vsync-app(应用 Vsync):
这是专门用于驱动应用层渲染的 Vsync 信号。当应用需要进行 UI 更新时(比如用户触摸、动画运行、界面滚动等),应用会向系统申请接收 Vsync-app 信号。
1 | // frameworks/base/core/java/android/view/Choreographer.java |
Vsync-app 是按需申请的。如果应用界面是静态的,没有任何动画或用户交互,那么应用不会申请 Vsync-app 信号,系统也就不会为这个应用生成 Vsync 事件。
Vsync-sf(SurfaceFlinger Vsync):
这是专门用于驱动 SurfaceFlinger 进行图层合成的 Vsync 信号。SurfaceFlinger 是 Android 系统中负责将所有应用的图层合成为最终画面的服务。
Vsync-appSf(应用-SurfaceFlinger Vsync):
Android 13 引入的新信号类型。为消除旧设计中 sf EventThread 既唤醒 SurfaceFlinger 又服务部分 Choreographer 客户端带来的时序歧义,系统将两类职责分离:vsync-sf
专注唤醒 SurfaceFlinger,vsync-appSf
面向需要与 SurfaceFlinger 同步的客户端。
在 Perfetto 中观察 Vsync
Perfetto trace 中包含多个与 Vsync 相关的 Track,理解这些 Track 的含义有助于分析性能问题。
在 SurfaceFlinger 进程中:
vsync-app
显示应用 Vsync 信号状态,数值在 0 和 1 之间变化。每次数值变化代表一个 Vsync 信号。**vsync-sf **
显示 SurfaceFlinger Vsync 信号状态。无 Vsync Offset 时与vsync-app
同步变化。vsync-appSf
Android 13+ 新增,服务于需要与 SurfaceFlinger 同步的特殊 Choreographer 客户端。HW_VSYNC
显示硬件 Vsync 开启状态。值为 1 表示开启,值为 0 表示关闭。为节省电量,硬件 Vsync 仅在需要精确同步时开启。
在应用进程中:
FrameDisplayEventReceiver.onVsync
Slice Track:
显示应用接收 Vsync 信号的时间点。由于信号通过 Binder 传递,时间可能略晚于 SurfaceFlinger 中的 vsync-app
。
UI Thread
Slice Track:
包含 Choreographer#doFrame
及相关的 Input、Animation、Traversal 等 Slice。每个 doFrame
对应一帧的处理工作。
RenderThread
Slice Track:
包含 DrawFrame
、syncAndDrawFrame
、queueBuffer
等 Slice,对应渲染线程工作。
Android App 每一帧是如何基于 Vsync 工作的
Android 应用的每一帧基于 Vsync 机制完成从渲染到显示的完整过程涉及多个关键步骤。
流程总览(按顺序)
- 触发重绘/输入:
View.invalidate()
、动画、数据变化或输入事件触发 →ViewRootImpl.scheduleTraversals()
→Choreographer.postCallback(TRAVERSAL)
- 申请 Vsync:
Choreographer
通过DisplayEventReceiver.scheduleVsync()
申请下一次 Vsync(app 相位) - 接收 Vsync:
DisplayEventReceiver.onVsync()
收到 Vsync 后,向主线程消息队列投递异步消息 - 主线程帧处理:
Choreographer.doFrame()
按顺序执行五类回调:INPUT → ANIMATION → INSETS_ANIMATION → TRAVERSAL → COMMIT
- 渲染提交:
RenderThread
执行syncAndDrawFrame/DrawFrame
,CPU 记录 GPU 命令,queueBuffer
提交到 BufferQueue - 合成显示:
SurfaceFlinger
在vsync-sf
到来时合成(GPU/或HWC),生成present_fence
,输出到显示 - 帧完成度量:通过
FrameTimeline
(PresentType/JankType)与acquire/present_fence
判定是否按期显示
下面分别展开每一步的关键实现与 Perfetto 观测点。
App 什么时候会申请 Vsync 信号
应用并不是时刻都在申请 Vsync 信号的。Vsync 信号是按需申请的,只有在以下情况下,应用才会向系统申请下一个 Vsync:
触发申请 Vsync 的场景:
- UI 更新需求:当 View 调用
invalidate()
时 - 动画执行:ValueAnimator、ObjectAnimator 等动画开始时
- 用户交互:触摸事件、按键事件等需要 UI 响应时
- 数据变化:RecyclerView 数据更新、TextView 文本改变等
App 申请 Vsync 的完整流程
当应用需要更新 UI 时,会通过以下流程申请 Vsync 信号:
1 | // 1. UI 组件请求重绘 |
主线程如何监听 Vsync 信号
应用主线程通过 DisplayEventReceiver
来监听 Vsync 信号。这个过程涉及几个关键步骤:
1. 建立连接:
1 | // frameworks/base/core/java/android/view/Choreographer.java |
2. 接收 Vsync 信号:
1 |
|
几个遗留问题
Q1:为什么不在 onVsync()
中直接执行 doFrame()
?
- 线程边界:
onVsync()
可能不在主线程,UI 必须在主线程执行 - 调度控制:通过
sendMessageAtTime()
精确对齐执行时刻 - 队列语义:进入主线程 MessageQueue,确保与其他高优先级任务协同
Q2:Vsync 消息来了但主线程在忙,会丢吗?
- 不会。Vsync 消息入队后等待执行
Q3:CPU/GPU 是否必须在单个 Vsync 周期内完成?如果任何一个环节超过 1 个 vsync ,都会导致掉帧?
现代 Android 系统采用多缓冲(通常是三缓冲)机制:
应用端:Front Buffer(显示中)+ Back Buffer(渲染中)+ 可能的第三个 Buffer
SurfaceFlinger 端:也有类似的缓冲机制
这意味着即使应用的某一帧超过了 Vsync 周期,也不一定会立即掉帧。
GPU 异步流水;关键是
queueBuffer
是否赶上 SF 合成窗口,多缓冲可掩盖单帧延迟但可能引入额外时延,可以看到下图里面,App 端的 BufferQueue 和 SurfaceFlinger 端的 Buffer 都是充足的,且有冗余,所以没有掉帧。但是如果 App 在之前没有堆积 Buffer ,则还是会出现掉帧。
Q5:GPU 和 CPU 是怎么协同的?:
GPU 渲染是异步的,这带来了额外的复杂性:
- CPU 工作正常,GPU 成为瓶颈:即使应用主线程在 Vsync 周期内完成工作,GPU 渲染耗时过长仍会导致掉帧
- GPU Fence 机制:
presentFence
是连接 GPU 渲染和 SurfaceFlinger 合成的关键同步机制。根据系统Latch Unsignaled Buffers
策略,SurfaceFlinger 并非总是等待 GPU 完成,而是可以“抢跑”提前开始合成,仅在最终需要使用 Buffer 内容时才等待 fence 信号,以此来隐藏延迟。
Q6:Vsync Phase(相位差)的真正作用是什么?:
- 提升跟手性:通过调整 sf vsync 的相位差,可以让应用从开始绘制到显示在屏幕上的时间从 3 个 Vsync 周期缩短到 2 个 Vsync 周期。这对于触摸响应等交互场景非常重要。
- 解决应用绘制超时问题:当应用绘制超时时,合理的 sf 相位差可以为应用争取更多的处理时间,避免因为时序不当导致的掉帧。
- 通过观察 Perfetto 中的 VsyncWorkDuration 指标可以了解系统的 Vsync Offset 配置。
- 下图中显示的时间段就是我手上的手机配置的 app offset (13.3ms)
Vsync Offset 的技术实现
在 Android 系统中,Vsync Offset 是通过 VsyncConfiguration 来实现的:
1 | // frameworks/native/services/surfaceflinger/Scheduler/VsyncConfiguration.cpp |
关键概念:
appOffset
:应用 Vsync 相对于硬件 Vsync 的时间偏移sfOffset
:SurfaceFlinger Vsync 相对于硬件 Vsync 的时间偏移Vsync Offset = sfOffset - appOffset
:应用和 SurfaceFlinger 接收信号的时间差
实际的优化效果
以 120Hz 设备为例,配置 3ms Offset 的效果:
无 Offset(传统方式):
- T0:应用和 SurfaceFlinger 同时接收 Vsync
- T0+3ms:应用完成渲染
- T0+8.333ms:下一个 Vsync,SurfaceFlinger 开始合成
- T0+16.666ms:用户看到画面(总延迟 16.666ms)
有 Offset(优化方式):
- T0+1ms:应用接收 Vsync-app,开始渲染
- T0+3ms:应用完成渲染,提交 Buffer
- T0+4ms:SurfaceFlinger 接收 Vsync-sf,立即开始合成
- T0+6ms:SurfaceFlinger 完成合成
- T0+8.333ms:用户看到画面(总延迟 8.333ms)
通过合理配置 Offset,可以将延迟从 16.666ms 减少到 8.333ms,提升一倍的响应性能。
实际的时间预算分配:
以 120Hz 设备为例(8.333ms 周期):
- 理想情况:应用 4ms + SurfaceFlinger 2ms + 缓冲 2.333ms
- 但实际可以接受:应用 6ms + SurfaceFlinger 3ms(如果有足够的 Buffer 缓冲)
- GPU 限制:在低端设备上,GPU 渲染可能需要 10-15ms,成为真正的瓶颈
掉帧的真正原因:
- 应用端超时 + Buffer 耗尽:连续多帧超时导致 BufferQueue 没有可用 Buffer
- GPU 渲染超时:即使 CPU 工作正常,GPU 渲染超时也会掉帧
- SurfaceFlinger 超时:系统级合成超时,影响所有应用
- 系统资源竞争:CPU/GPU/内存等资源被其他进程占用
Vsync 信号的完整代码流程
Vsync 信号从硬件传递到应用层的完整链路如下。
Native 层:Vsync 信号的产生与管理
硬件 Vsync 的产生
Vsync 信号最初由显示硬件产生。在 HWC(Hardware Composer)层面,硬件会定期产生 Vsync 中断:
1 | // hardware/interfaces/graphics/composer/2.1/utils/hwc2on1adapter/HWC2On1Adapter.cpp |
VsyncController:核心控制器
VsyncController 是整个 Vsync 系统的大脑,它接收硬件 Vsync 并管理软件预测:
1 | // frameworks/native/services/surfaceflinger/Scheduler/VsyncController.h |
VsyncDispatch:信号分发机制
VsyncDispatch 负责将 Vsync 信号精确地分发给不同的消费者,每个消费者可以有不同的触发时间:
1 | // frameworks/native/services/surfaceflinger/Scheduler/VSyncDispatch.h |
EventThread:连接 Native 和 Framework
EventThread 是连接 Native 层 Vsync 系统和 Framework 层的桥梁:
1 | // frameworks/native/services/surfaceflinger/Scheduler/EventThread.cpp |
Framework 层:Vsync 信号的接收与处理
DisplayEventReceiver:Java 和 Native 的桥梁
DisplayEventReceiver 是 Framework 层接收 Vsync 信号的关键组件:
1 | // frameworks/base/core/jni/android_view_DisplayEventReceiver.cpp |
Choreographer 中的 Vsync 处理
在 Java 层,Choreographer 的 FrameDisplayEventReceiver 负责处理 Vsync 信号:
1 | // frameworks/base/core/java/android/view/Choreographer.java |
关键时序点分析
通过上述代码流程,我们可以看到完整的时序链路:
- HWC 产生硬件中断 →
VsyncController::onVsync()
- VsyncController 处理 →
VsyncDispatch::schedule()
- VsyncDispatch 分发 →
EventThread::onVSyncEvent()
- EventThread 通知 →
NativeDisplayEventReceiver::onVsync()
- Native 传递到 Java →
FrameDisplayEventReceiver::onVsync()
- 主线程处理 →
Choreographer::doFrame()
各环节的职责和优化点不同,理解完整流程有助于在 Perfetto 中分析 Vsync 相关性能问题。
FrameTimeline
App 和 SurfaceFlinger 都有 FrameTimeline
- 轨道:
Expected Timeline
、Actual Timeline
- PresentType/JankType:
- PresentType 指示本帧呈现方式(例如 On-time、Late),JankType 指示卡顿类型来源
- 常见 JankType:
AppDeadlineMissed
、BufferStuffing
、SfCpuDeadlineMissed
、SfGpuDeadlineMissed
等
- 操作步骤(Perfetto UI):
- 在应用进程选择目标
Surface/Layer
或使用 FrameToken 过滤 - 对齐 Expected 与 Actual,查看偏移与颜色编码
- 向上钻取:
Choreographer#doFrame
、RenderThread
、queueBuffer
、acquire/present_fence
- 在应用进程选择目标
- 误判规避:
- 仅凭
doFrame
时长判断掉帧不可靠;以 FrameTimeline 的 PresentType/JankType 为准 - 多缓冲可能掩盖单帧超时,需要看连续帧与 Buffer 可用性
- 仅凭
刷新率/显示模式/VRR 对 Vsync 与 Offset/预测的影响
- 模式切换:刷新率变更会重新配置
VsyncConfiguration
,影响 app/sf Offset 与预测模型;- Perfetto:查
display mode change
事件与随后的vsync
间隔变化
- Perfetto:查
- VRR(可变刷新率):目标周期不恒定,软件预测更依赖 present_fence 反馈校准;
- Perfetto:观察
vsync
间隔分布与present_fence
偏差
- Perfetto:观察
- 多显示/外接显示:不同
physicalDisplayId
的 vsync 源独立,注意 app/sf/appSf 的选择与对齐;- Perfetto:按显示 ID 过滤相关 Counter/Slice
Perfetto 实战 Checklist(建议按序查看)
- Vsync 信号与周期
vsync-app / vsync-sf / vsync-appSf
间隔是否稳定(60/90/120Hz 对应周期)- 是否存在异常密集/稀疏的 Vsync(预测抖动)
- Vsync 相位差配置
VsyncWorkDuration
是否符合机型预期的 app/sf Offset- app 与 sf 的先后是否匹配“先绘制后合成”的策略
- FrameTimeline 判读
- 先看
PresentType
,再看JankType
;确认是 app 还是 SF/GPU 侧问题 - 选择目标 Surface/FrameToken 定位具体帧
- 先看
- 应用主线程与渲染线程
Choreographer#doFrame
各阶段耗时(Input/Animation/Traversal)RenderThread
的syncAndDrawFrame/DrawFrame
耗时是否异常
- BufferQueue 与 Fence
- 生产者:RenderThread queueBuffer 之后,这个 Buffer 就被标记可以被消费了,在经过一次 Transaction 之后就可以被 sf 消费。刚刚 queueBuffer 之后 Buffer 还需要让 GPU 实际进行渲染,这里是用的 Fence 来追踪 GPU 的执行耗时。SF 也需要看这个 Fence 来决定这个 Buffer 是不是 Ready (新版本 SF 有个属性值配置,也可以不等这个 Fence,等后面实际要合成的时候才去看)。
- 消费者 SF 与 BufferTX:SF 作为 Buffer 的消费者,会在 vsync 到来的时候是会取一个 Buffer 进行合成的,这里 App 的 Buffer 是以 BufferTX-xxxx 的名字存在的,下图可以看到每次 SF 取走一个 Buffer,BufferTX 个数就会减1,这就是正常的。如果 BufferTX 为 0,说明 App 没有及时送 Buffer 上来,SF 也就停止合成了,这就会出现卡顿 (当然如果同时有多个出图源,SF 会取其他的 Buffer,但是对于 App 来说依然是卡顿的)
- 生产者:RenderThread queueBuffer 之后,这个 Buffer 就被标记可以被消费了,在经过一次 Transaction 之后就可以被 sf 消费。刚刚 queueBuffer 之后 Buffer 还需要让 GPU 实际进行渲染,这里是用的 Fence 来追踪 GPU 的执行耗时。SF 也需要看这个 Fence 来决定这个 Buffer 是不是 Ready (新版本 SF 有个属性值配置,也可以不等这个 Fence,等后面实际要合成的时候才去看)。
- 合成策略与显示
- SF 是否频繁走 ClientComposition;HWC validate/present 是否异常
- 多显示/模式切换/VRR 时是否伴随明显预测偏差
- 资源与其他干扰
- CPU 竞争(大核占用)、GPU 忙、IO/内存抖动(GC/compaction)
- 其他前台应用/系统服务是否占用关键资源
参考文档
- Android Graphics Architecture
- VSYNC Implementation Guide
- Frame Pacing
- Perfetto Documentation
- Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
- Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
- Vsync offset 相关技术分析
- Android 13/14高版本SurfaceFlinger出现VSYNC-app/VSYNC-appSf/VSYNC-sf剖析
关于我 && 博客
下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!
- 博主个人介绍 :里面有个人的微信和微信群链接。
- 本博客内容导航 :个人博客内容的一个导航。
- 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
- Android性能优化知识星球 : 欢迎加入,多谢支持~
一个人可以走的更快 , 一群人可以走的更远