本文是 Systrace 系列文章的第九篇,主要是是介绍 Android App 中的 MainThread 和 RenderThread,也就是大家熟悉的主线程和渲染线程。文章会从 Systrace 的角度来看 MainThread 和 RenderThread 的工作流程,以及涉及到的相关知识:卡顿、软件渲染、掉帧计算等
本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些
本文目录
系列文章目录
- Systrace 简介
- Systrace 基础知识 - Systrace 预备知识
- Systrace 基础知识 - Why 60 fps ?
- Systrace 基础知识 - SystemServer 解读
- Systrace 基础知识 - SurfaceFlinger 解读
- Systrace 基础知识 - Input 解读
- Systrace 基础知识 - Vsync 解读
- Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
- Systrace 基础知识 - MainThread 和 RenderThread 解读
- Systrace 基础知识 - Binder 和锁竞争解读
- Systrace 基础知识 - Triple Buffer 解读
- Systrace 基础知识 - CPU Info 解读
- Systrace 流畅性实战 1 :了解卡顿原理
- Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
- Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
- Systrace 响应速度实战 1 :了解响应速度原理
- Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
- Systrace 响应速度实战 3 :响应速度延伸知识
正文
版本说明(2026-10-13 核查):原文和截图来自 2019 年,设备系统及 Flutter SDK 的完整版本未记录。下面用 AOSP
android-9.0.0_r1/android-10.0.0_r1解释当时的 View 渲染与进程入口,并对照android-16.0.0_r1的 HWUI 同步逻辑。Flutter 的旧线程图、Skia 描述已不能直接代表新版本,相关章节补充了 Flutter 3.27、3.29 的变化。
这篇文章和 Perfetto 系列里的 Android App 基于 Choreographer 的渲染流程 / MainThread 和 RenderThread 解读 是同一个主题。旧 Systrace 文章强调一帧里 UI Thread、RenderThread、SurfaceFlinger 的相对位置;新 Perfetto 文章会把同一套流程放到线程轨道、FrameTimeline、CPU 调度和 Buffer 流转里看。
分析卡顿时,先确定目标帧率、刷新率和截止时间,再沿着主线程、RenderThread、GPU 到显示的依赖查下去。doFrame 的 Input / Animation / Insets Animation / Traversal / Commit 是不同阶段,并非每帧都有工作;CPU 调度、Binder、锁和 Buffer 等待也可能让帧错过截止时间。
这里以硬件加速的 View 列表滑动为例,截取其中一帧,重点看 UI Thread 和 RenderThread 两行。这是一次具体执行,并非游戏、软件绘制和所有 View 帧都必须走完的固定流程。

这张图可以沿以下顺序阅读:
- 主线程暂时没有可执行工作,处于等待状态;它也会被输入或其他事件唤醒,并非只等 Vsync。
- 请求过帧回调后,Vsync 事件进入
FrameDisplayEventReceiver.onVsync;它向所属 Looper 投递异步消息,随后执行run()/doFrame(),不是在收到信号的一刻就完成绘制。 - 执行到期的 Input 回调;输入也有不走这一批处理阶段的路径。
- 执行到期的 Animation 回调。Android 10/16 在 Traversal 前还有 Insets Animation 阶段,图中没有单独展示。
- Traversal 按需执行 Measure、Layout、Draw。更新 View 绘制记录时,主线程会与 RenderThread 同步;
doFrame后面还有 Commit 阶段,不能从这张局部图判断其一定有回调。 - 主线程在同步调用中等待 RenderThread 放行,随后才能继续。满足条件时它可以与后续渲染并行;
IdleHandler是否执行取决于消息队列的空闲条件,不是每帧结束都自动执行。 - 图中 RenderThread 取得 Buffer、处理绘制记录并通过 GL/EGL 提交工作。
queueBuffer交出 Buffer 及相关 fence,不代表 GPU 已完成,也不代表屏幕已显示。SurfaceFlinger 还要按调度、Buffer 就绪状态和合成策略处理这一层。
图中“绘制好的 Buffer”等标注是旧流程的简写。新版本的 HWUI 可使用不同图形后端,Android 16 的 View Surface 路径还涉及 BLASTBufferQueue;不能把旧图的函数名和队列位置照搬到所有设备。CPU 线程提交、GPU 完成、合成和 present 要分开看。
上面的 Choreographer 部分可配合 Android 基于 Choreographer 的渲染机制详解 阅读。下面主要补充主线程的创建、RenderThread 的发展和分工,以及游戏、Flutter 的不同做法。
主线程的创建
Android 的普通 App 进程由 Zygote 派生,底层涉及 fork;进程和线程是不同的对象。子进程中的初始执行线程继续完成运行时初始化,再进入 App 的主入口。不能把“fork 出来的进程”直接等同于一个 Java Thread 对象。
这里引入 ActivityThread:它并不继承 Thread,而是负责 App 主线程上框架事务的管理。ActivityThread.main() 初始化主 Looper,并创建 ActivityThread 及其 Handler H。主线程还可运行其他 Handler 和 native Looper 回调,不能把所有 App 消息都算到 H 上。
ActivityThread 的创建
以 Android 10 的 ZygoteConnection 普通 App 分支为例,路径是 ZygoteInit.zygoteInit() → RuntimeInit.applicationInit() → findStaticMain(),再调用 android.app.ActivityThread.main()。原文引用的 childZygoteInit() 是创建子 Zygote 的专用分支,不是普通 App 的入口。
下面是 ZygoteInit.java(android-10.0.0_r1) 中的完整方法;findStaticMain() 则位于同版本 RuntimeInit.java。
1 | public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, |
接着看同版本 ActivityThread.main()。这里保留 startSeq 的解析,避免原节选中变量没有定义;这些都是 framework 内部代码,不是可以复制进普通 App 的独立示例。
1 | public static void main(String[] args) { |
attach() 通过 AMS 的 attachApplication 注册 App 端接口,后续再处理应用绑定等事务。进入 Looper.loop(),主线程这就算正式上线、持续分派工作了;并不是等 main() 返回后才开始运行。源码最后的异常也说明,主 Looper 意外退出并不是正常启动完成。

ActivityThread 的功能
Activity 生命周期、Service 的常见生命周期回调、默认广播接收等,通常由各自进程的主线程处理。Android 10 的 H 中可以看到以下内部消息号(节选,不是稳定的 SDK 接口):
1 | public static final int BIND_APPLICATION = 110; |
Activity 的客户端事务还会经 EXECUTE_TRANSACTION / TransactionExecutor 处理。进程是先创建再执行这些消息,不能说 H 创建了进程。也不能从消息列表推出“四大组件的所有方法都在主线程”:例如远程调用 Service 的 Binder 方法,以及 ContentProvider 的跨进程查询,会进入 Binder 线程池;广播也可在注册时指定 Handler。具体线程取决于调用方式,见 Android 进程与线程说明。

渲染线程的创建和发展
RenderThread 在 Android 5.0(API 21)引入 HWUI,承担一部分原来由主线程执行的渲染工作。但 View 的 GPU 硬件加速从 Android 3.0(API 11)就已存在,不能把所有 Lollipop 之前的版本都说成纯 CPU 绘制。
软件绘制
对于 View 窗口,硬件加速默认与 targetSdkVersion >= 14 有关,也可以在 Application、Activity 等层级配置;不同层级的开关能力并不相同。下面是 <application> 标签中的一个属性片段,关闭应用默认硬件加速,Activity 仍可能覆盖它:
1 | android:hardwareAccelerated="false" |
软件 View 绘制由 CPU 栅格化,并通过窗口 Surface 提交内容。它不需要为这个窗口走通常的 HWUI RenderThread 绘制路径,但不能据此保证整个进程都没有 RenderThread:其他窗口、嵌入组件可能仍使用 HWUI。单个 View 的软件 layer 也不等于关闭整个窗口的硬件加速。绘制时判断当前 Canvas,应看 Canvas.isHardwareAccelerated();配置范围见 硬件加速文档。

这张旧图显示主线程上的 lockAsync、绘制和 Buffer 提交,选中区间为 21.064 ms,其中还包括 fence 等待。它说明这次窗口的软件路径,不能把整段墙钟时间都算成 CPU 绘图,更不能单靠两张不同采样图量化“关闭加速必然慢多少”。额外的主线程工作或等待,会压缩处理其他消息的时间。
硬件加速绘制
硬件加速 View 的 Draw 阶段主要记录或复用 DisplayList,再与 RenderThread 同步;View.onDraw() 等记录过程本身仍消耗主线程时间。RenderThread 可以在主线程获准继续之后处理剩余工作,但获准继续的时刻有条件,见下面的同步逻辑。

渲染线程初始化
要区分 创建线程 与 为窗口初始化 Surface。Android 9 的 ThreadedRenderer 构造过程创建 native RenderProxy,后者取得 RenderThread::getInstance();因此 RenderThread 可以早于第一次 Draw 就已创建。ViewRootImpl 中的 initializeIfNeeded() 主要涉及 renderer / Surface 的初始化,不能把它直接当作线程诞生的唯一位置。
下面是 ThreadedRenderer.java(android-9.0.0_r1) 的完整 draw()。原文将方法归到 android/graphics/HardwareRenderer.java 并混用了不同版本签名,这里统一为同一标签:
1 | void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks, |
它更新 DisplayList、注册动画节点和回调,再经 JNI 进入 RenderProxy.cpp:
1 | int RenderProxy::syncAndDrawFrame() { |
这个调用不是“发个通知就立即返回”。DrawFrameTask.cpp(Android 9) 的 postAndWait() 向 RenderThread 投递任务后等待条件变量。RenderThread 的 syncFrameState() 决定是否能提前放行 UI 线程;若不能,放行会推迟到后面的绘制或 fence 等待之后。Android 16 同文件 仍保留这一条件分支。因此看到主线程的同步区间很长,还应检查 RenderThread 在等待什么。

图中的 dequeueBuffer、flush commands、eglSwapBuffersWithDamageKHR 是这次 GL/EGL 路径的标记。它们不是全部后端通用的函数列表,结束位置也不直接等于 GPU 完成或 present。
主线程和渲染线程的分工
在普通硬件加速 View 管线中,主线程处理消息、输入、常见动画与布局,按需更新绘制记录;RenderThread 同步 RenderNode 树并驱动后端渲染。两者都是 CPU 线程,后端再把相关工作提交给 GPU,不能把 RenderThread 的墙钟时间直接当 GPU 时间。主线程也可能通过窗口或 SurfaceControl 操作与系统图形服务交互,“主线程不与 SurfaceFlinger 打交道”不是通用边界。
DisplayList 是绘制操作的记录;RenderNode 还包含变换等属性,二者不是同一个概念。这种分工有几个好处:
- 内容未变时,可以复用已有绘制记录,避免重复执行 View 的记录逻辑。
- translation、scale 等节点属性变化,可以在适用条件下复用内容;内容本身变化仍可能需要重新记录。
- 后端可以在保持绘制顺序、裁剪、混合等语义的前提下优化,不能保证“所有文本一起画一次”。
- RenderThread 可以处理渲染工作和部分支持的动画,但业务逻辑、普通 View 更新仍需要主线程配合。
- 同步状态允许时,两线程可以并行推进;主线程并不是永远不等 RenderThread。
原参考 RenderThread 文章 可作为历史阅读材料,具体分支以文中版本化的 AOSP 源码为准。
游戏的主线程与渲染线程
游戏引擎常通过自己的 Surface 和渲染管线提交 Buffer,不一定使用 View 的 HWUI 绘制路径。Android 主线程仍负责生命周期、窗口、输入和一些插件调用;游戏逻辑、渲染提交和工作线程如何分工,要看具体引擎与配置。
下面保留原文标作 30 fps 的王者荣耀案例。它是当时的采样,不是今日游戏的帧率规格;游戏出帧频率也不能直接当作屏幕刷新率或 SurfaceFlinger 的运行频率。

图里可以区分 Android 的 UI thread 与 UnityMain。线程名和时序能帮助定位,但不能只凭 UnityMain 就认定它只做渲染,或整个游戏的绝大部分逻辑都在此线程。下面的方法 Trace 显示了 Android 主线程分发输入、进入游戏 View 的路径;它没有完整展示后续引擎任务、GPU 执行和最终显示,也不能单凭前后相邻证明某个输入导致了某一帧。

Flutter 的主线程和渲染线程
下面是 2019 年 Flutter 的旧 Trace:Android 平台主线程之外,还能看到 .ui 和 .gpu。这里的 .gpu 是执行 raster 工作的 CPU 线程,不是硬件 GPU。Flutter 内容由自己的 Engine 管线处理,不走普通 View 内容的 HWUI 绘制路径;原因不是“用了 Skia”,因为 Android HWUI 也使用 Skia。混合原生 View 的 App 仍可能同时存在 HWUI RenderThread。

线程图已过期:从 Flutter 3.29 起,Android / iOS 默认合并 UI 与 platform 线程,Dart 的 UI 工作运行在平台线程上。 Raster 仍是另一类工作,不能理解成整个引擎都合为一个线程。分析新 Trace 时,不应要求一定存在旧图那样独立的 .ui 轨道;平台线程上的耗时插件调用也会影响同线程的 Dart UI 工作。见 Flutter 架构说明。
Flutter 在 Android 上仍依赖平台提供窗口、输入和帧调度。旧方法图中类名是 VsyncWaiter,原文写成了 VsyncView。在 Flutter 3.29.3 的 VsyncWaiter.java 中,也能看到 Choreographer.postFrameCallback(),回调通过 flutterJNI.onVsync() 把时序传给引擎。

Flutter 把渲染引擎随应用交付,可以随 SDK 升级,而不用等待 Android 系统升级;这不等于每个应用自动使用最新 Skia,也不直接证明某个页面比原生快。下面的演讲截图解释的是当时的 Skia 架构,性能仍须结合场景测量。

渲染后端也有版本变化:Flutter 3.27 起,Impeller 成为 Android API 29+ 的默认渲染引擎。 官方说明中,较低 Android 版本或不支持 Vulkan 的设备回退到旧 OpenGL renderer。因此不能继续用“Flutter Engine 一律 Skia → OpenGL”概括设备行为;应记录 Flutter 版本、系统版本和实际后端。见 Impeller 文档。
原图中的 Framework / Engine 分层仍适合入门:Dart framework 负责构建、布局、绘制记录等,Engine 负责把场景栅格化并提交。图里 Engine 只有 Skia 的画法则需要按上面的版本说明理解,不是新版本的完整架构图。

更新 UI 时,framework 请求帧,平台帧回调驱动动画和构建、布局、绘制等阶段,再将场景交给 raster 管线。不是每个 Layer 都必定生成一张新纹理,提交 GPU 也不是直接显示到屏幕:Android 还需后续 Buffer、fence 和系统合成流程。

最后这张图同样是历史示意:GPU Thread 指 raster CPU 线程;顶部 GPU → Vsync 的箭头不能理解为 Flutter 直接等 GPU 完成回调来开始一帧;下方已写了 GL 或 Vulkan,也不是只有 OpenGL。阅读新版本时,需同时替换线程合并和 Impeller 这两处假设。
性能
主线程上的耗时查询、计算或同步等待,会推迟输入和绘制处理。性能分析时,要分清两个问题:
- 卡顿:60 / 90 / 120 Hz 的刷新间隔约为 16.67 / 11.11 / 8.33 ms,但帧是否错过显示时机,要看目标帧率和实际截止时间。主线程与 RenderThread 有同步和并行,不能把两段墙钟时间简单相加判定掉帧;GPU 或系统合成也可能延迟结果。Android 12(API 31)起,在采集到相应数据时,可结合 Perfetto FrameTimeline 查看预期与实际时序。
- ANR:由输入、广播、Service 等各自的超时规则触发,不是任意消息执行几秒就必定弹窗;是否显示对话框也受场景和系统策略影响。先看具体 ANR 类型、日志与阻塞依赖,见 诊断 ANR。
这两种情况都是用户不愿意看到的,也是开发者需要持续解决的问题。ANR 栈有帮助,但采样可能晚于真正的阻塞,不能保证一眼定位;间歇性卡顿还要结合系统 Trace,必要时补方法采样。TraceView 等方法插桩也会扰动耗时,不能把其时间直接当无插桩性能。理解主线程与渲染线程的关系,正是这个系列的初衷。
另外关于卡顿,可以参考下面三篇文章。App 卡顿也可能涉及系统调度或资源竞争,先用证据找出延迟发生在哪一段。
参考
- 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
附件
原仓库目录仍包含王者荣耀、Flutter 的 Systrace HTML 压缩包和对应方法 Trace。两份 ZIP 可解压读取,旧 HTML 查看器在现代 Chrome 中的兼容性未验证;请按文件格式选择支持 legacy Systrace 或 ART 方法 Trace 的工具,不要把 .trace 方法文件当 Perfetto 系统 Trace 直接解释。
本文其他地址
由于博客留言交流不方便,点赞或者交流,可以移步本文的知乎或者掘金页面
掘金 - Systrace 基础知识 - MainThread 和 RenderThread 解读
关于我 && 博客
下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!
- 博主个人介绍 :里面有个人的微信和微信群链接。
- 本博客内容导航 :个人博客内容的一个导航。
- 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
- Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。
一个人可以走的更快 , 一群人可以走的更远
