Why Manual IPC is a Nightmare
Android strictly isolates every app in its own memory space. When your app needs to talk to a system service or another app, you cannot just pass a Java object reference. The other process cannot read your memory directly. Instead, you have to serialize your data, send it to the operating system kernel, and let the kernel hand it to the target process.
The following flowchart illustrates the mandatory route for cross-process communication. Seeing this layout helps you understand why direct object sharing is impossible. Pay attention to how the kernel acts as an inescapable middleman between the two isolated processes.
The framework uses the Binder driver for this communication. Binder expects data packed tightly into a Parcel object. Writing the code to manually pack and unpack every integer, string, and custom object into a Parcel is incredibly tedious. Doing this by hand invites bugs because the packing order must exactly match the unpacking order.
This is why the Android Interface Definition Language (AIDL) exists. AIDL is a code generator. You write a simple interface defining your methods, and the Android build system generates all the ugly Binder transaction boilerplate for you.
You get to call remote methods as if they were local. AIDL handles the heavy lifting underneath.
Inside the Generated Code: Stub and Proxy
When you create an .aidl file, you are only defining a contract. A contract cannot execute code on its own. You need a translation layer to move data across the process boundary.
During compilation, the toolchain reads your .aidl file and generates a Java interface. Inside this interface, the system creates two critical inner classes. One is the Stub, which lives in the service process. The other is the Proxy, which lives in the client process.
The following sequence diagram shows exactly how these generated classes work together. Seeing this flow helps you understand how the client code is shielded from the underlying Binder transactions. Pay attention to how the Proxy and Stub act as mirror images of each other across the boundary.
Notice how the Proxy and Stub act as translators. When the client calls a method, the Proxy intercepts it, packs the arguments into a Parcel, and calls the Binder driver. On the receiving end, the Stub.onTransact() method unpacks the Parcel and calls your actual service implementation. This abstraction makes cross-process calls look exactly like local method calls.
Defining the Contract and Directional Tags
The build system cannot guess how your data should move across the process boundary. Primitive types like integers always travel from the client to the service. For complex custom objects, the system needs explicit instructions.
Copying large custom objects back and forth across the IPC boundary takes time and memory. You need a way to tell the Binder system whether the service only needs to read the object, only needs to write to it, or needs to do both. This is where directional tags come in.
An AIDL file looks remarkably like a standard Java interface, but it requires directional tags for any non-primitive parameters. You must specify in, out, or inout.
// IMyService.aidl
package com.example.android.ipc;
interface IMyService {
int getStatus();
void performAction(in String data);
void updateList(inout List<String> items);
}
Tip: Always use the most restrictive tag possible. If a service only needs to read data, use
in. If it only needs to populate an empty object provided by the client, useout.
Using inout everywhere forces the system to serialize and deserialize the object twice. That wastes CPU cycles. These tags act as strict instructions for the generated code. They dictate exactly when the Proxy should pack arguments and unpack results.
Implementing the Server and Client
Having a generated contract is useless without actual logic behind it. You still need to implement the service that does the work and the client that requests it.
On the service side, you must create a concrete instance of the generated Stub class. This is where you write the actual business logic for the methods you defined in the .aidl file. You then expose this implementation to clients by returning it from your service's onBind() method.
public class MyService extends Service {
private final IMyService.Stub mBinder = new IMyService.Stub() {
@Override
public int getStatus() throws RemoteException {
return 1;
}
@Override
public void performAction(String data) throws RemoteException {
Log.i("MyService", "Received: " + data);
}
@Override
public void updateList(List<String> items) throws RemoteException {
items.add("Processed");
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
For the client, you connect to the service using bindService(). When the connection is established, the system gives you a raw IBinder reference. Calling your methods on a raw IBinder is impossible. You must convert it into your specific interface using IMyService.Stub.asInterface().
private IMyService mService;
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
mService = IMyService.Stub.asInterface(service);
try {
int status = mService.getStatus();
Log.d("Client", "Service status: " + status);
} catch (RemoteException e) {
e.printStackTrace();
}
}
public void onServiceDisconnected(ComponentName className) {
mService = null;
}
};
Common Mistake: Forgetting to handle
RemoteException. The service process could crash while handling your request. The OS throws aRemoteExceptionto let the client know the transaction failed.
This conversion method handles a brilliant optimization. It checks if the service and client happen to be in the same process. If they are, it returns the actual Stub implementation to avoid IPC overhead completely. When they are in different processes, it returns the Proxy object that routes calls through Binder.
The Cost of Cross-Process Communication
AIDL makes IPC look simple by hiding the complexity behind familiar Java interfaces. You define the methods, handle the directional tags, and connect the components.
However, this convenience masks a serious performance cost. Every time you invoke a remote method, data is serialized, pushed through the kernel, and deserialized on the other side. This takes significantly more time than a local method call.
If you place a remote call on your app's main thread, you risk dropping frames or triggering an Application Not Responding error. The system handles the mechanics of communication perfectly, but you are responsible for performance. This reality forces engineers to think carefully about how to handle asynchronous IPC calls.