Android from the foundation · Puzzle 6 of 6

The main thread is one clerk with one queue

All UI and lifecycle code runs on one thread that takes messages from a queue, one at a time. A slow message blocks everything behind it.

The problem

In puzzle 5, a Binder thread picked up the town hall's letter. But your screens can't be touched from just any thread. Android's UI code isn't thread-safe: if two threads changed the same view at the same moment, it could end up broken. So every UI change and every lifecycle letter has to end up on one thread, in order.

See it

Letters wait in the main thread's queue; a Binder thread drops a new one in with a Handler; the main thread takes one at a time; a slow one blocks the touch behind it until the system shows an ANR your app's process main thread queue → frame onCreate touch Binder thread resume 6 s… App isn't respondingtouch waited 5 s
  1. Inside your app, the main thread works through a queue of letters: draw a frame, run your onCreate, handle a touch. One at a time, in order.

    Step 1 of 5
  2. Remember the couriers from puzzle 5? When the town hall says "resume", a Binder thread receives it and drops it at the end of the queue with a Handler, the slot into this queue.

    Step 2 of 5
  3. The clerk takes the first letter, finishes it, and the queue moves up. Predict: what if the next letter takes a long time?

    Step 3 of 5
  4. Your onCreate calls Thread.sleep(6000): it blocks for 6 seconds. Everything behind it waits, including the user's touch: the clerk only does one thing at a time.

    Step 4 of 5
  5. If a touch isn't handled within 5 seconds, the system shows App isn't responding (an ANR). Long before that, the screen stops being redrawn: the UI freezes.

    Step 5 of 5

The building block

Inside your process:

  • MessageQueueWhen nothing is due, the queue sleeps in native code until a message arrives or a timer fires. 387 nativePollOnce(ptr, nextPollTimeoutMillis);MessageQueue.java, line 387 · AOSP frameworks/base ↗: the queue of messages ("run this"), sorted by when they should run.
  • Looper: Looper.loop()The main thread's endless loop: take one message, run it, repeat. 304 public static void loop() { … 327 for (;;) { 328 if (!loopOnce(me, ident, thresholdOverride)) { 329 return; 330 } 331 }Looper.java, line 304 · AOSP frameworks/base ↗ is an endless loop on one thread: take the next message, run it, repeat; sleep when the queue is empty.
  • Handler: the slot that puts a message into one Looper's queue. handler.post { … }post() wraps your code in a Message and puts it in this Handler's queue. 438 public final boolean post(@NonNull Runnable r) { 439 return sendMessageDelayed(getPostMessage(r), 0); 440 } … 782 private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, 783 long uptimeMillis) { 784 msg.target = this; … 789 }Handler.java, line 438 · AOSP frameworks/base ↗ drops your code in as a letter.
val main = Handler(Looper.getMainLooper())
main.post { textView.text = "Done" }        // runs when the clerk reaches it
main.postDelayed({ hideHint() }, 2_000)     // runs about 2 s later

Every app process starts in ActivityThread.main()The real entry point of every app process: it prepares the main Looper, says hello to system_server, then loops forever. 8907 public static void main(String[] args) { … 8927 Looper.prepareMainLooper(); … 8943 thread.attach(false, startSeq); … 8956 Looper.loop();ActivityThread.java, line 8907 · AOSP frameworks/base ↗, which prepares this loop for the main thread and runs it forever.

The screen is redrawn about every 16 ms (60 times a second), and each redraw is also a message in this queue. If a message takes longer, frames are skipped and the app stutters ("jank"). If a touch waits 5 seconds, the system shows an ANR dialog: Application Not Responding.

In puzzle 0 the letter seemed to go straight into the clerk's tray. Now you know the real hand-off: a Binder thread receives it and posts it with a Handler.

The analogy

One clerk at a desk with an in-tray. The clerk takes one letter at a time and finishes it before the next. Anyone, including the post office's couriers, can drop letters in through the slot (a Handler).

Check yourself

onCreate()Your override of this method is what the main thread calls when system_server says "create". 1864 protected void onCreate(@Nullable Bundle savedInstanceState) {Activity.java, line 1864 · AOSP frameworks/base ↗ calls Thread.sleep(6000) and the user taps the screen. What happens?

Not quite. Touches are letters for the main thread too, so they wait in its queue behind the sleeping onCreate.

Right. One message at a time: the touch waits behind the sleeping onCreate(), and after 5 seconds the system shows an ANR.

Not quite. Nothing is cancelled: the system shows a dialog and the user decides. Watch what stays stuck.

What does handler.post { … }post() wraps your code in a Message and puts it in this Handler's queue. 438 public final boolean post(@NonNull Runnable r) { 439 return sendMessageDelayed(getPostMessage(r), 0); 440 } … 782 private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, 783 long uptimeMillis) { 784 msg.target = this; … 789 }Handler.java, line 438 · AOSP frameworks/base ↗ do?

Right. A Handler is the slot into one Looper's queue. Nothing runs until that thread reaches the message.

Not quite. A Handler doesn't start anything: it only drops a letter into one queue. The code runs later, on that queue's thread.

A Binder thread receives "resume your screen". How does it reach your onCreate-style code?

Right. The Binder thread hands the work to the main thread through its queue, so all UI code runs on one thread.

Not quite. Screen code must run on the main thread, in order. The courier drops a letter in the queue instead.

Sources

What you now know

The full chain: system_server decides → Binder carries the letter → a Binder thread posts it with a Handler → the main thread's Looper runs it → your code. Keep every main-thread message short.