Android path · The process · Topic 3 of 12

The main thread, Looper and Handler

Not started

When your process starts, Android creates one thread for you, the main thread (also called the UI thread), and runs a Looper on it. A Looper is a loop over a MessageQueue: take the next message, run it to the end, take the next. Input events, drawing, lifecycle callbacks and the code you post all arrive as messages on that one queue, so while one of them runs, nothing else on screen can happen.

Why it matters: Every frame, touch and lifecycle callback runs on one thread. Block it for a few hundred milliseconds and frames drop; block it for five seconds and the system shows an ANR.

  1. 1A loop over a queue

    The main thread runs a Looper that takes one Message or Runnable at a time from its MessageQueue and runs it to completion.

  2. 2Everything shares it

    Drawing, input, lifecycle callbacks, Compose recomposition and your click handlers all wait their turn on the same queue.

  3. 3ANR is a timeout

    If input isn't handled within 5 seconds, or a receiver or service callback overruns, the system reports Application Not Responding.

1

How it works

The explanation, with examples you can run.

The loop in a few lines

Most threads have no Looper. You can give one a loop yourself, which is exactly what the framework does for the main thread:

val worker = thread {    Looper.prepare()                                  // attach a MessageQueue to this thread    handler = Handler(Looper.myLooper()!!)            // a way for other threads to post into it    Looper.loop()                                     // run messages until quit() is called}

In practice you use HandlerThread for this, or more often coroutines, but the model is the same one the main thread uses.

Posting work

A Handler is a handle onto one Looper's queue. Any thread can use it to enqueue work for that Looper's thread:

val main = Handler(Looper.getMainLooper()) main.post { statusText.text = "Done" }                 // run as soon as the queue gets to itmain.postDelayed({ hideBanner() }, 3_000)              // run no earlier than 3 seconds from now

postDelayed is "no earlier than", not "exactly at": if the main thread is busy, the message waits. Also note that pending messages hold references to whatever their lambdas capture; remove them (removeCallbacksAndMessages(null)) when the screen goes away, or prefer a lifecycle-aware coroutine.

View.post {} and Activity.runOnUiThread {} are shortcuts for the same thing.

The budget

At 60 Hz a frame has about 16 ms; at 120 Hz about 8 ms. Any message that runs longer than that delays the next frame, which users notice as jank. Long-running blocking work belongs on other threads: networking (which throws NetworkOnMainThreadException on the main thread), database queries, file I/O, JSON parsing of large payloads, bitmap decoding.

ANRs

If the main thread stays blocked, the system steps in. The documented triggers include:

  • an input event (touch, key) not handled within 5 seconds;
  • a BroadcastReceiver that hasn't finished in time (5 seconds while the app has an activity in the foreground, longer otherwise);
  • a service whose onCreate/onStartCommand/onBind doesn't return within a few seconds;
  • a service started with startForegroundService() that doesn't call startForeground() within 5 seconds.

The cause is often not slow code but a wait: a lock held by a background thread, a synchronous binder call, or runBlocking on the main thread. Play Console's Android vitals reports ANR rates, and the stack traces show what the main thread was waiting on.

Catching it early

StrictMode can flag disk and network access on the main thread during development:

if (BuildConfig.DEBUG) {    StrictMode.setThreadPolicy(        StrictMode.ThreadPolicy.Builder().detectDiskReads().detectDiskWrites().detectNetwork().penaltyLog().build()    )}
2

Check yourself

3 questions. Get them all right to finish this topic.

  1. 1.Your click handler reads a 20 MB file synchronously. What does the user see?
  2. 2.What does Handler(Looper.getMainLooper()).post { ... } do when called from a background thread?
  3. 3.Why is the no-argument Handler() constructor deprecated?

0 of 3 right so far. Get all three to finish this topic.