跪拜 Guibai
← Back to the summary

The Android Thread Map: Main, HandlerThread, and Binder Threads Are Not the Same

Handler (Part 3): How to Distinguish Between the Main Thread, HandlerThread, and Binder Threads?

Author: 小明的运行时 Tags: Android

Handler (Part 3): How to Distinguish Between the Main Thread, HandlerThread, and Binder Threads?

The first two articles addressed the Handler mechanism itself:

Part 1:
    Why a Runnable executes on the target thread after post().

Part 2:
    Why a Message is not executed immediately after being enqueued.

This article places the mechanism back into the context of Android system threads.

When reading source code and troubleshooting, many problems are not about being unable to understand post() or next(), but about mixing up thread concepts:

Is the main thread a Handler?
Is a HandlerThread a Handler?
Why might directly calling `new Handler()` in a regular Thread crash?
What is the relationship between Binder threads and Handler threads?
Why does system_server have threads named main, android.ui, android.display, and android.io?

If these boundaries are not clear, it's easy to attribute all problems to:

The main thread is stuck.
system_server is stuck.
The Handler is stuck.

These statements are too vague. They don't tell you which thread the target Looper belongs to, nor what that thread is designed to do.

So this article answers only one question:

Which threads run a Looper long-term?
Which threads are responsible for executing server-side Binder entry points?
Why do Binder entry points often hand off subsequent tasks to a Handler?

Let's start with the core conclusions:

Main Thread:
    Thread + MainLooper.

Regular Thread:
    By default, just a Thread, without a Looper.

HandlerThread:
    Thread + Looper.prepare() + Looper.loop().

Handler:
    Holds a reference to a Looper, serving as the entry point for posting tasks to its MessageQueue and dispatching messages after retrieval.

Binder Thread:
    Receives transactions from the Binder driver and executes the server-side Binder entry point on the current thread;
    The entry code can handle the task directly, or hand off subsequent state processing to a Handler.

system_server Dedicated Threads:
    Multiple ServiceThreads with Loopers, used to serially process the state of different system modules.

The source code in this article references the android-16.0.0_r4 tag of Android 16 QPR2. The main text only discusses the stable mechanism of Handler and does not expand on version-specific implementation differences.

image.png

Read this diagram with two questions in mind:

First question:
    Is this thread running a Looper?

Second question:
    If it is running a Looper, which system domain's serial state processing is it responsible for?

This diagram only shows the dedicated threads within system_server that run a Looper long-term. The Binder thread pool does not rely on Looper.loop() to receive transactions, so it is not drawn in this group of Handler threads; Section 5 will separately expand on how it executes server-side IPC entry points.

1. Why Does the Main Thread Naturally Have a Looper?

The entry point for an application's main thread is in ActivityThread.main().

The main flow is:

ActivityThread.main()
  -> Looper.prepareMainLooper()
  -> new ActivityThread()
  -> thread.attach(false, startSeq)
  -> Looper.loop()

Source code entry points:

This shows that the main thread is not just a regular Thread with a name. It prepares the MainLooper at startup and enters Looper.loop().

Therefore:

Handler mainHandler = new Handler(Looper.getMainLooper());

The meaning of this line of code is not "create the main thread," but:

Create a posting entry point;
This entry point holds a reference to the MainLooper;
The MainLooper is bound to the application's main thread.

The main thread naturally has a Looper because Android prepares it for you when the process starts.

2. Why Doesn't a Regular Thread Have a Looper by Default?

A regular Java thread is only responsible for executing run(). It does not automatically create a message queue, nor does it automatically enter a loop.

new Thread(() -> {
    Looper.myLooper(); // null by default
}).start();

If you implicitly create a Handler in such a thread without a Looper:

new Handler();

The constructor reads Looper.myLooper() which is null, so it throws an exception directly. If the thread had previously manually executed Looper.prepare(), the no-argument constructor would not crash, but it would implicitly bind to the current thread's Looper; the no-argument Handler constructor is deprecated precisely because this implicit choice easily causes crashes, lost tasks, race conditions, or binding to the wrong thread.

But "a regular thread does not have its own Looper by default" does not mean "a regular thread cannot create any Handler." It can still explicitly hold another thread's Looper:

new Thread(() -> {
    Handler mainHandler = new Handler(Looper.getMainLooper());
    mainHandler.post(() -> updateUi());
}).start();

Here, the Handler is created by a regular thread, but the Handler holds a reference to the MainLooper, and the callback is still executed by the application's main thread. The real restriction is on "using the no-argument constructor to implicitly select the current thread's Looper on a thread without one," not the action of new Handler(...) itself.

Source code entry points:

So the difference between a regular thread and a Handler thread is not "is it a child thread?" but:

Regular Thread:
    Finishes after executing run(), no message loop.

Thread with a Looper:
    After prepare, enters loop and can continuously receive Messages.

3. What Exactly is a HandlerThread?

HandlerThread is not a Handler.

It is a Thread with a Looper.

The main source code flow is very short:

HandlerThread.run()
  -> mTid = Process.myTid()
  -> Looper.prepare()
  -> mLooper = Looper.myLooper()
  -> notifyAll()
  -> Process.setThreadPriority(mPriority)
  -> onLooperPrepared()
  -> Looper.loop()

Source code entry points:

Expressed as a formula:

HandlerThread = Thread + Looper.prepare() + Looper.loop()

It solves the problem of:

I need a worker thread that can receive Handler messages.

While a Handler solves the problem of:

I need an entry point to post tasks to a Looper's MessageQueue.

The two are often used together:

HandlerThread thread = new HandlerThread("camera-state");
thread.start();

Handler handler = new Handler(thread.getLooper());

The object relationships here are:

thread:
    The thread that actually executes the tasks.

thread.getLooper():
    The message loop within this thread.

handler:
    The entry point for posting to this message loop.

So don't say "HandlerThread processes messages." A more accurate statement is:

The Looper inside the HandlerThread retrieves messages;
Messages return to the Handler via the target;
Finally, Handler.dispatchMessage dispatches the callback.

4. Is a HandlerThread a Thread Pool?

No.

A single HandlerThread still only dispatches one Message at a time.

While Message A is executing:
    Message B can only wait.

If Message A does slow I/O:
    All subsequent Messages are held up.

If Message A waits for a lock:
    The serial state machine on this HandlerThread also stops.

This is where HandlerThread is most easily misused. It is suitable for carrying state processing that needs to be serialized, such as:

Device state machines;
Display state changes;
Permission state synchronization;
Events within a module that must be processed in order.

It is not suitable for carrying a large number of unrelated, time-consuming tasks.

If tasks are independent of each other, can be concurrent, and are mainly computation or I/O, you should consider:

Executor;
ExecutorService;
Thread pools;
Coroutines;
Dedicated asynchronous frameworks.

The official comment for HandlerThread also suggests: if you don't have to interface with the Handler API, prioritize Executor, ExecutorService, or Kotlin coroutines.

So to judge whether a HandlerThread is appropriate, ask:

Must these tasks be executed sequentially on the same thread?
Is this thread allowed to be occupied by a single task for a long time?
When subsequent messages are queued and waiting, is the system behavior acceptable?

5. Are Binder Threads Handler Threads?

No.

Binder threads and Handler threads solve two different types of problems.

Binder Thread:
    Executes the server-side Binder entry code.

Handler Thread:
    Serially processes the internal state of a module.

A typical chain is:

App process
  -> Binder transact
  -> Binder driver delivers the transaction to a Binder pool thread in system_server
  -> Binder.execTransact() / Binder.onTransact()
  -> AIDL Stub dispatches to the specific service method
  -> Server entry point performs parameter validation, permission checks, identity handling
  -> Entry code chooses to:
       Process directly on the Binder thread;
       Post to a Handler and return;
       Post to a Handler and synchronously wait for the result;
       Or continue to synchronously call other Binder services.

Source code entry points:

A Handler is not the mandatory next layer for a Binder call. Only when the server entry point chooses to hand off subsequent work to a Handler do you need to further distinguish two key forks:

Internal Asynchronous Posting Pattern:
    The Binder thread posts the task to a Handler and returns quickly;
    The App sees the result through subsequent callbacks, state changes, broadcasts, or the next query.

Internal Synchronous Waiting Pattern:
    After posting the task to a Handler, the Binder thread also waits for the Handler thread to process the result;
    If the Handler thread is backlogged or blocked, the Binder thread is also held up;
    What the App sees is a slow synchronous Binder call.

The "asynchronous posting / synchronous waiting" here describes whether the server entry point waits for the Handler's result, which is not equivalent to the distinction between AIDL oneway and regular synchronous Binder transactions.

This fork is very important. Seeing "the service internally posts to a Handler" does not immediately mean the Binder thread has completely disengaged. You must continue to see whether it returns after posting, or synchronously waits for the result after posting.

Why not do all the business logic on the Binder thread?

Because the Binder thread pool is a shared resource. If a Binder thread spends a long time processing business logic, waiting for locks, doing I/O, or synchronously calling other services, it will hold up the caller and also consume the Binder thread pool capacity of system_server.

Many system services post the actual state changes to their own Handler threads to gain several benefits:

Serialization:
    State changes for the same module are processed in order, reducing lock complexity.

Decoupling the caller during internal asynchronous posting:
    The Binder thread returns as soon as possible after the post is accepted, leaving time-consuming processing to the target Handler thread.

Isolating responsibilities:
    The Binder thread executes the IPC entry point, and the Handler thread processes module state that needs serialization.

Easier troubleshooting:
    You can pinpoint a specific Looper thread instead of vaguely saying system_server is slow.

The internal synchronous waiting pattern only retains the benefit of "business state is serially processed by the target thread"; it cannot be said to have decoupled the caller. The Binder thread is still waiting for the result, and the target Handler's queuing, lock waiting, and slow callbacks will propagate back to the App along the synchronous call chain.

But this does not mean "posting to a Handler is definitely fine." If the Handler thread itself is backlogged or blocked, the problem just moves location:

In the asynchronous posting pattern, the Binder entry quickly posts the message and returns;
But the Handler's thread never processes it;
The effect the user sees still never happens.

This is why subsequent troubleshooting must look at both the Binder thread and the target Handler thread.

6. Why Doesn't system_server Only Have One Main Thread?

system_server is a process, not a single thread.

Inside it, there is a main thread, a Binder thread pool, and multiple dedicated Handler threads.

The system_server main thread also prepares a MainLooper:

SystemServer.run()
  -> Looper.prepareMainLooper()
  -> startBootstrapServices()
  -> startCoreServices()
  -> startOtherServices()
  -> Looper.loop()

Source code entry points:

But many system tasks are not all pushed onto the main thread. The Framework has multiple dedicated threads, common ones include:

Thread Nature Common Responsibilities
system_server main system_server main Looper Service startup, mainline tasks for some system services
android.ui ServiceThread for UI within system_server Short UI-related tasks that need to avoid the system main thread
android.display Display-related ServiceThread Display/Window/Input related tasks for DisplayManager, WindowManager, InputManager, etc.
android.anim Animation-related ServiceThread Window animations, tasks related to animation timing
android.io I/O-related ServiceThread System I/O tasks that allow brief blocking
android.perm Permission-related ServiceThread Synchronizing state for permission, appops, etc.
Binder threads Binder thread pool worker threads Execute server-side Binder entry points; the entry can handle directly or hand off tasks

The android.ui here is the UiThread inside the system_server process, not the main thread of the com.android.systemui application process. The processes, Loopers, and responsibilities of the two are different and must not be confused during troubleshooting.

Source code entry points:

These thread names are not decorative. They determine where system services post certain tasks for serial processing.

For example, for window, display, and input related issues, you can't just stare at the App's main thread or SurfaceFlinger. Many state changes first enter system_server, and are then posted to android.display or related Handler threads for processing.

7. Why Can't You Just Say "system_server is Slow" When Troubleshooting?

Because system_server is just a process name.

You must at least ask further:

Is the system_server main slow?
Is android.ui slow?
Is android.display slow?
Is android.io slow?
Are the Binder threads slow?
Or is a HandlerThread created by some service itself slow?

Each answer corresponds to a different direction for investigation.

If the Binder threads are slow: Look at cross-process calls, caller waiting, whether the Binder thread pool is exhausted, and whether the synchronous call chain is too long.

If android.display is slow: Look at whether window, display, and input related callbacks are time-consuming, whether there are lock waits, synchronous Binder calls, or native/HAL blocking.

If android.io is slow: Look at whether I/O exceeds expectations, or whether work that should be asynchronous or cancellable has been stuffed into a serial Looper.

If a specific HandlerThread is slow: Look at whether this thread is being abused as a thread pool, or whether multiple unrelated time-consuming tasks are queuing up.

So a traceable statement should look like this:

The target message was posted to system_server's android.display Handler;
android.display is currently executing a previous window callback;
Subsequent display state updates are waiting in that Looper's queue.

Instead of:

system_server is stuck.

8. What Three Things Should You Ask First When You See a Handler?

Don't first ask "Is it the main thread?".

First ask three things:

1. Which Looper does this Handler hold?

2. Which Thread is this Looper bound to?

3. What task is this Thread responsible for in the system design?

These three questions can untangle many conceptual confusions.

Example one:

Handler handler = new Handler(Looper.getMainLooper());

Answer:

Holds a reference to the MainLooper;
MainLooper is bound to the main thread;
This thread is responsible for UI and application mainline callbacks.

Example two:

Handler handler = new Handler(DisplayThread.get().getLooper());

Answer:

Holds a reference to the DisplayThread's Looper;
This Looper is bound to the android.display thread;
This thread is responsible for display, window, and input related system tasks.

Example three:

Handler handler = new Handler(handlerThread.getLooper());

Answer:

Holds a reference to a self-created HandlerThread's Looper;
This Looper is bound to this self-created thread;
Its responsibility depends on the module that created it.

This is the entry point for Handler troubleshooting.

9. Core Distinction Table

Condensing this article into one table:

Thread Type Nature Common Misconception Accurate Statement
Main Thread Thread + MainLooper Handler is the main thread The main thread naturally has a MainLooper; Handler is the posting and dispatching entry point that holds it.
Regular Thread Thread Can bind itself with a no-arg Handler by default By default, it has no Looper of its own, but can still explicitly bind to another thread's Looper.
HandlerThread Thread + Looper It is a Handler or a thread pool It is a thread with a Looper; it still only dispatches one message at a time.
Binder Thread Binder thread pool worker thread It just receives or forwards calls It executes the server-side Binder entry point on the current thread; the entry can process directly or choose to hand off to a Handler.
system_server main system_server main Looper system_server only has this one thread system_server also has Binder threads and multiple dedicated ServiceThreads.
android.ui Internal UI Handler thread of system_server It is the SystemUI process main thread It is system_server's UiThread, different from the main thread of com.android.systemui.
android.display Display/Window/Input related Handler thread Display problems are all in SurfaceFlinger Framework display state can also be in a system_server thread.
android.io System I/O Handler thread Background threads can block indefinitely It is still a serial Looper; blocking will cause subsequent messages to pile up.

10. Conclusion of This Article

Handler troubleshooting is first and foremost about thread identification.

When you see a Handler, don't first say "it's the main thread" or "it's a child thread." You should first ask:

Which Looper does this Handler hold?
Which Thread is this Looper bound to?
What task is this Thread responsible for in the system design?

Only when the thread is clearly identified does it become meaningful to discuss "why the message wasn't executed," "is it slow delivery," or "is it slow dispatch."

The three articles up to this point form a chain:

How tasks travel:
    Handler.post -> Message -> enqueue -> next -> dispatch.

How the queue waits:
    when, sync barriers, nativePollOnce, nativeWake, quit.

How threads are distinguished:
    Main thread, HandlerThread, Binder threads, system_server dedicated Loopers.

The next article enters practical application:

How to locate Handler lag? From Slow delivery to Slow dispatch.

Further Reading