EN

Android Systrace 基础知识 - MainThread 和 RenderThread 解读

字数统计: 5.2k阅读时长: 20 min
2019/11/06
loading

本文是 Systrace 系列文章的第九篇,主要是是介绍 Android App 中的 MainThread 和 RenderThread,也就是大家熟悉的主线程和渲染线程。文章会从 Systrace 的角度来看 MainThread 和 RenderThread 的工作流程,以及涉及到的相关知识:卡顿、软件渲染、掉帧计算等

本系列的目的是通过 Systrace 这个工具,从另外一个角度来看待 Android 系统整体的运行,同时也从另外一个角度来对 Framework 进行学习。也许你看了很多讲 Framework 的文章,但是总是记不住代码,或者不清楚其运行的流程,也许从 Systrace 这个图形化的角度,你可以理解的更深入一些

本文目录

系列文章目录

  1. Systrace 简介
  2. Systrace 基础知识 - Systrace 预备知识
  3. Systrace 基础知识 - Why 60 fps ?
  4. Systrace 基础知识 - SystemServer 解读
  5. Systrace 基础知识 - SurfaceFlinger 解读
  6. Systrace 基础知识 - Input 解读
  7. Systrace 基础知识 - Vsync 解读
  8. Systrace 基础知识 - Vsync-App :基于 Choreographer 的渲染机制详解
  9. Systrace 基础知识 - MainThread 和 RenderThread 解读
  10. Systrace 基础知识 - Binder 和锁竞争解读
  11. Systrace 基础知识 - Triple Buffer 解读
  12. Systrace 基础知识 - CPU Info 解读
  13. Systrace 流畅性实战 1 :了解卡顿原理
  14. Systrace 流畅性实战 2 :案例分析: MIUI 桌面滑动卡顿分析
  15. Systrace 流畅性实战 3 :卡顿分析过程中的一些疑问
  16. Systrace 响应速度实战 1 :了解响应速度原理
  17. Systrace 响应速度实战 2 :响应速度实战分析-以启动速度为例
  18. 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 帧都必须走完的固定流程。

这张图可以沿以下顺序阅读:

  1. 主线程暂时没有可执行工作,处于等待状态;它也会被输入或其他事件唤醒,并非只等 Vsync。
  2. 请求过帧回调后,Vsync 事件进入 FrameDisplayEventReceiver.onVsync;它向所属 Looper 投递异步消息,随后执行 run() / doFrame(),不是在收到信号的一刻就完成绘制。
  3. 执行到期的 Input 回调;输入也有不走这一批处理阶段的路径。
  4. 执行到期的 Animation 回调。Android 10/16 在 Traversal 前还有 Insets Animation 阶段,图中没有单独展示。
  5. Traversal 按需执行 Measure、Layout、Draw。更新 View 绘制记录时,主线程会与 RenderThread 同步;doFrame 后面还有 Commit 阶段,不能从这张局部图判断其一定有回调。
  6. 主线程在同步调用中等待 RenderThread 放行,随后才能继续。满足条件时它可以与后续渲染并行;IdleHandler 是否执行取决于消息队列的空闲条件,不是每帧结束都自动执行。
  7. 图中 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
2
3
4
5
6
7
8
9
10
11
12
13
public static final Runnable zygoteInit(int targetSdkVersion, String[] argv,
ClassLoader classLoader) {
if (RuntimeInit.DEBUG) {
Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from zygote");
}

Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
RuntimeInit.redirectLogStreams();

RuntimeInit.commonInit();
ZygoteInit.nativeZygoteInit();
return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
}

接着看同版本 ActivityThread.main()。这里保留 startSeq 的解析,避免原节选中变量没有定义;这些都是 framework 内部代码,不是可以复制进普通 App 的独立示例。

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
43
44
45
46
47
48
49
50
public static void main(String[] args) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");

// Install selective syscall interception
AndroidOs.install();

// CloseGuard defaults to true and can be quite spammy. We
// disable it here, but selectively enable it later (via
// StrictMode) on debug builds, but using DropBox, not logs.
CloseGuard.setEnabled(false);

Environment.initForCurrentUser();

// Make sure TrustedCertificateStore looks in the right place for CA certificates
final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
TrustedCertificateStore.setDefaultUserDirectory(configDir);

Process.setArgV0("<pre-initialized>");

Looper.prepareMainLooper();

// Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
// It will be in the format "seq=114"
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();
}

if (false) {
Looper.myLooper().setMessageLogging(new
LogPrinter(Log.DEBUG, "ActivityThread"));
}

// End of event ActivityThreadMain.
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
Looper.loop();

throw new RuntimeException("Main thread loop unexpectedly exited");
}

attach() 通过 AMS 的 attachApplication 注册 App 端接口,后续再处理应用绑定等事务。进入 Looper.loop(),主线程这就算正式上线、持续分派工作了;并不是等 main() 返回后才开始运行。源码最后的异常也说明,主 Looper 意外退出并不是正常启动完成。

ActivityThread 的功能

Activity 生命周期、Service 的常见生命周期回调、默认广播接收等,通常由各自进程的主线程处理。Android 10 的 H 中可以看到以下内部消息号(节选,不是稳定的 SDK 接口):

1
2
3
4
5
6
7
8
public static final int BIND_APPLICATION = 110;
public static final int RECEIVER = 113;
public static final int CREATE_SERVICE = 114;
public static final int STOP_SERVICE = 116;
public static final int BIND_SERVICE = 121;
public static final int UNBIND_SERVICE = 122;
public static final int INSTALL_PROVIDER = 145;
public static final int EXECUTE_TRANSACTION = 159;

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
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
void draw(View view, AttachInfo attachInfo, DrawCallbacks callbacks,
FrameDrawingCallback frameDrawingCallback) {
attachInfo.mIgnoreDirtyState = true;

final Choreographer choreographer = attachInfo.mViewRootImpl.mChoreographer;
choreographer.mFrameInfo.markDrawStart();

updateRootDisplayList(view, callbacks);

attachInfo.mIgnoreDirtyState = false;

// register animating rendernodes which started animating prior to renderer
// creation, which is typical for animators started prior to first draw
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();
// We don't need this anymore as subsequent calls to
// ViewRootImpl#attachRenderNodeAnimator will go directly to us.
attachInfo.mPendingAnimatingRenderNodes = null;
}

final long[] frameInfo = choreographer.mFrameInfo.mFrameInfo;
if (frameDrawingCallback != null) {
nSetFrameCallback(mNativeProxy, frameDrawingCallback);
}
int syncResult = nSyncAndDrawFrame(mNativeProxy, frameInfo, frameInfo.length);
if ((syncResult & SYNC_LOST_SURFACE_REWARD_IF_FOUND) != 0) {
setEnabled(false);
attachInfo.mViewRootImpl.mSurface.release();
// Invalidate since we failed to draw. This should fetch a Surface
// if it is still needed or do nothing if we are no longer drawing
attachInfo.mViewRootImpl.invalidate();
}
if ((syncResult & SYNC_INVALIDATE_REQUIRED) != 0) {
attachInfo.mViewRootImpl.invalidate();
}
}

它更新 DisplayList、注册动画节点和回调,再经 JNI 进入 RenderProxy.cpp:

1
2
3
int RenderProxy::syncAndDrawFrame() {
return mDrawFrameTask.drawFrame();
}

这个调用不是“发个通知就立即返回”。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 还包含变换等属性,二者不是同一个概念。这种分工有几个好处:

  1. 内容未变时,可以复用已有绘制记录,避免重复执行 View 的记录逻辑。
  2. translation、scale 等节点属性变化,可以在适用条件下复用内容;内容本身变化仍可能需要重新记录。
  3. 后端可以在保持绘制顺序、裁剪、混合等语义的前提下优化,不能保证“所有文本一起画一次”。
  4. RenderThread 可以处理渲染工作和部分支持的动画,但业务逻辑、普通 View 更新仍需要主线程配合。
  5. 同步状态允许时,两线程可以并行推进;主线程并不是永远不等 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 这两处假设。

性能

主线程上的耗时查询、计算或同步等待,会推迟输入和绘制处理。性能分析时,要分清两个问题:

  1. 卡顿:60 / 90 / 120 Hz 的刷新间隔约为 16.67 / 11.11 / 8.33 ms,但帧是否错过显示时机,要看目标帧率和实际截止时间。主线程与 RenderThread 有同步和并行,不能把两段墙钟时间简单相加判定掉帧;GPU 或系统合成也可能延迟结果。Android 12(API 31)起,在采集到相应数据时,可结合 Perfetto FrameTimeline 查看预期与实际时序。
  2. ANR:由输入、广播、Service 等各自的超时规则触发,不是任意消息执行几秒就必定弹窗;是否显示对话框也受场景和系统策略影响。先看具体 ANR 类型、日志与阻塞依赖,见 诊断 ANR。

这两种情况都是用户不愿意看到的,也是开发者需要持续解决的问题。ANR 栈有帮助,但采样可能晚于真正的阻塞,不能保证一眼定位;间歇性卡顿还要结合系统 Trace,必要时补方法采样。TraceView 等方法插桩也会扰动耗时,不能把其时间直接当无插桩性能。理解主线程与渲染线程的关系,正是这个系列的初衷。

另外关于卡顿,可以参考下面三篇文章。App 卡顿也可能涉及系统调度或资源竞争,先用证据找出延迟发生在哪一段。

  1. Android 中的卡顿丢帧原因概述 - 方法论
  2. Android 中的卡顿丢帧原因概述 - 系统篇
  3. Android 中的卡顿丢帧原因概述 - 应用篇

参考

  1. https://juejin.im/post/5a9e01c3f265da239d48ce32
  2. http://www.cocoachina.com/articles/35302
  3. https://juejin.im/post/5b7767fef265da43803bdc65
  4. http://gityuan.com/2019/06/15/flutter_ui_draw/
  5. 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 解读

关于我 && 博客

下面是个人的介绍和相关的链接,期望与同行的各位多多交流,三人行,则必有我师!

  1. 博主个人介绍 :里面有个人的微信和微信群链接。
  2. 本博客内容导航 :个人博客内容的一个导航。
  3. 个人整理和搜集的优秀博客文章 - Android 性能优化必知必会 :欢迎大家自荐和推荐 (微信私聊即可)
  4. Android性能优化知识星球:付费社群已于 2026 年停止运营,不再招收新成员;此处保留历史介绍。

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. 本文目录
  • 系列文章目录
  • 正文
  • 主线程的创建
    1. 1. ActivityThread 的创建
    2. 2. ActivityThread 的功能
  • 渲染线程的创建和发展
    1. 1. 软件绘制
    2. 2. 硬件加速绘制
    3. 3. 渲染线程初始化
    4. 4. 主线程和渲染线程的分工
  • 游戏的主线程与渲染线程
  • Flutter 的主线程和渲染线程
  • 性能
  • 参考
  • 附件
  • 本文其他地址
  • 关于我 && 博客