一次滑动卡顿,主线程可能在处理布局,也可能早已把绘制命令交给 RenderThread。这篇用 Perfetto 截图对照两条线程、Buffer 提交和 SurfaceFlinger,找出等待发生在哪一步。
先沿截图读一帧,再看 ActivityThread、RenderThread 和 BLASTBufferQueue 的职责。Trace 能缩小调查范围;帧最终是否上屏,还要核对 FrameTimeline 与合成侧记录。
本文目录
- 系列文章目录
- 基于 Perfetto 的渲染流程分析
- 双线程渲染架构的演进
- 主线程的创建过程
- ActivityThread 的功能
- 渲染线程的创建和发展
- 性能
- Perfetto 独有的 FrameTimeline 功能
- Perfetto 中 Vsync 信号
- 参考
- 附件
- 关于我 && 博客
系列文章目录
- 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 机制与性能分析
- Android Perfetto 系列 9:CPU 信息解读
- Android Perfetto 系列 10:Binder 调度与锁竞争
- Android Perfetto 系列 11:PerfettoSQL、Trace Processor 与回归检测
- Android Perfetto 系列 12:Trace 数据流与丢失排查
- Android Perfetto 系列 13:Perfetto SDK、Track Event 与 App 现场 Trace
- Android Perfetto 系列 14:heapprofd 与内存 Profiling
- Android Perfetto 系列 15:Boot Trace、长时间现场 Trace 与偶发问题抓取
- Android Perfetto 系列 16:GPU、Power Counters 与硬件瓶颈分析
- Android Perfetto 系列 17:专项场景自动化、问题清单与平台体系
- Android Perfetto 系列 18:响应速度实战,从输入事件到首帧反馈
- 视频(B站) - Android Perfetto 基础和案例分享
- 视频(B站) - Android Perfetto 分享 - 出图类型分享:AOSP、WebView、Flutter + OEM 系统优化分享
如果大家还没看过 Systrace 系列,下面是传送门:
- Systrace 系列目录 : 系统介绍了 Perfetto 的前身 Systrace 的使用,并通过 Systrace 来学习和了解 Android 性能优化和 Android 系统运行的基本规则。
- 个人博客 :个人博客,主要是 Android 相关的内容,也放了一些生活和工作相关的内容。
欢迎大家在 关于我 页面查看交流方式,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容.
本文使用到的 Trace 文件我上传到了 Github :https://github.com/Gracker/SystraceForBlog/blob/master/Android_Perfetto/demo_app_aosp_scroll.perfetto-trace ,需要的可以自取。
版本说明(2026-10 核查):本文 Java/HWUI 节选以
android-16.0.0_r1核对,BLAST 计数以android-15.0.0_r1为例;Android 13 的 unsignaled latch 和 Android 17 的 MessageQueue 更新另行标注。附带 trace 的 SystemInfo 记录为 OPPO PKH110、Android 15/API 35、Perfetto v46.0,文件名含 aosp 不代表纯 AOSP 固件。多数新图显示示例 App,另有com.tencent.mm详情图和旧 Systrace 图,不能把它们拼成同一次采集。
基于 Perfetto 的渲染流程分析
这里以滑动列表为例,从主线程的一次 doFrame 走到 RenderThread 的绘制和提交。帧的路径会随软件/硬件绘制、窗口类型和更新内容变化;截图展示的是这一份 Trace 中实际出现的阶段。
帧的概念和基本参数
先区分显示刷新间隔与 App 实际产帧数。固定显示模式的名义刷新间隔如下:
- 60 Hz 模式:约每 16.67 ms 刷新一次。
- 90 Hz 模式:约每 11.11 ms 刷新一次。
- 120 Hz 模式:约每 8.33 ms 刷新一次。
屏幕可以重复显示上一帧,不能由刷新率直接推出 App 每秒提交了多少新帧。
在 Perfetto 中分析渲染性能时,需要重点关注以下两个线程:
- UI Thread:应用主线程,处理用户输入、业务逻辑、布局计算
- RenderThread:运行在 CPU 上的渲染线程,组织和提交 GPU 命令;GPU 执行命令,buffer 提交连接到 SurfaceFlinger
主线程和渲染线程的工作流程

截图里,主线程处理回调并记录绘制命令,RenderThread 接手后继续执行绘制和提交。先沿时间线找到两条线程的交接点,再看 Buffer 和 SurfaceFlinger;不要把图中的相邻 slice 全部当成同一帧。
重要说明:并非每一帧都会执行所有步骤。Input、Animation、Insets Animation 都是按需执行。Traversal(measure、layout、draw)同样是按需触发:requestLayout、invalidate、窗口属性/可见性变化等场景常会通过 scheduleTraversals() 投递 CALLBACK_TRAVERSAL。在连续滑动/动画场景下它常常“看起来像每帧都在跑”。
下面按截图中的观察顺序逐段看。
1. 主线程等待 Vsync 信号
- Perfetto trace:主线程在两次工作之间可能处于 Sleeping 状态。
- 流程说明:有待处理的帧时,Choreographer 请求下一次 VSync 回调;Sleeping 只能说明线程当时未运行,不能仅凭空白轨道断定它正在等待 VSync。
2. Vsync-app 信号传递过程
SurfaceFlinger 的调度器结合硬件时序与软件预测安排通知,不能把每次 VSYNC-app 翻转都解释为一次硬件中断即时转发。图中的 app/EventThread 服务可能同时面对多个连接;这条全局轨道不能单独归属某个 App。
Choreographer 按需请求一次通知并处理到期回调。其他 Vsync 订阅方式和渲染路径可能不同;某个 App 未申请这一轮 Choreographer 回调,不等于系统中没有其他帧或 buffer 提交。
3. SurfaceFlinger 唤醒 App 主线程
Vsync 事件经 DisplayEventReceiver 的 native FD/JNI 路径到达 Java onVsync(),再投递异步 Message。Looper 稍后分发 Receiver Runnable 时才进入 doFrame();通知到达和开始处理帧不是同一时刻。能否看到对应 slice 取决于插桩和采集配置,详见第五篇。
4. 处理输入事件(Input)
- Perfetto trace:
Input块 - 流程说明:只有这一帧登记了相应回调,才会出现
Input阶段;系统输入事件并非全部在这里处理。 - 触发条件:
- 可能有 Input 回调:手指滑动期间,ViewRootImpl 为当前帧登记了输入处理。
- 可能没有 Input 回调:惯性滑动或静止时没有对应待处理输入。
- 注意:是否出现该 slice 以当次 Trace 为准,不能由手指状态单独推断。
5. 处理动画(Animation)
animation 阶段执行已到期的 CALLBACK_ANIMATION,其中包括属性动画、View.postOnAnimation 和应用 FrameCallback。有该阶段不一定存在肉眼可见的动画;创建列表 item 或处理输入也不保证出现它。回调可能在之前或当前帧前面的阶段登记,不限于上一帧 post。
6. 处理 Insets 动画
insetsAnimation 阶段处理已到期的窗口 Insets 动画回调,例如受支持的 IME/系统栏动画。Insets 指系统 UI 占用的边缘区域,不是“插入窗口”。静态 Insets 变化、键盘出现或导航栏改变不保证每次都产生这一 slice。
7. Traversal(测量、布局、绘制准备)
- Perfetto trace:
performTraversals,measure,layout,draw - 流程说明: Android UI 渲染的三大核心流程,但并不是每个 Vsync 都完整执行一遍,是否执行取决于本帧是否有布局/绘制请求。
7.1 Measure(测量阶段)
- 作用: 确定每个 View 的尺寸大小
- 过程: 父容器按布局策略测量需要更新的子 View;缓存或跳过分支可减少工作,并非每次遍历都重测整棵树
- 关键概念:
MeasureSpec:封装了父容器对子 View 的尺寸要求(EXACTLY、AT_MOST、UNSPECIFIED)onMeasure():自定义 View 可重写此方法实现测量逻辑,并非每个 View 都要重写- Perfetto 中的表现:
measure事件,耗时取决于 View 层级复杂度
7.2 Layout(布局阶段)
- 作用: 确定每个 View 在父容器中的位置坐标
- 过程: 基于 Measure 阶段的结果,为每个 View 分配实际的显示位置
- 关键概念:
layout(left, top, right, bottom):设置 View 的四个边界坐标onLayout():ViewGroup 重写此方法来确定子 View 的位置- Perfetto 中的表现:
layout事件;耗时由布局逻辑与层级决定,不能普遍认定比 measure 快
7.3 Draw(绘制阶段)
- 作用: 将 View 的内容绘制到画布上
- 硬件加速 View 路径:记录或更新 DisplayList;软件 Canvas 路径仍可直接栅格化像素
- 关键流程:
draw(Canvas):绘制 View 自身内容onDraw(Canvas):子类重写实现具体绘制逻辑dispatchDraw(Canvas):ViewGroup 用来绘制子 View- Perfetto 中的表现:
draw事件,在硬件加速下主要是构建 DisplayList
ViewRootImpl.performTraversals 核心代码
1 | // frameworks/base/core/java/android/view/ViewRootImpl.java |
Android 16 关系示意:局部变量和外围分支未列出,不是可直接编译的完整方法。不可见、取消绘制与取消后重调度是不同分支;performDraw 也可能返回未绘制。
三阶段的执行条件:
- Measure: 在首次绘制、窗口/Insets/配置变化或布局请求触发时执行,不是每帧固定执行
- Layout: 当本帧存在
layoutRequested且应用处于可绘制状态时执行 - Draw: 满足绘制条件时执行;若被
cancelDraw/predraw等分支中断,会重新调度下一次 Traversal
8. 同步 DisplayList 到渲染线程
- Perfetto trace: syncAndDrawFrame,可见 “sync” 或 “syncAndDrawFrame” 事件(通常显示为主线程向渲染线程的数据传递点)
- 流程说明:主线程通过
syncAndDrawFrame将本帧 RenderNode/DisplayList 状态同步到 RenderThread。UI 线程会在DrawFrameTask::postAndWait等待关键同步点,解除阻塞后继续处理消息;屏幕呈现还要经过后续渲染与合成。
9. 渲染线程获取 Buffer
- Perfetto trace:按实际插桩查 dequeueBuffer/queueBuffer 等 slice。
DequeueBufferDuration/QueueBufferDuration是 HWUI 帧统计字段,不保证以同名 slice 出现 - 流程说明: 渲染线程在绘制提交阶段会通过
ANativeWindow/RenderPipeline路径完成 buffer 申请与交换。是否等待、等待多久,直接影响这一帧是否赶上 deadline。
10. 处理渲染指令并 flush 到 GPU
- Perfetto trace:
drawing相关块 - 流程说明: RenderThread(运行在 CPU 上)通过
HardwareRenderer/CanvasContext处理 UI 线程同步过来的 RenderNode 树,生成 GPU 命令并提交。提交关联的 fence 在 GPU 工作完成后 signal,供后续合成同步。
11. 提交 Buffer(可能 unsignaled)
queueBuffer 提交可携带尚未 signal 的 acquire fence,这是异步 GPU 管线允许的情况,不等于已经发生 unsignaled latch。前者是生产者提交,后者是 SurfaceFlinger 是否提前采用 buffer 的策略。仅有 queueBuffer slice 不足以确定 fence 状态,需对应 fence/图形数据。
12. 触发 Transaction 到 SurfaceFlinger
- Perfetto trace: TransactionQueue 或 BLAST transaction 事件 ,一般在 queueBuffer 之后,有些 Trace 没有这个 Tag
- 流程说明: App 侧通过 BLAST/SurfaceControl transaction 将 buffer 与层属性更新关联后提交到 SurfaceFlinger。SurfaceFlinger 再按
LatchUnsignaledConfig等策略决定 latch 时机并完成合成显示。
在 Perfetto 中识别不同的渲染模式:
- 手指滑动时:可先找
Input与Traversal,再确认是否进入 RenderThread。 - 惯性滑动时:常见
Animation与Traversal,是否还有输入回调要看当次事件。 - 静止状态时:没有新内容时,App 可以不提交新帧;后台动画或其他更新仍可能触发绘制。
软件绘制 vs 硬件加速
虽然现在基本都使用硬件加速渲染,但了解两种渲染模式的区别仍然有助于理解 Perfetto trace:
| 方面 | 软件绘制 | 硬件加速 |
|---|---|---|
| 绘制线程 | 普通 View 路径通常在主线程;其他 Canvas 用法可不同 | 主线程记录命令,RenderThread 组织后续工作,GPU 执行 |
| 绘制引擎 | Skia (CPU) | Skia 的 OpenGL/Vulkan 等 GPU 后端 |
| Perfetto 特征 | 可能见主线程 draw 与栅格化/等待 |
主线程与 RenderThread 都可能耗时或等待 |
| 性能影响 | 绘制工作可能占用主线程 | 绘制任务可交给 RenderThread,但同步点仍可能阻塞主线程 |
上面介绍的是基本的渲染流程,更详细的 Choreographer 原理可以参考 Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程。
接下来我们重点讲解主线程和渲染线程的深入内容:
- 主线程的发展
- 主线程的创建
- 渲染线程的创建
- 主线程和渲染线程的分工
双线程渲染架构的演进
Android 的渲染系统经历了从单线程到双线程的重要演进过程。
单线程时代(Android 5.0 之前)
硬件加速从 Android 3.0(API 11)已支持,早于 Android 5.0 的 HWUI RenderThread。因此“5.0 前”不能等同于全部 CPU 软件绘制;这里仅讨论早期普通 View 路径。
在早期以软件绘制为主的 View 渲染路径中,主线程承担更多 UI 绘制工作:
- 处理用户输入事件
- 执行 measure、layout、draw
- 执行软件绘制并提交 Buffer;其他图形路径可能另有线程。
这种设计的问题:
- 响应性差:主线程负载过重,容易出现 ANR
- 性能瓶颈:主线程绘制工作更容易与输入和业务消息相互挤占
- 帧率不稳定:复杂界面容易导致掉帧
双线程时代(Android 5.0 Lollipop 开始)
Android 5.0 引入了 RenderThread,实现渲染工作的分离:
主线程职责:
- 处理用户输入和业务逻辑
- 执行 View 的 measure、layout、draw
- 构建 DisplayList(绘制指令列表)
- 与渲染线程同步数据
渲染线程职责:
- 接收并处理 DisplayList
- 生成并提交 OpenGL/Vulkan 渲染命令,由 GPU 执行
- 管理纹理和渲染资源
- 与 SurfaceFlinger 交互
这种架构带来的优势:
- 分工:主线程记录绘制命令,RenderThread 处理后续渲染工作。
- 同步边界:主线程在
syncAndDrawFrame的关键同步点仍会等待,不能把两条线程理解为完全独立。 - 并行机会:同步结束后,主线程可以继续处理消息;实际帧节奏取决于 RenderThread、GPU 与 BufferQueue。
主线程的创建过程
普通 Android App 进程通常由 Zygote fork 后专门化,USAP 等路径的准备时机可不同。fork 创建子进程,子进程保留调用线程;进程和主线程不是同一个概念。
frameworks/base/core/jni/com_android_internal_os_Zygote.cpp 中的核心操作可概括为:
1 | pid_t pid = fork(); |
ActivityThread 是运行在应用主线程上的框架对象,不继承 Thread,也不创建这个 Linux 主线程。它的入口准备主 Looper、实例和与系统的连接;H 处理应用生命周期等消息。应用自己的 Handler、Choreographer Handler 和 native FD 工作并不都经 H。
ActivityThread 的创建
下面是 Android 16 普通 Zygote 子进程的一条主路径(省略外围初始化和特殊分支):ZygoteConnection.handleChildProc → ZygoteInit.zygoteInit → RuntimeInit.applicationInit → ActivityThread.main
Android 16 ZygoteConnection.java
1 | private Runnable handleChildProc(ZygoteArguments parsedArgs, FileDescriptor pipeFd, |
对于普通 App 进程,上面会走 zygoteInit 分支并最终进入 ActivityThread.main;childZygoteInit 是子 zygote 的分支,不是常规 App 主路径。
Android 16 ActivityThread.java
1 | public static void main(String[] args) { |
以上是 Android 16 的简化节选。进入 Looper.loop(),主线程就算正式上线,开始分发消息了;正常运行时 main() 不会先返回。Android 17 中,面向 API 37 及以上的 App 使用新的无锁 MessageQueue 实现;Looper、Handler 的使用方式不因此改变,分析队列内部锁竞争时需核对 targetSdkVersion,依赖 MessageQueue 私有字段反射的监控也可能受影响。
ActivityThread 的功能
ActivityThread 的 Handler 负责分发许多应用生命周期消息。组件回调通常从主线程进入,但 Service、Receiver 等后续工作也可能转到其他线程,不能把四大组件的全部执行都算在主线程上。
1 | class H extends Handler { // 仅示意相关消息类型,具体数值以目标分支为准 |
H 不是进程创建者。Activity 生命周期还涉及 EXECUTE_TRANSACTION 等分发;Service/Receiver 生命周期通常在主线程,但远程 Binder 服务方法、Provider 的 query/insert 等请求可在线程池执行,不能把所有组件方法归到 H。
渲染线程的创建和发展
HWUI RenderThread 从 Android 5.0 起承担一部分原来位于 UI 线程的工作。它是 CPU 线程;更早版本已有 GPU 硬件加速,不能把 RenderThread 的引入当成 Android 首次使用 GPU。
软件绘制
硬件加速在 target API 14 及以上默认启用,也可在 Application/Activity 等层级配置。下面属性放在已有 <application> 标签中;Activity 覆盖或其他图形组件仍可能使用硬件路径:
1 | android:hardwareAccelerated="false" |
这会影响普通 View 的绘制路径,不保证进程里完全没有 RenderThread,也不同于单个 View 的软件 layer。下图保留为历史渲染等待示例:它明确出现 RenderThread、DrawFrame 和 dequeueBuffer,不能作为关闭硬件加速后的纯软件绘制证据。

这张旧 Systrace 与开头的设备 Perfetto 图不是同条件测试。线程 slice 还包含等待,不能据其长度推导硬件加速的加速比或纯 CPU 绘制成本。
硬件加速绘制
普通硬件加速 View 路径在主线程记录或更新 RenderNode 中的 DisplayList,再经 syncAndDrawFrame 进入同步。主线程可能早于后续绘制完成被释放,也可能等到 draw/flush 等分支之后;不能保证等待总是短暂。
渲染线程初始化
Android 16 的 RenderProxy 构造函数会取得 RenderThread::getInstance(),该入口按需创建并启动进程内 HWUI 渲染线程。因此首次 draw 中的 initializeIfNeeded() 主要是 Surface/渲染器状态初始化,不等于线程一定在此时才创建。BLAST 更新又在 ViewRootImpl 的另一条路径中,下面两段不表示连续调用顺序。
android/view/ViewRootImpl.java
1 | // ViewRootImpl: renderer/Surface initialization call (surrounding conditions omitted) |
这里创建的 BlastBufferQueue 将在后续的渲染过程中发挥关键作用:
- 在客户端管理 BufferQueue 端点与 SurfaceControl transaction
- 可合并 buffer 与层属性更新;是否减少调用或等待取决于具体路径
- 在 Perfetto 中可观察到 QueuedBuffer 指标的变化
进入绘制时,可调用 ThreadedRenderer.draw;以下为 Android 16 方法:
Android 16 ThreadedRenderer.java
1 | void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) { |
上面的 draw 先更新 DisplayList,之后调用 syncAndDrawFrame 进入关键同步阶段,完成 UI Thread 到 RenderThread 的数据对齐。
UI Thread 与 RenderThread 的 DisplayList 同步机制
在 syncAndDrawFrame 这个关键函数中,发生了以下重要的同步操作:
1 | // android-16.0.0_r1, RenderProxy.cpp |
syncAndDrawFrame 的底层并不是完全非阻塞。Android 16 中 DrawFrameTask::drawFrame() 会执行 postAndWait():先把任务投递到 RenderThread 队列,再在条件变量上等待;RenderThread 在合适的同步点会 unblockUiThread()。因此它是“有等待但尽量早释放 UI”的设计,而不是“UI 完全不等待”。
1 | // frameworks/base/libs/hwui/renderthread/DrawFrameTask.cpp |
run() 中 canUnblockUiThread 为真时在后续 draw 前释放 UI;为假则在后续绘制或 flush/wait 分支之后释放。排队、资源同步和 fence 等待均可能延长 UI 的等待,不是固定时长。参见 Android 16 DrawFrameTask。
具体的同步过程包括:
- RenderNode 树的传递:主线程在 draw 过程中构建的 RenderNode 树(包含 DisplayList)会被传递给 RenderThread
- 属性同步:View 的变换矩阵、透明度、裁剪区域等属性会一并同步
- 资源共享:同步按 HWUI 的 staging/渲染状态与资源生命周期进行,不代表应用可并发修改任意 Path/Paint
- 渲染状态传递:当前帧需要的渲染状态信息传递给 RenderThread
这个同步过程是 Android 硬件加速渲染的核心,它实现了 UI Thread 专注于逻辑处理,RenderThread 专注于渲染的分工模式。
渲染线程的核心实现在 libhwui 库里面,其代码位于 frameworks/base/libs/hwui
RenderThread 与 BlastBufferQueue 的交互流程
RenderThread 收到同步的 DisplayList 后继续执行渲染和提交,并通过 BLASTBufferQueue 与 SurfaceFlinger 交换 Buffer:
1 | // frameworks/base/libs/hwui/renderthread/CanvasContext.cpp |
注:旧版本常见的
dequeueBuffer/queueBuffer/flushTransaction直观流程仍可作为理解模型,但在本篇核对的 Android 16 中,这些细节分布在RenderPipeline/ANativeWindow路径中,CanvasContext::draw()本身已不是旧代码形态。
BlastBufferQueue 的关键特性:
- App 端管理:在这里比较的窗口路径中,BLASTBufferQueue 由客户端创建和管理
- 缓冲区获取:生产者和消费者在 App 侧建立,但没有可用 Buffer 时
dequeueBuffer仍可能等待 - 缓冲区轮转:具体等待时间和高刷新率收益要以目标设备的 Trace 为准
- 异步提交:通过 transaction 机制提交 buffer 和 acquire fence 给 SurfaceFlinger,GPU 工作可能尚未完成
- 支持 unsignaled buffer:配合 SurfaceFlinger 的 unsignaled latch 策略,允许在特定条件下减少端到端延迟
关于 Latching Unsignaled Buffers 的深入探讨
这里讨论的是 App 提交的 Buffer 所带的 acquire fence:它约束 SurfaceFlinger/HWC 何时可以读取 Buffer。present fence 描述的是合成呈现完成时间,不能混用。AOSP 同步文档区分了两者。允许提前 latch 未 signal Buffer 的策略称为「Latching Unsignaled Buffers」。
常规路径:SurfaceFlinger 按 acquire fence 的状态决定何时使用 Buffer;fence signal 前不能读取尚未完成写入的内容。
Latch Unsignaled 路径:满足策略条件时,SurfaceFlinger 可以先 latch acquire fence 尚未 signal 的 Buffer;到实际读取内容前仍须等待 fence。是否降低延迟要看设备实现和场景,不能只凭开关状态推断收益。
控制开关与策略 (Android 13+):
一个典型的策略是 AutoSingleLayer。按 AOSP 文档,Android 13 的默认模式只在一帧中单个 layer 更新、且更新仅包含 Buffer、没有同步事务或几何变化时允许提前 latch。多 layer 或其他事务条件不满足时,不走这条路径。文档说明该配置在启动时读取;具体设备还要核对分支与系统属性,不能当成所有版本通用的实时开关。
分析这类 Trace 时,先核对 acquire fence、layer 更新和 latch 时点;present fence 留给判断合成呈现完成时间。
主线程和渲染线程的分工
普通 View 路径由主线程处理输入、业务、布局和绘制命令记录,RenderThread 组织后续 GPU 工作与提交。主线程也可通过 ViewRootImpl/SurfaceControl 更新层属性和事务,不能用“只有 RenderThread 与 SurfaceFlinger 交互”划界。
RenderNode 包含绘制记录与变换、透明度等属性,DisplayList 是其中的命令记录,并非 RenderNode 的同义词。已有记录可复用,某些属性变化无须重新执行 onDraw;重新布局或内容改变仍可能要求更新。绘制优化必须保持先后顺序、裁剪和混合语义,不能任意把所有文字合成一次绘制。
BlastBufferQueue 的工作原理
这里比较旧式 SurfaceFlinger 消费窗口 BufferQueue 的路径与客户端 BLAST 路径;BufferQueue 还有相机、编解码器等用途,不是所有队列都由 SurfaceFlinger 创建。
Android 15 的常见窗口路径中,ViewRootImpl 管理 BLASTBufferQueue;本地消费者取得 buffer,再与 SurfaceControl transaction 关联提交。同步和合并会影响提交时点,有 buffer/fence 背压时仍可等待。以下 counter 解释按 Android 15 BLASTBufferQueue,不能仅凭使用 BLAST 就声称性能改善。
App 端的 QueuedBuffer 指标
- Perfetto 显示:
QueuedBuffer数值轨道 - AOSP 定义(BLASTBufferQueue):
QueuedBuffer = mNumFrameAvailable + mNumAcquired - mPendingRelease.size() - 解释方式:它是一个“综合状态量”,反映可用帧、已获取帧、待 release 的平衡关系,不建议用固定常量偏移去做一刀切换算
- 实战建议:重点看趋势和持续时间,而不是单个瞬时值

QueuedBuffer 数值变化时机
上升图可见 queueBuffer/onFrameAvailable 与计数更新。Android 15 会更新可用帧等状态后输出组合值;同步、合并和释放分支可能影响变化,不能把每次函数调用机械换算为 +1。

下降图可见 App Binder 线程上的 release 回调与计数下降。该样例支持二者相关,但回调到达不等于 release fence 已完成,也不保证 buffer 可立即重写。BLAST 还可能保留部分 pending release;counter 扣除这些项,不是简单的空闲 buffer 数。

SurfaceFlinger 端的 BufferTX 指标
Android 15 Layer 对应字段是 mPendingBufferTransactions,输出名为 BufferTX - 加 layer 名。收到含有效 buffer 的事务时增加,latch 或 drop 路径减少;只有属性变化的事务不能一概计为 +1,减少也不代表呈现完成。
图中标出的 1、2 是该段观察值;轨道左侧纵轴范围不是通用队列容量或固定最大 3。实际行为还受图层与提交策略影响。

App 端和 SF 端的协作流程
- BLAST 接收并按队列/同步状态取得 App 提交的 buffer。
- SurfaceControl transaction 关联 buffer、frameNumber 和层属性;与 doFrame token 是不同标识,需显式关联。
- SF 更新待处理计数,latch/drop 与后续合成/呈现分别处理。
- release callback/fence 与 BLAST 的 pending-release 管理控制回流,不能只看计数下降认定已上屏或已可复用。
关键性能观察点
counter 的趋势可提示进一步调查的位置,但持续高值可能来自有意的流水线、同步事务、GPU/fence 等待或消费延后;低值也可能只是内容不更新。先对齐 frameNumber/token/Layer,再结合 doFrame、DrawFrames、调度状态、fence 和最终呈现判断。单个数值或生产/消费高低不能唯一归因到 App、GPU 或 SF。
性能
同步数据库访问、锁等待等工作会占用主线程的消息分发时间。阻塞会延后输入和帧回调,但卡顿与 ANR 需要分别判断:
- 卡顿:按该帧期限和最终呈现分析;UI、RenderThread、GPU 可重叠,不能把墙钟耗时相加与固定 8.33 ms 比较。
- ANR:由具体输入/组件超时规则触发,不是任何主线程停顿几秒都必弹框。前后台、系统配置与故障类型影响处置,也不能笼统说“不弹框就 Crash”。详见ANR 系列。
ANR dump 可能晚于现场,不能因为有栈就认定容易定位。用 Perfetto/System Trace 观察等待和调度,再用 Android Studio 的方法采样/插桩工具补充函数证据,注意额外开销。用户遇到卡顿或卡死都不好受,这也是我写这个系列的初衷:先看懂两条线程怎样交接,再去找问题卡在哪一步。
Perfetto 独有的 FrameTimeline 功能
FrameTimeline 提供 App 与 SurfaceFlinger 的预期/实际帧记录,帮助筛选晚帧与延迟,再回到具体线程调查。相对旧 Systrace 查看方式,它提供了可关联的帧记录,也可由 Trace Processor 查询。
需要 Android 12 及以上支持,并实际启用相应数据源。轨道缺失可能是支持范围或采集问题,不能直接当作没有卡顿。
FrameTimeline 的核心概念
根据 Perfetto 官方文档,FrameTimeline 按预期/实际时序等信息分类;分类可含预测误差,不等于每次偏差都有同样的可感知卡顿。对受支持并被采集的帧,可观察两类轨道:

1. Expected Timeline(预期时间线)
- 作用: 显示系统分配给应用的渲染时间窗口
- 开始时间: 调度器预测 Choreographer 回调应开始的时间,不是实际获得 CPU 的时刻
- 含义: 为了避免系统卡顿,应用需要在这个时间范围内完成工作
2. Actual Timeline(实际时间线)
- 作用: 显示应用完成帧的实际时间(包括 GPU 工作)
- 开始时间:
Choreographer#doFrame或AChoreographer_vsyncCallback开始运行的时间 - 结束时间:
max(GPU 时间, Post 时间),其中 Post 时间是帧被提交到 SurfaceFlinger 的时间
点击 Actual Timeline 的切片,可以看到 App 帧的完成时间、jank 类型和 layer 等详情;要判断何时在屏幕呈现,还需沿帧关联线查看 SurfaceFlinger 侧。

颜色编码系统
FrameTimeline 使用直观的颜色来标识不同的帧状态:
| 颜色 | 含义 | 说明 |
|---|---|---|
| 绿色 | 正常帧 | 没有观察到卡顿,理想状态 |
| 浅绿色 | 高延迟状态 | 帧率稳定但帧呈现延迟,导致输入延迟增加 |
| 红色 | 卡顿帧 | 当前进程导致的卡顿 |
| 黄色 | 应用无责任卡顿 | 帧出现卡顿但应用不是原因,SurfaceFlinger 导致的卡顿 |
| 蓝色 | 丢帧 | SF 侧表示跳过该帧、选择更新帧;App 侧也可能表示 UI 状态更新未及时到达 RenderThread |
点击不同颜色的 Actual Timeline 切片,详情区会显示 jank_type 等归类信息;具体阻塞点仍要回到相关线程与 fence:
上面的表描述常见 UI 分类,颜色不是具体代码根因。上方详情示例来自 com.tencent.mm(token 54160848),与前面示例 App 的 token 8227… 不属于同一现场。图中 App Deadline Missed 与 Buffer Stuffing 同时出现,说明标签可组合;不能强行挑一个当唯一根因。
卡顿类型分析
FrameTimeline 可以识别多种卡顿类型:
应用端卡顿
- AppDeadlineMissed: 应用运行时间超过预期
- BufferStuffing:持续提交形成待呈现积压和额外延迟的状态;正常流水线中前帧呈现前准备下一帧,本身不够证明异常
SurfaceFlinger 卡顿
- SurfaceFlingerCpuDeadlineMissed:SF 对应合成路径晚于期限,不是 ANR 超时或纯 CPU Running 时长
- SurfaceFlingerGpuDeadlineMissed:CPU 部分按时,但后续 GPU 合成未及时完成
- DisplayHAL:SF 按时交付后呈现仍错过预期;还需查 HAL 所获预算与实际工作
- PredictionError: 调度器预测偏差
配置 FrameTimeline
这是要加入完整 Perfetto TraceConfig 的 textproto 数据源片段,不是 YAML,也不是含 buffer/时长的独立采集配置;完整示例见采集篇:
1 | data_sources { |
Perfetto 中 Vsync 信号
在这份 Perfetto Trace 中,VSYNC-app 使用 Counter 类型显示,这与很多人的直觉认知不同:
- 0 → 1 的变化:表示一个 Vsync 信号
- 1 → 0 的变化:同样表示一个 Vsync 信号
- 错误理解:很多人误以为只有变成 1 才是 Vsync 信号
正确的 Vsync 信号识别
下图中 1 、2、3、4 的时间点都是 Vsync 信号到达

关键要点:
- 本图的每次数值变化对应一个已记录的 Vsync 标记:无论是 0→1 还是 1→0;这不是所有 Vsync 轨道的通用编码规则
- 信号频率:本例连续通知可测量相邻翻转的间隔;不能只由“支持 120 Hz”推出恒定 8.33 ms,实际模式、软件调度和 ARR 都会影响对应关系
- 多 App 场景:Counter 可能因为其他 App 的申请而保持活跃状态
分析技巧
SF 的 VSYNC-app counter 不够证明目标 App 收到通知。若采集包含 App 端 onVsync/相关插桩,可结合 PID、时间和 doFrame token 核对;没有名为 FrameDisplayEventReceiver.onVsync 的 slice 也可能只是未插桩或未采集。区分通知、队列分发、回调处理与最终呈现。
参考
- https://juejin.im/post/5a9e01c3f265da239d48ce32
- http://www.cocoachina.com/articles/35302
- https://juejin.im/post/5b7767fef265da43803bdc65
- http://gityuan.com/2019/06/15/flutter_ui_draw/
- https://developer.android.google.cn/guide/components/processes-and-threads
附件
本文涉及到的 Perfetto 跟踪文件也上传了,各位下载后可以在 Perfetto UI (https://ui.perfetto.dev/) 中打开分析
关于我 && 博客
下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!
- 博主个人介绍 :里面有个人的微信和微信群链接。
- 本博客内容导航 :个人博客内容的一个导航。
- 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
- Android性能优化知识星球 :历史介绍(已停止运营)
一个人可以走的更快 , 一群人可以走的更远
