Why Synchronous IPC Fails for Long Tasks
Android processes communicate via Binder using synchronous remote procedure calls by default. When your application requests data from a system service, the calling thread blocks until the service returns a result. This works perfectly for fast operations like retrieving a setting or checking a permission. But suppose the service needs to perform a slow operation, like scanning for nearby Bluetooth devices. If the application waits synchronously, its main thread hangs, and the system eventually triggers an Application Not Responding (ANR) crash.
We need a mechanism that allows the client process to request an operation and immediately resume its own work. The service must then have a way to reach back across the process boundary to notify the client when the result is ready.
The Callback pattern in AIDL solves this issue. Instead of returning a result directly, the primary service accepts a secondary AIDL interface from the client. Providing an implementation of this secondary interface effectively turns the client into a temporary server. The system service stores this interface reference and invokes its methods later.
To understand how this reversed communication flows, we will look at a sequence diagram. This visualizes the control flow when a client registers a callback and the service later triggers it, helping you see exactly when blocking occurs. Notice how the initial transaction completes immediately, while the data delivery happens as a separate asynchronous transaction.
The application registers the callback and resumes execution instantly. Later, the system service initiates a new transaction back to the application to deliver the results.
To implement this, you define two separate AIDL files. The first defines the callback interface that the client will implement.
// IResultCallback.aidl
package com.example.android.ipc;
interface IResultCallback {
void onResult(String resultData);
}
The second defines the primary service interface. It imports the callback interface and provides a method for the client to register it.
// IMyService.aidl
package com.example.android.ipc;
import com.example.android.ipc.IResultCallback;
interface IMyService {
void registerCallback(IResultCallback cb);
void unregisterCallback(IResultCallback cb);
}
Because the IResultCallback object crosses the Binder boundary, the system service receives a proxy object pointing back to the client. This inversion of control is powerful, but it creates a dangerous new problem for the system service.
Protecting the Service with One-Way Calls
When the system service invokes the callback on the client, it initiates a standard synchronous Binder transaction. This means the system service's Binder thread will block until the client finishes executing the callback. If the client application is poorly written, stuck in a deadlock, or paused by the debugger, the system service hangs waiting for it. System services are critical infrastructure, and they must never trust untrusted client applications to respond quickly.
To prevent malicious or buggy clients from stalling the system, callback interfaces must prevent blocking.
We achieve this using the oneway keyword in AIDL. When an interface or method is marked as oneway, the caller dispatches the transaction to the Binder driver and immediately returns without waiting for a response. The Binder driver delivers the message to the target process asynchronously.
You apply this keyword directly to the callback interface definition.
// IResultCallback.aidl
package com.example.android.ipc;
oneway interface IResultCallback {
void onResult(String resultData);
}
By making the callback asynchronous, the system service can notify hundreds of clients in rapid succession without risking thread starvation. However, non-blocking calls do not solve the issue of client lifecycle management. What happens if a client registers a callback and then immediately crashes?
Managing Client Death Safely
System services run for weeks or months, while client applications constantly start, crash, and get killed by the low memory killer. If a service stores callback references in a standard Java list, it will eventually accumulate dozens of dead proxies. Attempting to invoke a callback on a dead process throws an exception. More importantly, holding these dead references leaks memory and wastes CPU cycles.
A reliable mechanism is required to track which clients are still alive and automatically remove dead ones from the notification list.
The Android platform provides a specialized collection class specifically for this purpose called RemoteCallbackList. This class handles all the complex lifecycle and thread-safety requirements of AIDL callbacks. Under the hood, it uses a mechanism called a DeathRecipient. When you register a callback, RemoteCallbackList calls IBinder.linkToDeath() on the underlying binder proxy. If the client process dies, the Binder driver notifies the service, and the list automatically drops the dead reference.
The following example demonstrates how a system service utilizes this class in practice. It instantiates the list and uses it to broadcast results to all currently active clients.
private final RemoteCallbackList<IResultCallback> mCallbacks = new RemoteCallbackList<>();
@Override
public void registerCallback(IResultCallback cb) {
if (cb != null) {
mCallbacks.register(cb);
}
}
private void notifyClients(String data) {
// beginBroadcast returns the count of active callbacks
int count = mCallbacks.beginBroadcast();
for (int i = 0; i < count; i++) {
try {
mCallbacks.getBroadcastItem(i).onResult(data);
} catch (RemoteException e) {
// The RemoteCallbackList handles cleanup natively
}
}
// You must always finish the broadcast to release the internal lock
mCallbacks.finishBroadcast();
}
Common Mistake: Always call
finishBroadcast()in afinallyblock or immediately after your loop. Failing to call it will leave the internal lock held, causing the next call tobeginBroadcast()to deadlock the service.
By using RemoteCallbackList, the system service completely insulates itself from client instability. The combination of oneway interfaces and automatic death handling provides a resilient architecture for asynchronous communication. This pattern forms the foundation of almost every event-driven API in the Android framework, from location updates to sensor events.
But how does the Binder driver actually know when a process dies to trigger these cleanups? That low-level kernel mechanism requires a closer look at the Binder driver internals.