androidengineers.Book a session

Android Architecture Fundamentals

Binder IPC: Concepts & Performance

article30 minHard

Almost every Android API call — startActivity(), getSystemService(), sendBroadcast() — crosses a process boundary via Binder. Understanding how Binder works helps you avoid performance pitfalls and design efficient cross-process APIs.

Why Binder?

Traditional IPC mechanisms have limitations on Android:

MechanismProblem
Pipes/FIFOsUnidirectional, no object passing
SocketsOverhead, no Android identity
Shared memorySynchronization complexity, no security model
BinderDesigned for Android: secure identity, object-oriented, efficient

Binder uses a single copy_from_user kernel call per transaction (pipes use two). It integrates with Android's security model: each Binder call carries the caller's UID/PID, which the system uses for permission checks.

The Binder Model

Client Process                   Server Process
┌─────────────────┐              ┌──────────────────┐
│  Proxy (stub)   │──Binder IPC──│  Stub (impl)     │
│  IBinder ref    │              │  Binder object   │
└─────────────────┘              └──────────────────┘
         │                                │
         └──────── Binder Driver ─────────┘
                  (kernel module)

The Proxy lives in the client process and marshals arguments into a Parcel. The Stub lives in the server process and unmarshals the Parcel, calls the real implementation, and returns the result.

AIDL: Defining a Binder Interface

// IMyService.aidl
interface IMyService {
    String processData(in String input);
    void uploadFile(in ParcelFileDescriptor fd);  // for large data
}

Android generates the proxy/stub boilerplate from .aidl files:

// Server side — implement the stub
class MyService : Service() {
    private val binder = object : IMyService.Stub() {
        override fun processData(input: String): String {
            // runs on Binder thread pool, not main thread
            return input.reversed()
        }

        override fun uploadFile(fd: ParcelFileDescriptor) {
            // read from fd directly — no Binder copy for the data
            FileInputStream(fd.fileDescriptor).use { stream ->
                // process stream
            }
        }
    }

    override fun onBind(intent: Intent) = binder
}

// Client side
val service = IMyService.Stub.asInterface(iBinder)
val result = service.processData("hello")  // synchronous cross-process call

The 1 MB Transaction Limit

Binder uses a 1 MB shared memory buffer per process. All in-flight Binder transactions for a process share this buffer. Sending a large Parcel (images, byte arrays) risks TransactionTooLargeException.

// ❌ Never do this — risks TransactionTooLargeException
val intent = Intent(this, MyActivity::class.java)
intent.putExtra("bitmap", largeBitmap)  // Bitmap in Bundle = Binder transaction

// ✅ Pass a reference (file path, content URI, ID) instead
intent.putExtra("imageUri", uri.toString())

For large data transfers, use ParcelFileDescriptor (file descriptor passing — the data stays in a file, only the FD crosses the boundary) or shared memory (MemoryFile).

Binder Thread Pool

Each process that receives Binder calls has a thread pool of up to 15 threads (+ the main thread). If all threads are busy, incoming calls block.

// This runs on a Binder thread, NOT the main thread
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
    // Be careful with threading here
    return super.onTransact(code, data, reply, flags)
}

Binder calls are synchronous by default — the caller blocks until the server returns. Calling a Binder method on the main thread that does slow work in the server will cause an ANR.

Performance Guidelines

// ❌ Calling a slow Binder method on the main thread
val result = remoteService.heavyOperation()  // blocks UI thread

// ✅ Always call Binder from a background thread
viewModelScope.launch(Dispatchers.IO) {
    val result = remoteService.heavyOperation()
    _state.value = result
}

Reduce Binder calls by batching:

// ❌ Many small Binder calls
items.forEach { item ->
    remoteService.processItem(item)  // N cross-process round-trips
}

// ✅ One Binder call with a list
remoteService.processItems(items)  // 1 round-trip

How System Services Use Binder

Every call to ActivityManager, PackageManager, WindowManager, etc. is a Binder IPC to a system server process.

// This innocent line crosses a Binder boundary
val pm = context.packageManager
val isInstalled = pm.getPackageInfo("com.example", 0) != null
// getPackageInfo → Binder → system_server → PackageManagerService

Avoid calling system services in tight loops or hot paths.

Key Takeaways

ConceptRule
1 MB limitNever put Bitmaps or large byte arrays in Intents/Bundles
SynchronousBinder calls block the caller — always call from background thread
Thread pool15 threads max per process; avoid holding Binder threads
Large dataUse ParcelFileDescriptor or URI references, not raw bytes
BatchingMinimize round-trips by combining multiple operations into one call

YOUR LEARNING JOURNEY

0 of 177 available lessons completed

Progress saved in this browser. No account needed.
Binder IPC: Concepts & Performance | Android System Design | Android Engineers