AOSP Framework & Internals
5 min read

Process Boundaries and Proxies

Learn about Process Boundaries and Proxies.

The Memory Isolation Problem

Your app components need to use system services constantly. When your Activity wants to open the camera or read location data, it expects to just call a method. But Linux strictly isolates process memory for security. You cannot pass a memory pointer from your app to a background system service.

Android solves this memory isolation using the Proxy pattern. The framework gives your app a local object that looks and acts exactly like the real remote service.

When you call a method on this local proxy, it intercepts the call. It packages the arguments into a byte buffer and ships them across the process boundary. The remote service receives the buffer, unpacks the data, runs the real method, and sends the result back.

This architecture creates the illusion that everything runs in the same memory space. Your application code stays simple and synchronous. The kernel handles all the complex routing securely in the background.

Speaking a Universal Language with IBinder

If every service used a custom format for cross-process communication, the system would drown in routing logic. The framework needs a standard way to verify that any object can send and receive messages. Without a common interface, the kernel would not know how to deliver payloads.

The IBinder interface provides this universal language. Every object that expects to receive calls from another process must implement it. It acts as the fundamental building block for all IPC in Android.

At its core, IBinder relies on a single method called transact(). This method acts as a universal choke point. It takes a byte payload and a destination identifier, then pushes the request down to the kernel driver.

Because every remote object is an IBinder, the framework only needs to know how to handle this one interface. It can route messages to any service in the system without caring about the specific implementation details.

Proxies, Stubs, and the Kernel Bridge

Your application cannot reach directly into the system server to execute code. It needs a safe intermediary to handle the transition from user space to kernel space.

Android divides the architecture into two distinct halves: a Proxy on the client side and a Stub on the server side.

The client process receives a BpBinder object, which acts as the Proxy. Calling a method on this proxy executes no local logic. It just packs the arguments and calls transact(). On the server side, a BBinder object acts as the Stub. It sits in a thread pool waiting for the kernel to deliver transactions. When a message arrives, the Stub unpacks the data and executes the real method on the service.

This strict separation keeps the memory spaces completely isolated.

We can visualize how a method call travels through these layers. This sequence diagram shows the complete round trip of an IPC request. Watch how the data flattens out to cross the kernel boundary and reconstructs on the other side.

Notice that the Client and Service never interact directly. The Proxy and Stub handle all the complex packaging logic. The framework generates these bridge classes for you, keeping the boilerplate out of your app code.

Packaging Data with Parcels

Virtual memory addresses mean nothing outside the process that created them. If you try to pass a raw pointer to another process, it will read garbage memory and crash.

Android flattens data into a format that can safely travel through the kernel using the Parcel class. A Parcel is a highly optimized container designed strictly for fast IPC.

When you pass a primitive value, the Parcel copies the bits directly into a byte buffer. For complex objects, the object must implement the Parcelable interface. This interface tells the Parcel exactly how to write the object fields sequentially into the buffer.

This flattening process allows complex data structures to survive the journey across process boundaries. The receiving process simply reads the bytes in the exact same order to reconstruct the object.

Common Mistake: Parcel is not a replacement for Java's Serializable. The framework team designed it strictly for active memory speed. You should never use a Parcel to save data to disk because the internal format changes frequently between Android versions.

Securing References with Binder Handles

The receiving process needs a way to identify the target remote object. However, the kernel cannot just hand out raw memory pointers due to security risks.

Instead of pointers, the kernel issues secure 32-bit integers called Binder handles.

Inside the kernel driver, an internal struct represents every active Binder object. When your app requests a reference to a system service, the kernel does not give you the struct. It gives you a handle like 1. The kernel maintains a private mapping table for your process. When you call transact(1, payload), the kernel looks up Handle 1 in your private table, finds the target struct, and wakes up the correct server process.

This abstraction ensures that a malicious app cannot spoof handles or hijack objects. A handle means absolutely nothing outside the context of the calling process.

Secure handles keep all process communication flowing safely. But they raise an interesting question. If your app needs a valid handle to talk to the WindowManager, how does it get that very first reference when it starts up?