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:
| Mechanism | Problem |
|---|---|
| Pipes/FIFOs | Unidirectional, no object passing |
| Sockets | Overhead, no Android identity |
| Shared memory | Synchronization complexity, no security model |
| Binder | Designed 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
| Concept | Rule |
|---|---|
| 1 MB limit | Never put Bitmaps or large byte arrays in Intents/Bundles |
| Synchronous | Binder calls block the caller — always call from background thread |
| Thread pool | 15 threads max per process; avoid holding Binder threads |
| Large data | Use ParcelFileDescriptor or URI references, not raw bytes |
| Batching | Minimize round-trips by combining multiple operations into one call |