androidengineers.Book a session

Threading & Concurrency

Handler/Looper Basics to Internals

article25 minMedium

Before coroutines existed, Handler was the mechanism for communicating between threads on Android. It's still used today inside the framework, inside WorkManager's internals, and in any code that needs precise control over thread dispatch. Understanding it deeply prevents subtle bugs and memory leaks.

The Mental Model

Think of Handler + Looper + MessageQueue as a mailbox system:

  • Looper — the mail carrier that runs continuously on one thread
  • MessageQueue — the physical mailbox; messages wait here in time order
  • Handler — the address label; it specifies which mailbox gets the message and who processes it
Thread A                     Thread B (has a Looper)
────────                     ──────────────────────
handler.post { doWork() }    Looper.loop() → picks up Runnable → runs doWork()

A Handler is permanently bound to the Looper of the thread where it was created. You can create a Handler on the main thread and post to it from any background thread — the Runnable always executes on the main thread.

Creating Handlers

Posting Runnables (most common):

// Handler tied to the main thread
val mainHandler = Handler(Looper.getMainLooper())

// From any background thread:
mainHandler.post {
    // Runs on main thread
    textView.text = "Done"
}

mainHandler.postDelayed({
    // Runs 500ms from now on the main thread
    hideLoadingSpinner()
}, 500L)

Sending Messages (more structured):

companion object {
    const val MSG_UPDATE_PROGRESS = 1
    const val MSG_DOWNLOAD_COMPLETE = 2
}

val handler = object : Handler(Looper.getMainLooper()) {
    override fun handleMessage(msg: Message) {
        when (msg.what) {
            MSG_UPDATE_PROGRESS -> progressBar.progress = msg.arg1
            MSG_DOWNLOAD_COMPLETE -> showResult(msg.obj as DownloadResult)
        }
    }
}

// From a background thread:
handler.sendMessage(Message.obtain(handler, MSG_UPDATE_PROGRESS).apply {
    arg1 = 75
})

Using Message.obtain() instead of Message() reuses pooled Message objects — important in high-frequency dispatch scenarios.

HandlerThread: A Thread With a Built-in Looper

Preparing your own Looper on a raw Thread is boilerplate. HandlerThread does it for you:

class ImageProcessor {
    private val handlerThread = HandlerThread("image-processor")
    private lateinit var handler: Handler

    fun start() {
        handlerThread.start()
        // getLooper() blocks until Looper.prepare() has run on the new thread
        handler = Handler(handlerThread.looper)
    }

    fun processImage(bitmap: Bitmap) {
        handler.post {
            // Runs on the image-processor thread, not main
            val result = applyFilters(bitmap)
            mainHandler.post { displayResult(result) }
        }
    }

    fun stop() {
        handlerThread.quitSafely() // drains pending messages before quitting
    }
}

quitSafely() vs quit():

  • quit() — drops all pending messages immediately
  • quitSafely() — processes messages already due, drops future-scheduled ones

Always use quitSafely() unless you're shutting down urgently.

Message Internals: The Object Pool

Android maintains a global pool of Message objects (max 50) to avoid GC pressure. When you call msg.recycle() after dispatch, the message is cleared and returned to the pool. When you call Message.obtain(), you get a recycled instance instead of allocating.

Message pool (max 50)
┌────┬────┬────┬────┐
│ M1 │ M2 │ M3 │... │  ← obtain() pulls from here
└────┴────┴────┴────┘
         ↑
   recycle() returns here

Never hold a reference to a Message after you've passed it to a Handler — the framework owns it from that point and may recycle it.

The Classic Handler Memory Leak

This pattern leaks the Activity:

// ❌ Leaks MyActivity
class MyActivity : AppCompatActivity() {
    private val handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            // Implicit reference to MyActivity.this
            updateUI()
        }
    }

    override fun onCreate(...) {
        handler.postDelayed({ finish() }, 60_000) // 60 second delay
    }
}

The anonymous Handler subclass holds an implicit reference to MyActivity. If the Activity is destroyed before the 60-second delay fires, the GC cannot collect it — the Handler still holds a reference from the MessageQueue's pending message.

Fix: use a WeakReference

class MyActivity : AppCompatActivity() {
    private val handler = Handler(Looper.getMainLooper())
    private val finishRunnable = Runnable { finish() }

    override fun onCreate(...) {
        handler.postDelayed(finishRunnable, 60_000)
    }

    override fun onDestroy() {
        super.onDestroy()
        handler.removeCallbacks(finishRunnable) // ← critical
    }
}

Or even better with coroutines:

lifecycleScope.launch {
    delay(60_000)
    finish() // cancelled automatically when Activity is destroyed
}

Barrier Messages: Synchronous Barriers

The MessageQueue supports synchronous barriers — an advanced mechanism used by the Choreographer internally. A barrier blocks synchronous messages while allowing async messages to jump the queue.

// Mark a message as async so it can pass synchronous barriers
val msg = Message.obtain(handler, { drawNextFrame() })
msg.isAsynchronous = true
handler.sendMessage(msg)

The framework uses this to ensure VSYNC callbacks (frame rendering) always fire ahead of pending synchronous messages — guaranteeing the UI stays responsive even when the queue is busy.

When to Use Handler Today

ScenarioRecommendation
UI updates from background threadlifecycleScope.launch(Dispatchers.Main) — simpler
Delayed UI action (< lifecycle scope)handler.postDelayed() with removeCallbacks on destroy
Low-level framework integrationHandler is appropriate
Custom thread message loopHandlerThread + Handler
Periodic background workWorkManager — not Handler

Handler is not obsolete — it's used inside the Android framework itself. But for new application code, Kotlin coroutines with the right Dispatcher express the same intent more safely and with automatic cancellation tied to the lifecycle.

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Handler/Looper Basics to Internals | Android System Design | Android Engineers