You write this:
val speed = properties.getFloatProperty(PERF_VEHICLE_SPEED, 0)It looks like a method call. It is not. That line crosses a process boundary, goes through the Linux kernel, wakes a thread in another process, and blocks your thread until an answer comes back.
Understanding what actually happens explains a surprising number of automotive bugs — including ones that look like rendering problems and are not.
Why processes are separate at all#
Android isolates things. Your app is one process. is another. The is a third. None of them can read each other's memory.
That mechanism is .
What AIDL actually generates#
is a small language for describing what one process will let another ask it. You write an interface; the build generates the plumbing.
interface ICarComfort {
int getMassageIntensity(int seatAreaId);
void setMassageIntensity(int seatAreaId, int level);
}From that, the build produces two classes you never write by hand:
A Proxy, used on the calling side. It takes your arguments, packs them into a buffer, and sends them.
A Stub, used on the receiving side. It unpacks the buffer, works out which method was meant, and calls your real implementation.
public int getMassageIntensity(int seatAreaId) throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
try {
data.writeInterfaceToken(DESCRIPTOR); // "you are talking to ICarComfort"
data.writeInt(seatAreaId); // pack the argument
// This is where the process boundary is crossed. It BLOCKS.
mRemote.transact(TRANSACTION_getMassageIntensity, data, reply, 0);
reply.readException();
return reply.readInt(); // unpack the result
} finally {
reply.recycle();
data.recycle();
}
}public boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
switch (code) {
case TRANSACTION_getMassageIntensity: {
data.enforceInterface(DESCRIPTOR);
int seatAreaId = data.readInt(); // unpack
int result = this.getMassageIntensity(seatAreaId); // YOUR method
reply.writeNoException();
reply.writeInt(result); // pack the answer
return true;
}
}
}A Parcel is a byte buffer, not an object#
A Parcel is a flat sequence of bytes with a cursor. writeInt appends four
bytes; readInt consumes four bytes.
Crossing the boundary#
Step by step:
- The Proxy packs arguments into a
Parcel. - It calls
transact(), which issues anioctlon/dev/binder. - The kernel copies the data once into a memory region the target process mapped at startup. One copy, not two — this is the main reason Binder is fast.
- The kernel wakes a thread in the target's binder thread pool.
- That thread runs
onTransact, which calls the real method. - The reply is packed and travels back the same way.
- Your thread, which has been blocked this whole time, wakes up with the answer.
The two limits that cause real bugs#
The thread pool is finite#
Every process that serves Binder calls has a pool of threads to handle them. It is not unlimited — commonly around 15, and configurable per process.
When every thread is busy, new incoming calls queue.
adb shell dumpsys activity service com.android.car | grep -i binder
adb shell perfetto -o /data/misc/perfetto-traces/t -t 15s sched binder_driver amThe transaction buffer is about 1 MB — per process#
Each process has one shared buffer for all in-flight transactions. Roughly 1 MB, shared with everything else happening at that moment.
oneway: the escape from blocking#
Marking a method or an interface oneway makes it fire-and-forget.
oneway interface ICarComfortListener {
void onIntensityChanged(int seatAreaId, int level);
}The call is queued and returns immediately. There is no return value and no way to know it arrived.
Identity: who is calling?#
Binder carries the caller's identity into the receiving process.
int uid = Binder.getCallingUid();
int pid = Binder.getCallingPid();
// This is what permission enforcement is built on
mContext.enforceCallingOrSelfPermission(PERMISSION, "setMassageIntensity");There is a trap:
long token = Binder.clearCallingIdentity();
try {
// Now running as the SERVICE's identity, not the caller's.
// Needed to reach something the caller cannot — but you have just
// dropped the caller's permission context entirely.
doPrivilegedThing();
} finally {
Binder.restoreCallingIdentity(token); // ALWAYS in a finally
}Forgetting the restore leaves the thread running with elevated identity for
whatever it handles next. It is a genuine security bug and it is easy to write.
Death: knowing when the other side is gone#
Processes die. If you hold a reference to a dead one, calls throw.
binder.linkToDeath({
// Car Service died. Every manager you cached is now useless.
reconnect()
}, 0)The three binder domains#
Android runs more than one binder driver, to keep boundaries honest.
| Device node | Used for |
|---|---|
/dev/binder | Framework ↔ apps — the ordinary case |
/dev/hwbinder | HIDL interfaces, the older HAL mechanism |
/dev/vndbinder | Vendor process ↔ vendor process |
AIDL at the two levels you will meet#
The same technology appears twice in the automotive stack, and it is worth seeing that it is the same thing.
At the app level#
val properties = car.getCarManager(Car.PROPERTY_SERVICE) as CarPropertyManager
val speed = properties.getFloatProperty(PERF_VEHICLE_SPEED, 0)CarPropertyManager is a friendly wrapper. Underneath it is an AIDL proxy for
ICarProperty, sending a binder transaction to Car Service.
At the HAL level#
interface IVehicle {
void getValues(in IVehicleCallback callback, in GetValueRequests requests);
void setValues(in IVehicleCallback callback, in SetValueRequests requests);
VehiclePropConfigs getAllPropConfigs();
}Same mechanism, different interface, and one notable difference: the VHAL's methods take arrays and answer through a callback rather than returning a value.
Practical rules#
- Never make a blocking Binder call on the UI thread. It is a call into another process, and you cannot bound how long it takes.
- Make callbacks
oneway. - Never hold a lock across a Binder call, in either direction.
- Do not pass large payloads. Pass a descriptor.
- Check permission on the first line of every Binder method.
- Always
restoreCallingIdentityin afinally. - Enable
binder_driverin traces. Automotive problems frequently live in a process you are not profiling.
Next#
Car Service itself — the process on the other end of most of these calls.

