Looper, Handler & Threading Quiz
ANDROID › System
How many Loopers can a single thread have at once?
- Exactly one; Looper is thread-local to each thread.
- One per Handler attached to the thread's message queue.
- As many as you invoke Looper.prepare() on that thread.
- Unlimited, limited only by available memory and CPU.
Answer: Exactly one; Looper is thread-local to each thread.
A Looper is thread-local; calling Looper.prepare() a second time on the same thread throws because the thread already has a Looper.
Which call correctly posts work to the UI thread from a background thread?
- Handler().post(runnable) without binding it to the main Looper
- Looper.prepare(); handler.post(runnable) after setup on this thread
- Handler(Looper.getMainLooper()).post(runnable) to the main thread
- Looper.getMainLooper().post(runnable) as if Looper could enqueue work
Answer: Handler(Looper.getMainLooper()).post(runnable) to the main thread
Binding a Handler to Looper.getMainLooper() and calling post() enqueues the Runnable on the main thread's MessageQueue; Looper itself has no post() method.
What must you call to make a plain background thread able to receive Handler messages?
- Thread.setDaemon(true) only marks it as a background daemon thread
- Looper.prepare() creates the queue, then Looper.loop() runs it
- Looper.getMainLooper() returns the UI thread’s Looper, not this one
- Handler.obtainMessage() just creates a Message; it does not start looping
Answer: Looper.prepare() creates the queue, then Looper.loop() runs it
Looper.prepare() creates the thread's Looper and MessageQueue, and Looper.loop() starts dispatching messages so a Handler bound to it can run work.
Which scenario is the most direct cause of an ANR?
- Posting a delayed Runnable to a HandlerThread for later work
- Running a network request on Dispatchers.IO from a background scope
- Performing a synchronous disk read on the main thread in onCreate
- Creating a second Handler on the main Looper for extra callbacks
Answer: Performing a synchronous disk read on the main thread in onCreate
Blocking the main thread with synchronous I/O prevents it from servicing input events and rendering, which triggers an Application Not Responding error.
What is the difference between Looper.quit() and Looper.quitSafely()?
- quit() restarts the message loop, while quitSafely() merely pauses it in place
- quit() drops pending messages; quitSafely() first delivers those already due
- They are two identical aliases with no behavioral difference at all
- quitSafely() only works when called on the main thread's Looper
Answer: quit() drops pending messages; quitSafely() first delivers those already due
quit() terminates immediately and discards every pending message, while quitSafely() finishes dispatching messages whose time has arrived before stopping.
Why is the no-argument Handler() constructor deprecated?
- It leaks the thread's MessageQueue on every single message it posts
- It can only be used safely on API level 30 and below devices
- It implicitly uses the current thread's Looper, and crashes if none exists
- It blocks the calling thread entirely until handleMessage() returns each time
Answer: It implicitly uses the current thread's Looper, and crashes if none exists
Relying on the implicit current-thread Looper hides which thread the work runs on and throws if the thread has no Looper, so an explicit Looper should be passed.
What does a HandlerThread provide out of the box?
- A thread pool that runs posted tasks in parallel on many threads
- A thread that sets up its own Looper and runs the message loop
- Automatic execution of all work on the main UI thread by default
- A coroutine dispatcher that is backed by the shared IO pool
Answer: A thread that sets up its own Looper and runs the message loop
HandlerThread is a Thread that sets up its own Looper and runs the message loop, so a Handler attached to it serializes work on that single background thread.
Why is Message.obtain() preferred over constructing a Message with new Message()?
- It guarantees the Message is handled before any posted Runnable
- It automatically attaches the Message to the main thread's Looper
- It reuses a Message from a global pool, avoiding a new allocation
- It makes the Message immutable after it has been sent to a handler
Answer: It reuses a Message from a global pool, avoiding a new allocation
Message.obtain() pulls a recycled instance from Android's global Message pool, reducing garbage-collection pressure compared with allocating a new object each time.
When you call handler.postDelayed with a delay, how does the MessageQueue decide when to dispatch it?
- It inserts the message by absolute trigger time (uptimeMillis() + delay) and dispatches then
- It spawns a dedicated background timer thread that sleeps for the delay and then runs the Runnable
- It busy-waits on the Looper thread, spinning until the delay has fully elapsed
- It runs the Runnable immediately but hides its side effects until the delay passes
Answer: It inserts the message by absolute trigger time (uptimeMillis() + delay) and dispatches then
Delayed messages are stored with an absolute wake time based on uptimeMillis and the queue keeps them ordered by that time, blocking efficiently until the soonest one is due.
What does Looper.myLooper() return?
- Always the main thread’s Looper, no matter which thread calls it
- A new Looper instance created automatically for the current thread
- The Looper with the most queued messages at the moment of the call
- The Looper for the current thread, or null if none exists
Answer: The Looper for the current thread, or null if none exists
Looper.myLooper() reads the thread-local Looper for the current thread and returns null when the thread has not called Looper.prepare().
On Android, what does kotlinx.coroutines Dispatchers.Main ultimately use to resume coroutines on the UI thread?
- A separate HandlerThread distinct from the UI thread
- A Handler bound to the main thread's Looper
- The Dispatchers.IO background thread pool
- The legacy AsyncTask serial executor
Answer: A Handler bound to the main thread's Looper
The Android Main dispatcher is implemented on top of a Handler attached to Looper.getMainLooper(), so resumed continuations are posted as messages onto the main thread's queue.
You need a task to run roughly 15 minutes from now and to still execute even if the app's process is killed first. Which approach fits?
- Handler.postDelayed with a 15-minute delay; it vanishes when the process dies
- A HandlerThread that keeps a delayed Message; it stops once the app is killed
- WorkManager, which persists deferred work across process death and reboot
- Looper.loop() with Thread.sleep for 15 minutes; it's just in-memory waiting
Answer: WorkManager, which persists deferred work across process death and reboot
Handler/Looper scheduling lives only in memory and dies with the process, whereas WorkManager persists deferrable work and guarantees it runs even after the app is killed or the device reboots.
How do you cancel a Runnable you scheduled with handler.postDelayed before it executes, for example to debounce rapid input?
- Call handler.removeCallbacks(runnable) using the same Runnable
- Call Looper.quitSafely() on the main Looper to stop it early
- Set the Handler variable to null and let it be garbage-collected
- Nothing can cancel a posted Runnable; it always runs once queued
Answer: Call handler.removeCallbacks(runnable) using the same Runnable
removeCallbacks(runnable) removes any pending messages wrapping that exact Runnable from the MessageQueue, which is the standard way to implement debouncing with a Handler.
What is the purpose of registering an IdleHandler via MessageQueue.addIdleHandler?
- It raises the dispatch priority of the very next message queued behind it in line
- It blocks the queue until at least one new message gets enqueued
- It converts all pending delayed messages into immediate ones
- It runs a callback when the queue has no dispatchable message and goes idle
Answer: It runs a callback when the queue has no dispatchable message and goes idle
An IdleHandler is invoked when the Looper's MessageQueue runs out of currently dispatchable messages, letting you defer low-priority work until the thread would otherwise sit idle.