Android Performance

Android Systrace Basics - MainThread and RenderThread Explained

Word count: 2.3kReading time: 14 min
2019/11/06
loading

This is the ninth article in the Systrace series, primarily introducing the MainThread and RenderThread in Android Apps—commonly known as the Main Thread and Rendering Thread. This article examines their workflows from the perspective of Systrace and covers related topics: jank, software rendering, and dropped frame calculation.

The purpose of this series is to view the overall operation of the Android system from a different perspective using Systrace, while also providing an alternative angle for learning the Framework. Perhaps you’ve read many articles about the Framework but can never remember the code, or you’re unclear about the execution flow. Maybe from Systrace’s graphical perspective, you can gain a deeper understanding.

Table of Contents

Series Article Index

  1. Introduction to Systrace
  2. Systrace Basics - Prerequisites for Systrace
  3. Systrace Basics - Why 60 fps?
  4. Android Systrace Basics - SystemServer Explained
  5. Systrace Basics - SurfaceFlinger Explained
  6. Systrace Basics - Input Explained
  7. Systrace Basics - Vsync Explained
  8. Systrace Basics - Vsync-App: Detailed Explanation of Choreographer-Based Rendering Mechanism
  9. Systrace Basics - MainThread and RenderThread Explained
  10. Systrace Basics - Binder and Lock Contention Explained
  11. Systrace Basics - Triple Buffer Explained
  12. Systrace Basics - CPU Info Explained
  13. Systrace Smoothness in Action 1: Understanding Jank Principles
  14. Systrace Smoothness in Action 2: Case Analysis - MIUI Launcher Scroll Jank Analysis
  15. Systrace Smoothness in Action 3: FAQs During Jank Analysis
  16. Systrace Responsiveness in Action 1: Understanding Responsiveness Principles
  17. Systrace Responsiveness in Action 2: Responsiveness Analysis - Using App Startup as an Example
  18. Systrace Responsiveness in Action 3: Extended Knowledge on Responsiveness

Main Content

Using list scrolling as an example, we intercept the workflow of a single frame for the main thread and rendering thread (every frame follows this process, though some involve more work than others). Pay special attention to the “UI Thread” and RenderThread rows.

The workflow corresponding to this diagram is as follows:

  1. The main thread is in a Sleep state, waiting for the Vsync signal.
  2. Vsync arrived, and the main thread is woken up. Choreographer callbacks FrameDisplayEventReceiver.onVsync to start drawing a frame.
  3. Process the App’s Input events for this frame (if any).
  4. Process the App’s Animation events for this frame (if any).
  5. Process the App’s Traversal events for this frame (if any).
  6. The main thread synchronizes rendering data with the rendering thread. Once synchronization is finished, the main thread completes its drawing for the frame. It can then process the next Message (if any, IdleHandler will also be triggered if not empty) or return to Sleep to wait for the next Vsync.
  7. The rendering thread first takes a Buffer from the BufferQueue (dequeueBuffer). After processing data and calling OpenGL-related functions for the actual rendering, it returns the rendered Buffer to the BufferQueue (queueBuffer). Once Vsync-SF arrives, SurfaceFlinger takes all prepared Buffers for composition (this process is mentioned in the SurfaceFlinger section).

The above process is detailed in the article Detailed Explanation of Android Rendering Mechanism Based on Choreographer, including what doFrame does in each frame, jank calculation principles, and APM. I recommend checking that article if you haven’t yet.

In this article, we’ll dive into several points not covered in that previous piece to help you better understand the main and rendering threads:

  1. Evolution of the Main Thread
  2. Creation of the Main Thread
  3. Creation of the Render Thread
  4. Division of Labor: Main Thread vs. Render Thread
  5. Main Thread and Render Thread in Games
  6. Main Thread and Render Thread in Flutter

Creation of the Main Thread

An Android App process is based on Linux, and its management follows Linux process management mechanisms. Thus, its creation involves the fork function:

frameworks/base/core/jni/com_android_internal_os_Zygote.cpp

1
pid_t pid = fork();

The forked process can be seen as the main thread, but it’s not yet connected to Android and cannot handle Android App Messages. Since Android App execution is message-driven, this forked main thread must be bound to the Android Message mechanism to handle various Messages.

This is where ActivityThread comes in. More accurately, it could be called ProcessThread. ActivityThread connects the forked process with the App’s Messages. Their cooperation forms what we know as the Android App main thread. ActivityThread isn’t literally a thread; it initializes the MessageQueue, Looper, and Handler required for the Message mechanism. Since its Handler processes most Messages, we typically refer to ActivityThread as the main thread, though it’s actually a logic processing unit for the main thread.

Creation of ActivityThread

After the App process is forked, control returns to the App process to find the main function of ActivityThread:

com/android/internal/os/ZygoteInit.java

1
2
3
4
5
static final Runnable childZygoteInit(
int targetSdkVersion, String[] argv, ClassLoader classLoader) {
RuntimeInit.Arguments args = new RuntimeInit.Arguments(argv);
return RuntimeInit.findStaticMain(args.startClass, args.startArgs, classLoader);
}

Here, startClass is ActivityThread. Once found and called, the logic moves to the main function of ActivityThread:

android/app/ActivityThread.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
// 1. Initialize Looper and MessageQueue
Looper.prepareMainLooper();
// 2. Initialize ActivityThread
ActivityThread thread = new ActivityThread();
// 3. Primarily calls AMS.attachApplicationLocked to sync process info and perform initialization
thread.attach(false, startSeq);
// 4. Get the main thread Handler (H), where most App Messages are handled
if (sMainThreadHandler == null) {
sMainThreadHandler = thread.getHandler();
}
// 5. Initialization complete, Looper starts working
Looper.loop();
}

The comments are clear. Once main finishes, the main thread is officially online. Its Systrace flow looks like this:

Functions of ActivityThread

We often say Android’s four components run on the main thread. This is easily understood by looking at the ActivityThread‘s Handler Messages:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class H extends Handler { // Snippet
public static final int BIND_APPLICATION = 110;
public static final int EXIT_APPLICATION = 111;
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 DUMP_SERVICE = 123;
public static final int REMOVE_PROVIDER = 131;
public static final int DISPATCH_PACKAGE_BROADCAST = 133;
public static final int DUMP_PROVIDER = 141;
public static final int UNSTABLE_PROVIDER_DIED = 142;
public static final int INSTALL_PROVIDER = 145;
public static final int ON_NEW_ACTIVITY_OPTIONS = 146;
}

As shown, process creation, Activity launch, and management of Services, Receivers, and Providers are all handled here, leading into specific handleXXX methods.

Creation and Evolution of the Render Thread

Now for the rendering thread, or RenderThread. Early Android versions didn’t have a dedicated rendering thread; all rendering was done on the main thread using the CPU via the libSkia library. RenderThread was introduced in Android Lollipop to take over some of the main thread’s rendering workload, reducing its burden.

Software Rendering

“Hardware acceleration” usually refers to GPU acceleration—using RenderThread to call the GPU for faster rendering. In current Android versions, hardware acceleration is enabled by default. Thus, processes with visible content will have both a main thread and a rendering thread by default. If we disable it in the Application tag of the AndroidManifest via:

1
android:hardwareAccelerated="false"

The system will detect this and forgo initializing RenderThread, using the CPU via libSkia for direct rendering. Its Systrace looks like this:

Compared to the hardware-accelerated version at the start, the main thread’s execution time increases significantly because it must handle rendering, making jank more likely and reducing the idle intervals for other Messages.

Hardware-Accelerated Rendering

With hardware acceleration on, the main thread’s draw function doesn’t perform the actual drawCall. Instead, it records the content to a DisplayList, which is then synced to the RenderThread. Once synced, the main thread is freed to handle other tasks while the RenderThread continues rendering.

Rendering Thread Initialization

RenderThread is initialized when drawing content is required. Typically, when starting an Activity, the first draw execution checks if the rendering thread is initialized and performs the initialization if not.

android/view/ViewRootImpl.java

1
2
mAttachInfo.mThreadedRenderer.initializeIfNeeded(
mWidth, mHeight, mAttachInfo, mSurface, surfaceInsets);

Subsequently, draw is called directly:

android/graphics/HardwareRenderer.java

1
2
3
4
5
6
7
8
9
10
11
12
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);

// ... handle pending animating nodes ...

int syncResult = syncAndDrawFrame(choreographer.mFrameInfo);
// ... handle sync results ...
}

The draw here only updates the DisplayList. Afterward, syncAndDrawFrame is called to notify the rendering thread to start work while releasing the main thread. Core implementation of RenderThread is in the libhwui library (frameworks/base/libs/hwui).

frameworks/base/libs/hwui/renderthread/RenderProxy.cpp

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

I won’t detail the full RenderThread workflow here, as it warrants its own article. Many excellent resources on hwui exist, which you can consult alongside the source code. Its core flow in Systrace looks like this:

Division of Labor: Main Thread vs. Render Thread

The main thread handles process Messages, Input events, Animation logic, Measure, Layout, Draw, and updates the DisplayList, but it doesn’t interact directly with SurfaceFlinger. The rendering thread handles rendering-related work, partly via the CPU and partly by calling OpenGL functions.

With hardware acceleration, Android uses DisplayList instead of direct CPU drawing for each frame during the Draw phase. DisplayList is a record of drawing operations, abstracted as the RenderNode class. This indirect approach offers several advantages:

  1. DisplayList can be drawn multiple times as needed without re-interacting with business logic.
  2. Specific operations (like translation or scaling) can apply to the entire DisplayList without redistributing draw commands.
  3. Once all draw operations are known, they can be optimized (e.g., drawing all text together).
  4. DisplayList processing can be offloaded to another thread (RenderThread).
  5. The main thread can handle other Messages once sync is finished, without waiting for RenderThread to complete.

Detailed RenderThread flow can be found here: http://www.cocoachina.com/articles/35302

Main Thread and Render Thread in Games

Most games use a separate rendering thread with its own Surface and interact directly with SurfaceFlinger. The main thread plays a minimal role, as most logic is implemented within the custom rendering thread.

Check the Systrace for “Honor of Kings” (Honor of Kings), focusing on the application process and SurfaceFlinger process (30fps):

The main thread’s primary job is simply passing Input events to Unity’s rendering thread. The rendering thread then handles logic processing and screen updates.

Main Thread and Render Thread in Flutter

Flutter Apps also show distinct patterns in Systrace. Since Flutter rendering is based on libSkia, it doesn’t use the standard RenderThread. Instead, it builds its own RenderEngine. Flutter’s two most important threads are the UI thread and the GPU thread, corresponding to the Framework and Engine layers mentioned below:

Flutter also listens for Vsync signals. Its VsyncView uses postFrameCallback to monitor doFrame callbacks, then calls nativeOnVsync to pass information to the Flutter UI thread to begin a frame’s drawing.

Flutter’s approach is similar to game development: it doesn’t depend on the specific platform, builds its own rendering pipeline, and offers fast updates and cross-platform advantages.

Flutter SDK includes the Skia library, enabling the latest Skia features without waiting for OS updates. Google has optimized Skia heavily, claiming performance comparable to native apps.

Flutter’s framework is divided into Framework (Dart) and Engine (C++) layers. Apps are built on the Framework, which handles Build, Layout, Paint, and layer generation. The Engine combines those layers, generates textures, and submits data to the GPU via OpenGL.

When the UI needs updating, the Framework notifies the Engine. The Engine waits for the next Vsync, notifies the Framework, which then performs animations, build, layout, compositing, and paint to generate a layer for the Engine. The Engine combines the layers and submits them to the GPU.

Performance

If the main thread handles all tasks, long-running operations (like network access or database queries) will block the entire UI thread. A blocked thread cannot dispatch events, including draw events. Overloading the main thread leads to two major issues:

  1. Jank: If the combined work of the main and rendering threads exceeds 16.6ms (at 60fps), dropped frames occur.
  2. Freeze: If the UI thread is blocked for several seconds (thresholds vary by component), users see the “Application Not Responding“ (ANR) dialog (though some manufacturers skip the dialog and crash to desktop).

These are undesirable for users, so App developers must address them before release. ANR is relatively easy to locate via call stacks, but intermittent jank often requires tools like Systrace + TraceView. Understanding the relationship and mechanics of the main and rendering threads is crucial—this is the primary motivation for this series.

Regarding jank, consult these three articles. A janky App isn’t always the App’s fault; it could be the system. Regardless, you must first know how to analyze it.

  1. Overview of Jank Causes in Android - Methodology
  2. Overview of Jank Causes in Android - System Side
  3. Overview of Jank Causes in Android - App Side

References

  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

Attachments

Attachments for this article have been uploaded. Extract and open in Chrome:

Download Systrace attachments for this article

Other Addresses

Please visit the Zhihu or Juejin pages for this article for comments or likes:
Juejin - Systrace Basics - MainThread and RenderThread Explained

About Me && Blog

Below is my personal intro and related links. I look forward to exchanging ideas with fellow professionals. “When three walk together, one can always be my teacher!”

  1. Blogger Intro: Includes personal WeChat and WeChat group links.
  2. Blog Content Navigation: A guide for my blog content.
  3. Curated Excellent Blog Articles - Android Performance Optimization Must-Knows: Welcome to recommend projects/articles.
  4. Android Performance Optimization Knowledge Planet: Welcome to join and thank you for your support~

One walks faster alone, but a group walks further together.

Scan WeChat QR Code

CATALOG
  1. 1. Table of Contents
  • Series Article Index
  • Main Content
  • Creation of the Main Thread
    1. 1. Creation of ActivityThread
    2. 2. Functions of ActivityThread
  • Creation and Evolution of the Render Thread
    1. 1. Software Rendering
    2. 2. Hardware-Accelerated Rendering
    3. 3. Rendering Thread Initialization
    4. 4. Division of Labor: Main Thread vs. Render Thread
  • Main Thread and Render Thread in Games
  • Main Thread and Render Thread in Flutter
  • Performance
  • References
  • Attachments
  • Other Addresses
  • About Me && Blog