Zygote
Builder tracknerd

I06 — Looper lab

Rebuild the main thread's message loop by hand: a Looper, a Handler, a MessageQueue, and a choreographer-style frame callback. Then break it on purpose and watch the ANR.

3 minAPI 21–37 · 17verified 2026-10-27

Every Android app has one thread that is not allowed to block, and the reason is a loop that pulls messages from a queue. Building a small version of that loop is the fastest way to understand why the main thread behaves the way it does.

What you are building#

A single screen with buttons that post work to a Handler and a log that shows the order of execution. A second mode posts a long task to the main thread and demonstrates an ANR, so you can see the failure the design exists to prevent.

Steps#

  1. Create a Handler on a background Looper

    Create a HandlerThread, get its Looper, and create a Handler bound to it. Post a runnable that logs the thread name. That is the whole mechanism in miniature.1

    You should seeWork runs off the main thread.
  2. The Handler and the thread:

    val thread = HandlerThread("worker").apply { start() }
    val handler = Handler(thread.looper)
     
    handler.postDelayed({ Log.d("LooperLab", "100 ms") }, 100)
    handler.postDelayed({ Log.d("LooperLab", "50 ms") }, 50)
    handler.post { Log.d("LooperLab", "immediate") }
  3. Post with delays and order

    Post three runnables: immediate, 100 ms, and 50 ms. The order should be immediate, 50 ms, 100 ms. The queue is ordered by time, not by insertion.

    You should seeThe log shows the order you posted, with delays respected.
  4. Post from the main thread

    Post from a button click and log both the calling thread and the executing thread. They differ, and that difference is the point of a Handler.

    You should seeThe work runs on the background thread.
  5. Build the frame callback

    Use Choreographer to post a frame callback that logs its frame time and re-posts itself. Compare the frame times with the wall clock: the callback is aligned to vsync, not to your delay.

    You should seeA callback runs at a steady cadence.
  6. Block the main thread

    On a button click, sleep on the main thread for longer than the input dispatch timeout. The UI freezes and the system shows an ANR. The log has an ANR entry, and the trace has the blocked interval.

    You should seeAn ANR dialog or a frozen UI.
  7. Read the ANR trace

    Pull the ANR trace from the device and find the main thread's stack. It points at the sleep, which is the evidence the system uses.

    You should seeThe main thread stack, blocked in your sleep.
  8. Fix it properly

    Move the long task to the HandlerThread and post the result back to the main thread. The UI stays responsive and the work still completes.

    You should seeNo ANR, and the work still runs.
Reproduce and read an ANR
adb shell am start -W -n com.example.looperlab/.MainActivity
adb shell input tap 540 1200
adb logcat -b system -d | grep -i "ANR in"
adb shell ls /data/anr
Example output
Illustrative — the trace path and timings differ.

Verification#

The ordering test is the cleanest check: post with 0, 100 and 50 ms delays and confirm the execution order. Then confirm that the frame callback's timestamps cluster at the vsync interval rather than at arbitrary times.

What you learned#

  • A Handler is a queue, not a thread. It posts to a Looper, which owns the thread.
  • The queue is time-ordered. Delays are respected; posting order is not the only ordering.
  • The main thread is single and serial. That is the design, and blocking it is the only way to ANR.
  • The Choreographer is a vsync-aligned Handler. Frame callbacks are messages like any other.

Read Looper, Handler and MessageQueue for the real implementation, and Lab 12 — Jank for what happens when the queue is overloaded.

Extend it#

  1. Add a MessageQueue idle handler and log when the queue goes idle; it is the hook IdleHandler exposes for deferred work.
  2. Add a second HandlerThread and compare throughput with one thread doing both jobs.
  3. Add a SyncBarrier test by posting an asynchronous message and observing that it can jump ahead of synchronous ones.

Sources#

Sources

Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.

  1. docsProcesses and threads overview | App qualitydeveloper.android.comaccessed 2026-10-27↩
  2. docsAnrdeveloper.android.comaccessed 2026-10-27↩
  3. docsOverview of system tracingdeveloper.android.comaccessed 2026-10-27↩
  4. docsCompose performancedeveloper.android.comaccessed 2026-10-27↩
  5. docsAndroid platform architecture (AOSP)source.android.comaccessed 2026-10-27↩