AOSP Framework & Internals
6 min read

Binder Transactions

Learn about Binder Transactions.

Android isolates every application in its own memory space. A client process cannot directly execute a function in a system service because their memory regions are physically separated. This isolation protects the system, but it creates a massive hurdle when your application needs the platform to do something on its behalf. The platform requires a bridge that can safely pass execution and data across this gap.

Binder transactions are the physical mechanism for crossing that process boundary. A transaction is the complete lifecycle of packing data in a client, routing it through the kernel, and executing a method in a remote server. The Binder kernel driver sits between these processes and orchestrates the handoff. It acts as the ultimate authority for validating the sender, translating memory references, and managing thread execution. This provides a secure, structured way to execute code remotely.

The Command Protocol

We need a vocabulary to instruct the kernel driver to route our method calls. The kernel cannot understand raw Java or C++ objects. It only understands a specific set of integer commands and a flat byte buffer. Communication with the driver device file /dev/binder happens exclusively through the ioctl system call using the BINDER_WRITE_READ command.

The payload sent through this system call consists of commands starting with BC_ (Binder Command) and BR_ (Binder Return). You send BC_ commands to the driver. Then the driver sends BR_ commands back to your process. When a client wants to call a remote method, it constructs a payload and issues a BC_TRANSACTION command. The driver locates the target process and wakes up a sleeping thread in the server pool by delivering a BR_TRANSACTION.

The following sequence diagram visualizes how the kernel acts as a middleman for this entire exchange. You will see the client thread block while the driver wakes up the server. Watch for the specific commands that manage the request and response.

By default, this exchange represents synchronous behavior. Halting the client thread completely is required while waiting for the BR_REPLY. On the other side, the server unpacks the data, executes the requested method, and issues a BC_REPLY back to the driver. Then the driver wakes the sleeping client thread and delivers the result. Application code resumes exactly where it left off, but pausing the client thread entirely introduces a dangerous flaw.

Escaping the Block with One-way Calls

Synchronous calls create a performance trap. If your application calls a system service from its main UI thread, and that system service is under heavy load, your application freezes. The client thread is blocked waiting for a BR_REPLY that might take hundreds of milliseconds to arrive. This exact scenario is a leading cause of Application Not Responding crashes.

Developers can bypass this blocking behavior using one-way transactions. In AIDL interfaces, you enable this by adding the oneway keyword to a method signature. This sets a specific flag on the transaction payload called FLAG_ONEWAY.

When the kernel driver receives a BC_TRANSACTION with this flag, the routing logic changes. The driver still queues the BR_TRANSACTION on the target server. However, it immediately returns a BR_TRANSACTION_COMPLETE signal to the client instead of making the client thread wait for execution to finish. Client code resumes execution instantly. This provides a fire-and-forget mechanism for state updates, but the kernel still needs detailed instructions on what exactly to execute.

Warning: One-way transactions do not guarantee execution order if they are sent from different threads in the client. The server might process them concurrently.

Anatomy of the Transaction Data

The driver needs specific metadata to route a transaction correctly. Knowing which object to target, which method to invoke, and where the arguments live in memory is essential. The client packs all of this into a struct called binder_transaction_data.

This struct acts as the envelope for your remote procedure call. Inside, it contains a target handle pointing to the specific Binder object in the remote process. You will also find a 32-bit code integer that identifies the exact method to execute. A flags bitmask controls behaviors like the one-way routing we just discussed. Finally, the actual method arguments are serialized into a payload buffer referenced by the data field.

The most critical field in this struct is the offsets array. A transaction payload often contains complex objects like nested Binder references or file descriptors. Moving these objects across the process boundary requires translation by the kernel. This array tells the kernel exactly where these special objects reside inside the flat data buffer. From there, the driver parses the array, intercepts the file descriptors or handles, and safely duplicates them into the target process space. Such routing works perfectly for straightforward calls, but it creates complications when a server needs to talk back.

Handling Recursive Callbacks

A complex interaction might require the server to call back into the client to fetch more information. If Process A makes a synchronous call to Process B, the thread in Process A is blocked waiting for a reply. In this state, if Process B attempts a synchronous call back to Process A, you risk a deadlock. Process A has no available threads to handle the incoming request because its primary thread is stuck waiting for Process B.

The Binder driver prevents this deadlock by tracking blocked threads. Native support for nested transactions handles this scenario gracefully. The driver recognizes that the thread in Process A is currently suspended waiting for Process B.

Instead of spawning a new thread or failing the call, the driver pushes the new BR_TRANSACTION directly onto Process A's blocked thread. Waking up, the thread processes the callback, returns a reply, and then goes back to waiting for the original transaction to finish. The kernel essentially treats the cross-process exchange as a standard function call stack.

Understanding these mechanics reveals how Android maintains a fluid user experience while isolating components. You now know how the kernel routes commands, translates memory handles, and prevents deadlocks during callbacks. The next logical step is seeing how an application acquires the initial handle to a remote service in the first place.