本篇是 Perfetto 系列文章的第七篇,主要介绍 Android App 中的 MainThread 和 RenderThread,也就是大家熟悉的主线程和渲染线程。文章会从 Perfetto 的角度来看 MainThread 和 RenderThread 的工作流程,涉及卡顿、软件渲染、掉帧计算等相关知识。
随着 Google 正式推出 Perfetto 工具替代 Systrace,Perfetto 在性能分析领域已经成为主流选择。本文将结合 Perfetto 的具体 trace 信息,帮助读者理解 MainThread 和 RenderThread 的完整工作流程,让你在使用 Perfetto 分析性能问题时能够:
- 准确识别关键 trace tag:知道 UI Thread、RenderThread 等关键线程的作用
- 理解帧渲染的完整流程:从 Vsync 信号到屏幕显示的每个步骤
- 定位性能瓶颈:通过 trace 信息快速找到卡顿和性能问题的根因
本文目录
系列文章目录
- 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 系列 (九) - CPU 信息解读
- 视频(B站) - Android Perfetto 基础和案例分享
如果大家还没看过 Systrace 系列,下面是传送门:
- Systrace 系列目录 : 系统介绍了 Perfetto 的前身 Systrace 的使用,并通过 Systrace 来学习和了解 Android 性能优化和 Android 系统运行的基本规则。
- 个人博客 :个人博客,主要是 Android 相关的内容,也放了一些生活和工作相关的内容。
欢迎大家在 关于我 页面加入微信群或者星球,讨论你的问题、你最想看到的关于 Perfetto 的部分,以及跟各位群友讨论所有 Android 开发相关的内容.
本文使用到的 Trace 文件我上传到了 Github :https://github.com/Gracker/SystraceForBlog/tree/master/Android_Perfetto/demo_app_aosp_scroll.perfetto-trace ,需要的可以自取。
注:本文内容基于 Android 16 的最新渲染架构
基于 Perfetto 的渲染流程分析
这里以滑动列表为例,我们通过 Perfetto 截取主线程和渲染线程一帧的工作流程(每一帧都会遵循这个流程,不过有的帧需要处理的事情多,有的帧需要处理的事情少)。在 Perfetto UI 中,重点观察 “UI Thread” 和 “RenderThread” 这两个线程的活动。
帧的概念和基本参数
在分析 Perfetto trace 之前,需要先了解帧(Frame)的基本概念。Android 系统按照固定的时间间隔刷新屏幕内容:
- 60Hz 设备:每 16.67ms 刷新一次,每秒 60 帧
- 90Hz 设备:每 11.11ms 刷新一次,每秒 90 帧
- 120Hz 设备:每 8.33ms 刷新一次,每秒 120 帧
在 Perfetto 中分析渲染性能时,需要重点关注以下两个线程:
- UI Thread:应用主线程,处理用户输入、业务逻辑、布局计算
- RenderThread:渲染线程,执行 GPU 渲染命令,与 SurfaceFlinger 交互

主线程和渲染线程的工作流程
通过上面的 Perfetto 截图,可以看到一帧完整的渲染流程。我们可以将 Perfetto 图想象成一条河流:主线程在上游处理逻辑,渲染线程在下游执行绘制。河流从左到右流动,每段代表一个步骤。
重要说明:并非每一帧都会执行所有步骤。Input、Animation、Insets Animation 这些回调是基于前一帧的状态决定当前帧是否执行,而 Traversal(measure、layout、draw)是每帧的核心流程。
通过以下描述,试着在脑中”播放”这个完整流程:
1. 主线程等待 Vsync 信号
- Perfetto trace: 主线程处于 Sleep 状态(显示为空闲块)
- 流程说明: 主线程等待垂直同步信号(Vsync)到来,这确保渲染与屏幕刷新率同步,避免画面撕裂
2. Vsync-app 信号传递过程
- Perfetto trace:
vsync-app 相关事件,SurfaceFlinger app 线程活动 - 流程说明: 当硬件产生 Vsync 信号时,首先传递给 SurfaceFlinger。SurfaceFlinger 的 app 线程被唤醒,负责管理和分发 Vsync 信号给需要渲染的应用程序。这个中间层设计允许系统级的 Vsync 调度和优化
重要说明:
- Vsync-app 是按需申请的:只有 App 主动请求时才会收到 vsync-app 信号,不申请就没有
- 多 App 共享机制:同时可能有多个 App 申请 vsync-app 信号
- 信号归属问题:SurfaceFlinger 中的 vsync-app 信号可能是其他 App 申请的,当前分析的 App 如果没有申请,就不会有帧输出,这是正常现象
3. SurfaceFlinger 唤醒 App 主线程
- Perfetto trace:
FrameDisplayEventReceiver.onVsync - 流程说明: SurfaceFlinger 通过 FrameDisplayEventReceiver 机制将 Vsync 信号发送给已注册的 App。App 的 Choreographer 接收到信号后开始启动一帧绘制流程
4. 处理输入事件(Input)
- Perfetto trace:
Input 块 - 流程说明: 仅在有输入事件时才执行,主要处理触摸、滑动等用户交互
- 触发条件:
- 有 Input 回调:手指按压屏幕并滑动时(如列表滑动、页面拖拽)
- 无 Input 回调:手指抬起后的惯性滑动阶段、静止状态
- 注意: Input 回调是由前一帧的用户交互行为决定是否在当前帧执行
5. 处理动画(Animation)
- Perfetto trace:
Animation 块 - 流程说明: 仅在有动画需要更新时才执行,更新动画状态和当前帧的动画值
- 触发条件:
- 有 Animation 回调:惯性滑动阶段、属性动画运行时、列表 item 创建和内容变化、页面转场动画等
- 无 Animation 回调:界面静止状态、纯 Input 交互阶段(无动画效果时)
- 注意: Animation 回调同样由前一帧 post 的回调决定当前帧是否执行
6. 处理 Insets 动画
- Perfetto trace:
Insets Animation 块 - 流程说明: 仅在有窗口插入变化时才执行,处理窗口边界动画
- 触发条件:
- 有 Insets Animation 回调:键盘弹出/收起、状态栏显示/隐藏、导航栏变化等
- 无 Insets Animation 回调:窗口边界稳定状态,大部分普通交互场景
7. Traversal(测量、布局、绘制准备)
- Perfetto trace:
performTraversals, measure, layout, draw - 流程说明: Android UI 渲染的三大核心流程,每一帧都会执行这个完整的流程:
7.1 Measure(测量阶段)
- 作用: 确定每个 View 的尺寸大小
- 过程: 从根 View 开始,递归测量所有子 View 的宽高
- 关键概念:
MeasureSpec:封装了父容器对子 View 的尺寸要求(EXACTLY、AT_MOST、UNSPECIFIED)onMeasure():每个 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 的内容绘制到画布上
- 现代实现: 不直接绘制像素,而是构建 DisplayList(绘制指令列表)
- 关键流程:
draw(Canvas):绘制 View 自身内容onDraw(Canvas):子类重写实现具体绘制逻辑dispatchDraw(Canvas):ViewGroup 用来绘制子 View
- Perfetto 中的表现:
draw 事件,在硬件加速下主要是构建 DisplayList
ViewRootImpl.performTraversals 核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| private void performTraversals() { if (mFirst || windowShouldResize || viewVisibilityChanged || params != null) { performMeasure(childWidthMeasureSpec, childHeightMeasureSpec); } final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw); if (didLayout) { performLayout(lp, mWidth, mHeight); } if (!performRequestedAction(mAttachInfo, mFirst)) { if (mFirst || viewVisibilityChanged) { performDraw(); } } }
|
注:实际 AOSP 源码中 performTraversals 的条件判断更为复杂,会涉及 mFirst、mAdded、mWindowShouldResize 以及各种 dirty 状态标记,但以上代码清晰地展示了其核心的 Measure、Layout、Draw 三阶段流程。
三阶段的执行条件:
- Measure: 当窗口大小变化、首次创建或者视图可见性变化时执行
- Layout: 当布局被请求且应用未停止时执行
- Draw: 当有绘制请求或者是首次绘制、视图可见性变化时执行
8. 同步 DisplayList 到渲染线程
- Perfetto trace: syncAndDrawFrame,可见 “sync” 或 “syncAndDrawFrame” 事件(通常显示为主线程向渲染线程的数据传递点)
- 流程说明: 主线程通过 syncAndDrawFrame 将构建好的 DisplayList(包含 RenderNode 树、视图属性如变换矩阵、透明度和裁剪区域,以及共享资源如纹理)传递给渲染线程。这个同步过程是硬件加速的核心,确保渲染线程获得完整的绘制指令。同步完成后,主线程立即释放资源(可继续处理其他消息、IdleHandler 或进入 Sleep 等待下一个 Vsync),而渲染线程独立接管后续的 GPU 渲染工作。
9. 渲染线程获取 Buffer
- Perfetto trace:
dequeueBuffer 块 - 流程说明: 渲染线程从 BlastBufferQueue(App 端管理的缓冲队列)通过 dequeueBuffer 获取一个可用缓冲区,作为渲染目标(framebuffer)。BlastBufferQueue 采用生产者-消费者模型,预先管理缓冲池以减少等待时间;如果无可用 Buffer,可能短暂等待或触发新 Buffer 创建。
10. 处理渲染指令并 flush 到 GPU
Perfetto trace: drawing 相关块
- 流程说明: RenderThread(运行在 CPU 上)通过 HardwareRenderer 和 CanvasContext 处理从 UI thread 同步过来的 DisplayList,调用 OpenGL ES 或 Vulkan API 来准备绘制命令序列(如渲染视图的几何形状、纹理、着色效果)。这些命令被构建成命令缓冲区(command buffer),然后通过 flush 操作(例如 mRenderPipeline->flush())提交到 GPU 驱动,同时生成 present fence 用于后续同步。GPU 异步执行这些指令,实际生成图像内容。
11. 提交 Buffer(可能 unsignaled)
- Perfetto trace: queueBuffer(可观察 acquireFence 状态)
- 流程说明: RenderThread 通过 queueBuffer 将渲染完成的 Buffer 提交回 BlastBufferQueue,此时 Buffer 可能携带 unsignaled 的 acquire fence(即 GPU 渲染命令尚未完全执行完毕)。这种异步提交机制有助于减少整体渲染延迟。
12. 触发 Transaction 到 SurfaceFlinger
- Perfetto trace: TransactionQueue 或 BLAST transaction 事件 ,一般在 queueBuffer 之后,有些 Trace 没有这个 Tag
- 流程说明: 在 queueBuffer 完成后,RenderThread 通过 applyPendingTransactions 将积累的 Transaction(包括 Buffer 更新、层属性变化等)批量发送给 SurfaceFlinger。SurfaceFlinger 处理这些 Transaction,根据 LatchUnsignaledConfig 策略(例如 AutoSingleLayer 配置)检查并可能 latch unsignaled buffer 以进一步优化延迟;如果配置禁用 unsignaled latch,则等待 fence signaled 确保 Buffer 就绪。随后,SurfaceFlinger 执行层合成(composite)并将最终图像显示到屏幕。
在 Perfetto 中识别不同的渲染模式:
- 手指滑动时:每帧都有
Input → Traversal → RenderThread 的完整链路 - 惯性滑动时:每帧都有
Animation → Traversal → RenderThread,没有 Input - 静止状态时:偶尔出现
Animation → Traversal → RenderThread,没有 Input
软件绘制 vs 硬件加速
虽然现在基本都使用硬件加速渲染,但了解两种渲染模式的区别仍然有助于理解 Perfetto trace:
| 方面 | 软件绘制 | 硬件加速 |
|---|
| 绘制线程 | 主线程 | RenderThread |
| 绘制引擎 | Skia (CPU) | OpenGL/Vulkan (GPU) |
| Perfetto 特征 | 主线程有大块 draw 事件 | 主线程快速完成,RenderThread 处理绘制 |
| 性能影响 | 可能阻塞主线程 | 异步渲染,性能更好 |
上面介绍的是基本的渲染流程,更详细的 Choreographer 原理可以参考 Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程。
接下来我们重点讲解主线程和渲染线程的深入内容:
- 主线程的发展
- 主线程的创建
- 渲染线程的创建
- 主线程和渲染线程的分工
双线程渲染架构的演进
Android 的渲染系统经历了从单线程到双线程的重要演进过程。
单线程时代(Android 4.4 之前)
在早期的 Android 版本中,所有的 UI 相关工作都在主线程中执行:
- 处理用户输入事件
- 执行 measure、layout、draw
- 调用 OpenGL 进行实际绘制
- 与 SurfaceFlinger 交互
这种设计的问题:
- 响应性差:主线程负载过重,容易出现 ANR
- 性能瓶颈:CPU 和 GPU 无法并行工作
- 帧率不稳定:复杂界面容易导致掉帧
双线程时代(Android 5.0 Lollipop 开始)
Android 5.0 引入了 RenderThread,实现渲染工作的分离:
主线程职责:
- 处理用户输入和业务逻辑
- 执行 View 的 measure、layout、draw
- 构建 DisplayList(绘制指令列表)
- 与渲染线程同步数据
渲染线程职责:
- 接收并处理 DisplayList
- 执行 OpenGL/Vulkan 渲染命令
- 管理纹理和渲染资源
- 与 SurfaceFlinger 交互
这种架构带来的优势:
- 并行处理:主线程可以在渲染线程工作时处理下一帧
- 响应性提升:主线程不再被渲染阻塞
- 性能优化:GPU 资源得到更好利用
主线程的创建过程
Android App 的进程是基于 Linux 的,其管理也是基于 Linux 的进程管理机制,所以其创建也是调用了 fork 函数
frameworks/base/core/jni/com_android_internal_os_Zygote.cpp
Fork 出来的进程,我们这里可以把他看做主线程,但是这个线程还没有和 Android 进行连接,所以无法处理 Android App 的 Message ;由于 Android App 线程运行基于消息机制 ,那么这个 Fork 出来的主线程需要和 Android 的 Message 消息绑定,才能处理 Android App 的各种 Message
这里就引入了 ActivityThread ,确切的说,ActivityThread 应该起名叫 ProcessThread 更贴切一些。ActivityThread 连接了 Fork 出来的进程和 App 的 Message ,他们的通力配合组成了我们熟知的 Android App 主线程。所以说 ActivityThread 其实并不是一个 Thread,而是他初始化了 Message 机制所需要的 MessageQueue、Looper、Handler ,而且其 Handler 负责处理大部分 Message 消息,所以我们习惯上觉得 ActivityThread 是主线程,其实他只是主线程的一个逻辑处理单元。
ActivityThread 的创建
App 进程 fork 出来之后,回到 App 进程,查找 ActivityThread 的 Main函数
com/android/internal/os/ZygoteInit.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| static final Runnable childZygoteInit( int targetSdkVersion, String[] argv, ClassLoader classLoader) { RuntimeInit.Arguments args = new RuntimeInit.Arguments(argv); if (args.niceName != null) { Process.setArgV0(args.niceName); } if (args.invokeWith != null) { WrapperInit.execApplication(args.invokeWith, args.niceName, args.targetSdkVersion, VMRuntime.getCurrentInstructionSet(), null, args.remainingArgs); throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned"); } else { ZygoteInit.zygoteInit(args.targetSdkVersion, args.disabledCompatChanges, args.remainingArgs, classLoader); } return RuntimeInit.findStaticMain(args.startClass, args.startArgs, classLoader); }
|
这里的 startClass 就是 ActivityThread,找到之后调用,逻辑就到了 ActivityThread的main函数
android/app/ActivityThread.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| public static void main(String[] args) { Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain"); Looper.prepareMainLooper(); long startSeq = 0; if (args != null) { for (int i = args.length - 1; i >= 0; --i) { if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) { startSeq = Long.parseLong( args[i].substring(PROC_START_SEQ_IDENT.length())); } } } ActivityThread thread = new ActivityThread(); thread.attach(false, startSeq); if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); } Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); Looper.loop(); throw new RuntimeException("Main thread loop unexpectedly exited"); }
|
注释里面都很清楚,这里就不详细说了,main 函数处理完成之后,主线程就算是正式上线开始工作.
ActivityThread 的功能
另外我们经常说的,Android 四大组件都是运行在主线程上的,其实这里也很好理解,看一下 ActivityThread 的 Handler 的 Message 就知道了
1 2 3 4 5 6 7
| class H extends Handler { public static final int BIND_APPLICATION = 110; public static final int CREATE_SERVICE = 114; public static final int BIND_SERVICE = 121; public static final int RECEIVER = 113; }
|
可以看到,进程创建、Activity 启动、Service 的管理、Receiver 的管理、Provider 的管理这些都会在这里处理,然后进到具体的 handleXXX
渲染线程的创建和发展
主线程讲完了我们来讲渲染线程,渲染线程也就是 RenderThread ,最初的 Android 版本里面是没有渲染线程的,渲染工作都是在主线程完成,使用的也都是 CPU ,调用的是 libSkia 这个库,RenderThread 是在 Android Lollipop 中新加入的组件,负责承担一部分之前主线程的渲染工作,减轻主线程的负担
软件绘制
我们一般提到的硬件加速,指的就是 GPU 加速,这里可以理解为用 RenderThread 调用 GPU 来进行渲染加速 。 硬件加速在目前的 Android 中是默认开启的, 所以如果我们什么都不设置,那么我们的进程默认都会有主线程和渲染线程(有可见的内容)。我们如果在 App 的 AndroidManifest 里面,在 Application 标签里面加一个
1
| android:hardwareAccelerated="false"
|
我们就可以关闭硬件加速,系统检测到你这个 App 关闭了硬件加速,就不会初始化 RenderThread ,直接 cpu 调用 libSkia 来进行渲染。其 Trace 跟踪表现如下(资源比较老,用 Systrace 图示)

与这篇文章开头开启硬件加速的 Perfetto 图对比,可以看到主线程由于要进行渲染工作,所以执行的时间变长了,也更容易出现卡顿,同时帧与帧之间的空闲间隔也变短了,使得其他 Message 的执行时间被压缩。在 Perfetto 中,这种差异通过线程活动的时间长度和密集程度可以清晰地观察到。
硬件加速绘制
正常情况下,硬件加速是开启的,主线程的 draw 函数并没有真正的执行 drawCall ,而是把要 draw 的内容记录到 DisplayList 里面,通过 syncAndDrawFrame 将 DisplayList 同步到 RenderThread 中,一旦同步完成,主线程就可以被释放出来做其他的事情,RenderThread 则继续进行渲染工作。

渲染线程初始化
渲染线程初始化在真正需要 draw 内容的时候,一般我们启动一个 Activity ,在第一个 draw 执行的时候,会去检测渲染线程是否初始化,如果没有则去进行初始化
android/view/ViewRootImpl.java
1 2 3 4 5 6 7 8 9 10 11 12
| mAttachInfo.mThreadedRenderer.initializeIfNeeded( mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);
if (mBlastBufferQueue == null) { mBlastBufferQueue = new BLASTBufferQueue(mTag, mSurfaceControl, mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format); mBlastBufferQueue.update(mSurfaceControl, mSurfaceSize.x, mSurfaceSize.y, mWindowAttributes.format); }
|
这里创建的 BlastBufferQueue 将在后续的渲染过程中发挥关键作用:
- 为 RenderThread 提供高效的 Buffer 管理
- 支持批量 Transaction 提交,减少与 SurfaceFlinger 的交互开销
- 在 Perfetto 中可观察到 QueuedBuffer 指标的变化
后续直接调用 draw
android/graphics/HardwareRenderer.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| mAttachInfo.mThreadedRenderer.draw(mView, mAttachInfo, this);
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks) { final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer; choreographer.mFrameInfo.markDrawStart();
updateRootDisplayList(view, callbacks);
if (attachInfo.mPendingAnimatingRenderNodes != null) { final int count = attachInfo.mPendingAnimatingRenderNodes.size(); for (int i = 0; i < count; i++) { registerAnimatingRenderNode( attachInfo.mPendingAnimatingRenderNodes.get(i)); } attachInfo.mPendingAnimatingRenderNodes.clear(); attachInfo.mPendingAnimatingRenderNodes = null; }
int syncResult = syncAndDrawFrame(choreographer.mFrameInfo); if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) { setEnabled(false); attachInfo.mViewRootImpl.mSurface.release(); attachInfo.mViewRootImpl.invalidate(); } if ((syncResult & SYNC_REDRAW_REQUESTED) != 0) { attachInfo.mViewRootImpl.invalidate(); } }
|
上面的 draw 只是更新 DisplayList ,更新结束后,调用 syncAndDrawFrame ,通知渲染线程开始工作,主线程释放。在 syncAndDrawFrame 中完成了关键的 UI Thread 到 RenderThread 的数据同步过程。
UI Thread 与 RenderThread 的 DisplayList 同步机制
在 syncAndDrawFrame 这个关键函数中,发生了以下重要的同步操作:
1 2 3 4 5 6
| int RenderProxy::syncAndDrawFrame() { return mDrawFrameTask.drawFrame(); }
|
syncAndDrawFrame 的调用并非一个阻塞的同步过程,而是 UI 线程创建并派发一个 DrawFrameTask 到 RenderThread 的任务队列。这个 Task 封装了渲染这一帧所需的所有信息(主要是 RenderNode 树)。派发后,UI 线程就可以从渲染工作中解脱出来,处理其他事务。RenderThread 在自己的 Looper 中取出这个 Task 并执行,从而实现了两个线程的并行工作。
具体的同步过程包括:
- RenderNode 树的传递:主线程在 draw 过程中构建的 RenderNode 树(包含 DisplayList)会被传递给 RenderThread
- 属性同步:View 的变换矩阵、透明度、裁剪区域等属性会一并同步
- 资源共享:纹理、Path、Paint 等绘制资源在两个线程之间建立共享机制
- 渲染状态传递:当前帧需要的渲染状态信息传递给 RenderThread
这个同步过程是 Android 硬件加速渲染的核心,它实现了 UI Thread 专注于逻辑处理,RenderThread 专注于渲染的分工模式。
渲染线程的核心实现在 libhwui 库里面,其代码位于 frameworks/base/libs/hwui
RenderThread 与 BlastBufferQueue 的交互流程
RenderThread 接收到同步的 DisplayList 后,开始真正的渲染工作,这个过程中会与 BlastBufferQueue 进行密切的交互:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
|
void CanvasContext::draw() { SkRect dirty; mDamageAccumulator.finish(&dirty); ANativeWindowBuffer* buffer; int fenceFd; status_t result = mNativeSurface->dequeueBuffer(&buffer, &fenceFd); if (result != OK) { ALOGW("Failed to dequeue buffer: %s (%d)", strerror(-result), result); return; } mRenderPipeline->onDequeueBuffer(buffer); bool drew = mRenderPipeline->draw(frame, windowDirty, dirty, mLightGeometry, &mLayerUpdateQueue, mContentDrawBounds, mOpaque, mLightInfo, mRenderNodes, &(profiler())); if (drew) { int presentFence = mRenderPipeline->flush(); result = mNativeSurface->queueBuffer(buffer, presentFence); if (mBlastBufferQueue != nullptr) { mBlastBufferQueue->flushTransaction(); } } else { mNativeSurface->cancelBuffer(buffer, fenceFd); } }
|
BlastBufferQueue 的关键特性:
- App 端管理:不同于传统的 BufferQueue 由 SurfaceFlinger 创建,BlastBufferQueue 是由 App 端创建和管理
- 减少同步等待:通过生产者-消费者模型,减少了 RenderThread 在 dequeueBuffer 时的等待时间
- 高效的缓冲区轮转:支持更智能的缓冲区管理策略,特别适配高刷新率显示器
- 异步提交:通过 transaction 机制异步地将完成的帧提交给 SurfaceFlinger
- 支持 unsignaled buffer:配合 SurfaceFlinger 的
latch_unsignaled 策略,允许提交 GPU 尚未完成的 Buffer,进一步减少渲染延迟
关于 Latching Unsignaled Buffers 的深入探讨
现代 Android 系统对 presentFence 的处理有精细的控制,并非总是等待。这个机制被称为 **”Latching Unsignaled Buffers”**(捕获未就绪的缓冲区)。
传统模式: SurfaceFlinger 必须等待 App 的 presentFence 被 GPU signal 后,才能 “latch” (捕获) 这个 Buffer 进行合成。这保证了安全性,但增加了延迟。
Latch Unsignaled 模式: 在此模式下,SurfaceFlinger 可以立即 latch 一个 GPU 尚未完成渲染的 Buffer(即 fence 未 signaled),并提前开始部分合成工作。当它需要真正使用这个 Buffer 的内容时,它才会在内部等待 presentFence。这通过流水线化进一步隐藏了 GPU 渲染的延迟,对降低游戏、视频等全屏应用的输入延迟至关重要。
控制开关与策略 (Android 13+):
这个行为可以通过系统属性 debug.sf.latch_unsignaled 进行全局调试,但更重要的是,它由一个名为 LatchUnsignaledConfig 的分层策略控制。一个典型的策略是 AutoSingleLayer:
- 当屏幕上只有单个图层更新时(如全屏游戏或视频),系统会自动启用 Latch Unsignaled 模式,因为此时没有复杂的图层依赖,风险最低,收益最大。
- 当有多个图层更新时,系统会回退到更安全的传统等待模式,以避免潜在的视觉错误。
因此,SurfaceFlinger 并非总是盲目等待 presentFence,而是根据精密的策略来决定是否“抢跑”,以在稳定性和极致性能之间取得平衡。
主线程和渲染线程的分工
主线程负责处理进程 Message、处理 Input 事件、处理 Animation 逻辑、处理 Measure、Layout、Draw ,更新 DisplayList ,但是不涉及与 SurfaceFlinger 直接打交道;渲染线程负责渲染相关的工作,包括与 BlastBufferQueue 的交互、GPU 渲染命令的执行,以及与 SurfaceFlinger 的最终交互。
当启动硬件加速后,在 Measure、Layout、Draw 的 Draw 这个环节,Android 使用 DisplayList 进行绘制而非直接使用 CPU 绘制每一帧。DisplayList 是一系列绘制操作的记录,抽象为 RenderNode 类,这样间接的进行绘制操作的优点如下
- DisplayList 可以按需多次绘制而无须同业务逻辑交互
- 特定的绘制操作(如 translation、scale 等)可以作用于整个 DisplayList 而无须重新分发绘制操作
- 当知晓了所有绘制操作后,可以针对其进行优化:例如,所有的文本可以一起进行绘制一次
- 可以将对 DisplayList 的处理转移至另一个线程(也就是 RenderThread)
- 主线程在 sync 结束后可以