In the Linux OS, there are several techniques to achieve IPC (Inter-process communication) like files, sockets, signals, pipes, message queues, semaphores, shared memory, etc. However, Android’s modified Linux kernel comes with a binder framework which enables an RPC (remote procedure call) mechanism between the client and server processes, where the client process can execute remote methods in the server process as if they were executed locally. So data can be passed to the remote method calls and results can be returned to the client calling thread. It appears as if the thread from the client process jumps into another (remote) process and starts executing in there (known as Thread Migration).
Although the underlying RPC mechanism is very complicated, the binder framework wraps everything and exposes simple to use APIs to make the entire interprocess communication mechanism seem simple. Let’s see what all happens behind the scenes:
- Decompose data into primitives that can be understood by the operating system, also known as marshalling (similar to serialization).
- Transfer the marshalled information across process boundaries to the remote process.
- Recompose the information in the remote process, also known as unmarshalling/demarshalling (similar to deserialization).
- Transfering return values back to the client calling process.
Intents, Content Providers, Messenger, all system services like Telephone, Vibrator, Wifi, Battery, Notification, etc. utilize IPC infrastructure provider by Binder. Even the lifecycle callbacks in your Activity like
onDestroy() are invoked by
ActivityManagerServer via binders.
A lot of the
Binder terminology is covered here that might make more sense once you’ve seen how to work with AIDLs and achieve IPC. Clients and Services don’t want to know anything about the Binder protocol, hence they make use of proxies (by client) and stubs (by service). Proxies take your high-level Java/C++ method calls (requests) and convert them to Parcels (Marshalling) and submit the transaction to the Binder Kernel Driver and block. Stubs on the other hand (in the Service process) listens to the Binder Kernel Driver and unmarshalls Parcels upon receiving a callback, into rich data types/objects that the Service can understand.
When a client process makes a call to the server process, it transfers a code representing the method to call along with marshalled data (Parcels). This call is called a transaction. The client Binder object calls
transact() whereas the server Binder object receives this call in
onTransact() method. A call to
transact() blocks the client thread by default until
onTransact() is done with its execution in the remote thread.
onTransact() method is executed on a single thread from a pool of binder threads. The pool can have a maximum of 16 threads which means 16 remote calls can be handled concurrently (make sure to handle multi threading issues and ensure thread safety).
The server (service) can also issue a transaction to the client process (two-way communication) in which case it becomes the client and the transaction is handled in a binder thread in the client process. However if the server started a transaction while executing the
onTransact() method, the client will receive the transaction in the thread waiting for the first transaction to be finished rather than a binder thread.
If you don’t want the
transact() call to block then you can pass the
IBinder.FLAG_ONEWAY flag to return immediately without waiting for any return values.
Note: The entire IPC occurs through the Binder driver in the Linux Kernel. Communication across process boundaries are not possible directly, only through the kernel. The binder driver is a kernel module that makes this possible.
We’ll cover IPC with AIDL in the next article where all of these will start to make a lot more sense.
Other useful resources on the same topic:
- https://www.youtube.com/watch?v=Jgampt1DOak (super insightful, must watch. slides here)