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 nowpostDelayed 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
BroadcastReceiverthat hasn't finished in time (5 seconds while the app has an activity in the foreground, longer otherwise); - a service whose
onCreate/onStartCommand/onBinddoesn't return within a few seconds; - a service started with
startForegroundService()that doesn't callstartForeground()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() )}Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.