Implementing Inter-Process Communication (IPC) for Android
Most Android applications run in a single process and never encounter IPC. But as soon as a separate Service with android:process=":remote" appears, a third-party SDK running in its own process, or you need to pass data between applications—serious work with the Binder mechanism, AIDL, and Messenger begins.
IPC Mechanisms in Android
Android provides several levels of abstraction over Binder:
| Mechanism | When to Use | Complexity |
|---|---|---|
| Intent | Launch Activity/Service, pass small data | Low |
| Messenger | One-way messages, no parallelism needed | Medium |
| AIDL | Two-way interaction, parallel calls | High |
| ContentProvider | Structured data between applications | Medium |
| BroadcastReceiver | Broadcast events to all listeners | Low |
Binder is the transport layer for all of these. The Linux kernel transfers data between processes via /dev/binder. The maximum transaction buffer size is 1 MB (shared among all active transactions). Attempting to send a large Bitmap through Binder results in TransactionTooLargeException.
AIDL: When You Need Real IPC
AIDL (Android Interface Definition Language) generates Binder proxies on both sides. Suitable when a Service provides an API with multiple methods and synchronous responses are needed.
Interface definition (IDataService.aidl):
// IDataService.aidl
package com.example.service;
import com.example.service.IDataCallback;
interface IDataService {
void getData(String key, IDataCallback callback);
boolean setData(String key, String value);
List<String> getKeys();
}
// IDataCallback.aidl
package com.example.service;
oneway interface IDataCallback {
void onResult(String key, String value);
void onError(int code, String message);
}
oneway on the callback interface is asynchronous—doesn't block the calling thread. Without oneway, the callback blocks the Service thread until processing completes on the client side.
Service implementation:
class DataService : Service() {
private val binder = object : IDataService.Stub() {
override fun getData(key: String, callback: IDataCallback) {
// AIDL calls arrive in Binder thread pool, not main thread
val value = dataStore.get(key)
if (value != null) {
callback.onResult(key, value)
} else {
callback.onError(404, "Key not found: $key")
}
}
override fun setData(key: String, value: String): Boolean {
return try {
dataStore.set(key, value)
true
} catch (e: Exception) {
false
}
}
override fun getKeys(): List<String> = dataStore.getAllKeys()
}
override fun onBind(intent: Intent): IBinder = binder
}
Client-side connection:
class ClientActivity : AppCompatActivity() {
private var dataService: IDataService? = null
private val serviceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
dataService = IDataService.Stub.asInterface(service)
// now you can call methods
}
override fun onServiceDisconnected(name: ComponentName) {
dataService = null
// Service crashed or was killed by system—reconnect
}
}
override fun onStart() {
super.onStart()
val intent = Intent().apply {
component = ComponentName("com.example.service", "com.example.service.DataService")
}
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
}
override fun onStop() {
super.onStop()
unbindService(serviceConnection)
dataService = null
}
private fun fetchData(key: String) {
dataService?.getData(key, object : IDataCallback.Stub() {
override fun onResult(key: String, value: String) {
runOnUiThread { updateUI(key, value) }
}
override fun onError(code: Int, message: String) {
runOnUiThread { showError(message) }
}
})
}
}
Critical point: the callback IDataCallback.Stub is invoked in the Binder thread pool on the client side, not on the main thread. runOnUiThread or lifecycleScope.launch(Dispatchers.Main) are mandatory for UI updates.
Security: Who Can Connect
By default, a Service with exported="true" is accessible to any application. To restrict access:
<service
android:name=".DataService"
android:exported="true"
android:permission="com.example.permission.DATA_SERVICE">
</service>
// Check in onBind or in each method
override fun onBind(intent: Intent): IBinder? {
val callerUid = Binder.getCallingUid()
if (checkPermission("com.example.permission.DATA_SERVICE", callerUid) != PackageManager.PERMISSION_GRANTED) {
return null
}
return binder
}
Binder.getCallingUid() is the UID of the client process. It lets you implement UID whitelisting or verify APK signature via PackageManager.checkSignatures().
Messenger: Simpler Than AIDL
For simple scenarios (command queue from client to service), Messenger is more convenient:
class MessengerService : Service() {
private val handler = object : Handler(Looper.getMainLooper()) {
override fun handleMessage(msg: Message) {
when (msg.what) {
MSG_DO_WORK -> {
val data = msg.data.getString("payload")
processData(data)
// reply to client
msg.replyTo?.send(Message.obtain(null, MSG_RESULT, 0, 0).apply {
this.data = Bundle().apply { putString("result", "done") }
})
}
}
}
}
override fun onBind(intent: Intent): IBinder = Messenger(handler).binder
companion object {
const val MSG_DO_WORK = 1
const val MSG_RESULT = 2
}
}
Messenger's weakness: all messages are processed sequentially in the Handler. If processing one message is slow, the queue backs up. AIDL with Binder thread pool is parallel by default.
Transferring Large Data: SharedMemory
If you need to pass more than a few hundred KB (image, audio buffer), use SharedMemory (API 27+) or MemoryFile (older versions). Only the descriptor is passed through Binder; data is transferred via shared memory:
// Service side
val sharedMemory = SharedMemory.create("image_buffer", bitmap.byteCount)
val buffer = sharedMemory.mapReadWrite()
bitmap.copyPixelsToBuffer(buffer)
SharedMemory.unmap(buffer)
// Send ParcelFileDescriptor through Binder
val pfd = sharedMemory.fdDup // ParcelFileDescriptor for Binder transfer
This bypasses the 1 MB Binder transaction limit and is the only correct way to transfer media data between processes.
Common Issues
DeadObjectException. The Service is killed by the system, but the client doesn't know—the next method call throws an exception. Handle with try/catch, call bindService again in onServiceDisconnected.
ServiceConnection leak. If bindService() is called in onCreate() but unbindService() is forgotten, the Service is held in memory until the process is destroyed. Pair bind/unbind in onStart/onStop or onCreate/onDestroy—this is a rule without exceptions.
AIDL interface on two different project sides. If Service and client are separate APKs, .aidl files must be identical (including package name). Package mismatch causes SecurityException or ClassCastException during asInterface().
Implementing IPC via AIDL takes 3–7 days for interface design, security, connection loss handling, and testing. Integrating with an existing Service is 1–2 days. Cost is calculated individually.







