Looper, Handler and the main thread

Performance Mid-level Kotlin 2.0+ API 24+ Reviewed By Best Latest Apps

Predict the output

A tap handler runs for 300 ms on the main thread at 60 Hz. About how many frames are dropped?

button.setOnClickListener {
    val start = System.nanoTime()
    while (System.nanoTime() - start < 300_000_000L) { /* busy */ }
}
Options
Hint 1 of 1

How long is one frame at 60 Hz?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A About 17

Correct. 300 ms spans about 18 frame slots of 16.7 ms. The frame that finally draws uses one, so about 17 are skipped.

Why not the others?
  • B 1. Every frame due during those 300 ms is missed, not just one.
  • C 0, drawing happens on the RenderThread. The RenderThread needs the main thread to produce each frame's display list first.
  • D 300. Frames come every 16.7 ms, not every millisecond.
Six frames at 60 Hz: normal frames under 16.7 ms, then a 300 ms tap handler blocks frames.

Illustrative, based on Compose and Android rules; not a device recording

Text version
Normal
FrameWork (ms)Result
F17on time
F29on time
F36on time
F48on time
F57on time
F69on time

0 dropped frames at 60 Hz (16.7 ms budget); ANR dialog at 5 s: no.

300 ms handler
FrameWork (ms)Result
F17on time
F2300⏱ late (the next 17 dropped)
F3–✕ dropped (F2 still running)
F4–✕ dropped (F2 still running)
F5–✕ dropped (F2 still running)
F6–✕ dropped (F2 still running)
F7–F19–✕ dropped ×13 (F2 still running)

17 dropped frames at 60 Hz (16.7 ms budget); ANR dialog at 5 s: no.

30-second answer

The main thread runs a Looper: an endless loop that takes the next Message from its MessageQueue and runs it. Input events, lifecycle callbacks, drawing (via Choreographer) and your posted Runnables are all messages on that queue. A Handler posts messages to a specific Looper. If one message takes too long, everything behind it waits, which shows up as jank or an ANR.

Practise saying it · 35 s. No recording, just a timer

Explain Looper, MessageQueue and Handler.

Say your answer out loud, then check which of these points you covered:

  • Main thread runs a Looper
  • Everything is a message
  • Handler posts to a Looper
  • Slow messages block the rest

Deep dive

Dispatchers.Main posts coroutine resumptions to the main Looper through a Handler.

HandlerThread gives a background thread with its own Looper, used by some legacy APIs.

Choreographer schedules frame work on vsync, every 16.7 ms at 60 Hz.

An IdleHandler runs when the queue is empty, handy for deferred startup work.

Common mistakes

  • Thinking coroutines on Main avoid the queue; they're messages too.
  • Posting heavy work with Handler(Looper.getMainLooper()).post.
  • Leaking an Activity through a delayed Handler message.

Senior take & follow-ups

Draw the queue: input, frame, your code. Then say why one slow message hurts.

Follow-up questions

Think of your answer first, then open the hint.

  1. How does a delayed message leak an Activity?

    Show hint

    The message holds the Runnable, which holds the Activity, until it runs.

  2. What schedules drawing?

    Show hint

    Choreographer, on each vsync.

Suggest a fix if something here is wrong or out of date.