Why Memory Addresses Fail Across Processes
When you invoke a method on an object in the same application, you are simply passing a memory address. The caller and the receiver share the same memory space. But when applications communicate over Binder, their memory spaces are completely isolated. Passing a memory address across a process boundary is useless because that address does not exist in the target process.
To pass a custom data structure between applications, Android must convert the object into a format that can cross the boundary. The target process then reconstructs a fresh copy from that data. You might immediately think of standard Java Serialization for this task. However, standard serialization relies heavily on reflection. Reflection generates excessive garbage collection overhead and slows down inter-process communication significantly.
Android created the Parcelable interface and the Parcel buffer as a high-performance alternative. A Parcel is a highly optimized, contiguous block of memory. A Parcelable is a strict contract that tells Android exactly how to flatten your object's fields into that buffer sequentially. This raw sequential writing avoids reflection entirely. This speed makes process communication fast enough to support high-speed user interface operations.
Flattening the Object into a Buffer
Since the Binder driver only understands raw bytes, you have to manually define how your object translates into a byte stream. The Parcelable interface requires two main components. You need a writeToParcel() method to serialize the data and a static CREATOR field to deserialize it.
When you write to a Parcel, you append primitive data types in a specific order. The CREATOR then reads those types back out in the exact same sequence to reconstruct the object.
This flowchart illustrates how the system flattens a custom object into a buffer. This visualizes the lifecycle of the object as it crosses the process boundary. Watch how the object becomes a raw buffer in the kernel space before being rebuilt in the service process.
In practice, the client calls writeToParcel() to push primitives into the memory buffer. The Binder driver moves that buffer into the target process space. Finally, the service uses the CREATOR to read the buffer sequentially and instantiate a fresh copy of the object.
Here is what that implementation looks like for a simple user profile object.
package com.example.android.ipc;
import android.os.Parcel;
import android.os.Parcelable;
public class UserProfile implements Parcelable {
public int id;
public String name;
public UserProfile(int id, String name) {
this.id = id;
this.name = name;
}
protected UserProfile(Parcel in) {
id = in.readInt();
name = in.readString();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(id);
dest.writeString(name);
}
public static final Creator<UserProfile> CREATOR = new Creator<UserProfile>() {
@Override
public UserProfile createFromParcel(Parcel in) {
return new UserProfile(in);
}
@Override
public UserProfile[] newArray(int size) {
return new UserProfile[size];
}
};
}
Common Mistake: The order of operations in
writeToParcel()must strictly match the constructor that reads from theParcel. The buffer maintains a pointer that advances with every read. If you write an integer and then a string, but attempt to read the string first, the pointer will interpret the wrong bytes. This causes a corrupted state or a crash.
Teaching the Compiler About Your Class
The AIDL compiler generates the Java stubs and proxies needed for communication. By default, this tool only understands basic primitives like integers and strings. The system has no idea that your custom Java class exists. If you try to use your new object in an interface immediately, compilation will fail.
You must explicitly declare the class in an AIDL file so the framework can safely generate code that references it. You do this by creating a file with the exact same name as your class, but with an .aidl extension.
// UserProfile.aidl
package com.example.android.ipc;
parcelable UserProfile;
This declaration acts as a forward reference. The statement signals to the AIDL compiler that a Java class named UserProfile implements the Parcelable interface and handles its own marshalling. The compiler can now confidently include your object in its generated methods.
Directional Tags and Memory Optimization
Once you declare your object, you can use it in other interfaces. However, copying data across boundaries takes time and memory. If a service only needs to read data without modifying it, copying the object back to the client wastes resources.
AIDL requires you to specify a directional tag for any custom object parameter. These tags dictate which process is responsible for marshalling the data. They tell the compiler whether to serialize the object from the client to the service, from the service to the client, or both.
// IUserManager.aidl
package com.example.android.ipc;
import com.example.android.ipc.UserProfile;
interface IUserManager {
// Client serializes, Service deserializes
void updateUser(in UserProfile profile);
// Service returns an initialized object
UserProfile getUser(int id);
}
The in tag means the client serializes the object and sends it to the service. Modifications the service makes to the object will not reflect back on the client side.
If you use an out tag, the client actually passes an empty object. The service populates the object and serializes it back to the client. The framework optimizes this transaction to prevent unnecessary memory allocations.
An inout tag means the system serializes the object in both directions. This provides the most flexibility but incurs the highest performance cost.
To build an efficient architecture, you must balance these costs carefully. You now know how to flatten complex objects into memory buffers and route them through AIDL. But before an application can send these objects, it needs to find the service in the first place. The next question is how the system actually registers and discovers these remote services at boot time.