Binder File Descriptor Usages
The RPC mechanism provided by Binder is not a good fit for certain use cases. As we know, Android framework allocates about 1 MB of virtual memory for each process in ProcessState
. So you can only transfer at most several hundreds of KB within one transaction. Besides, every bits of data needs to be copied to target process's memory space so transferring bulk data with Binder transaction is less efficient. Another use case where Binder RPC mechanism doesn't fit well is streaming data transfer. Using Binder transactions to implement streaming data transfer will be highly inefficient.
But this doesn’t mean Binder cannot help with these use cases. File descriptor is a special kind of Binder object and Binder driver will special handle a file descriptor that goes across process boundaries. The details are discussed in the article “Binder data model” and “Binder transaction”. In short, when a file descriptor is sent to another process, Binder driver will open the same underlying file in the target process and the target process will receive the integer value of the new file descriptor. So both processes will have a file descriptor pointing the same underlying file after the transfer, which basically creates an area of shared memory between the two processes. This kind of shared memory provides a solution for both bulk data transfer and streaming data transfer. (It may be more accurate to call them data sharing though.)
Binder bulk data transfer
In the HamKing sample project [1], the client app can call pickupOrder
to get the "burger" when an order is ready, the server app will then send a Bitmap
of the "burger" to the client. These Bitmap
s can take more than 1 MB in memory, so if the server app indeed tries to copy the bits to client app in a Binder transaction, it will definitely receive a TransactionTooLargeException
. However, that doesn't happen in reality. What happens is that the server app passes a file descriptor to client app that points to the memory containing the image data, instead of copying the actual data to client app's memory space. Let's see how this is implemented by the Bitmap
class:
The writeToParcel
just calls the native method nativeWriteToParcel
to do the actual work. Line 11 to 28 writes some metadata of the Bitmap
into the Parcel
. Line 31 checks whether the data is already in an ashmem
region, if so this function just writes the file descriptor pointing the region and return. The ashmem
is a custom memory sharing mechanism implemented in Android. An area of shared memory in ashmem
is managed by a file descriptor so it can be naturally shared between processes if you send the file descriptor using Binder. If the data is not in an ashmem
region, writeBlob
method will be called to serialize the data.
Line 8 checks whether the size of the data is less than 16 KB or the Parcel
instance doesn't allow file descriptors, if so writeInplace
will be called to copy the data into the buffer of this Parcel
. The way writeInplace
works is similar to how primitive data types are serialized, which is discussed in the article "Binder data model". Otherwise, the method will create an ashmem
region to do the serialization. I will not talk about ashmem
in this article, but note line 36 writes the file descriptor of this region to the Parcel
. After this function returns, Bitmap_writeToParcel
method will copy the data from the original memory to the ashmem
region. So in short, if the size of a Bitmap
is less than 16 KB then the data will be copied to target process's memory space; otherwise an ashmem
region will be created to avoid data copying between processes.
Binder streaming data transfer
Perhaps the best usage example of streaming data in Android is the input event stream. The InputManagerService
(IMS) in Android is responsible for reading and parsing different kinds of input events from different devices and dispatching the events to different event consumers. Like most other system services, IMS runs in the system_server
process. However, the events consumers are usually the applications who run in other processes. So IMS needs some channels to stream the input events to other processes. With the help of unix domain socket and Binder's file descriptor awareness, a pipe can be created between the IMS and an application process.
The input event streaming channel is modeled by the InputChannel
class. At the end of day, the windows on the display device is the unit to receive the input events, so WindowManagerService
(WMS) needs to manage the InputChannel
instances as well. For example, when ViewRootImpl
adds a window for a new Activity
, it creates a new InputChannel
instance and sends it to WMS. The InputChannel
instance, which is created using the empty constructor, is just a placeholder and doesn't allocate any sockets inside. If you look at the addToDisplay
interface in IWindowSession.aidl
you will notice the inputChannel
argument is marked out
, which means this InputChannel
will be initialized with reply data sent back from WMS. What's going to happen is that WMS will allocates a pair of InputChannel
instances, send the write side instance to IMS and send the read side instance back to application in the transaction reply, then an input event channel is formed between IMS and the application.
The InputChannel
class literally does nothing besides calling the corresponding native methods. The mPtr
field points the memory address of its native peer. The empty constructor does nothing, which means the InputChannel
instance created by ViewRootImpl
is strictly a placeholder. The openInputChannelPair
method is used to create a pair of InputChannel
s with a read side and a write side. The transferTo
moves the socket file descriptor from outParameter
to itself. The describeContents
method returns CONTENTS_FILE_DESCRIPTOR
to indicate the presence of a file descriptor in the serialized data; until current Android version this is the only flag for this method other than 0. The getToken
returns a Binder token that identifies this InputChannel
instance.
The openInputChannel
method creates a pair of InputChannel
s then use the first one as the write side and the second one as the read side. The write side is registered to IMS and the read side is return to the application process later. When this pair of InputChannel
s are created, a Binder token is created for these them as the identity for this pair. WMS uses this token as the key to manage the mapping between InputChannel
s and WindowState
s.
In essential, an instance of InputChannel
is just a file descriptor pointing to a unix domain socket. The openInputChannelPair
method uses the socketpair
function to open a pair of file descriptors pointing to the same underlying socket. This pair of file descriptors form a duplex communication channel, which means if you write data on any of the two file descriptors, you can read it from the other one. This two file descriptors are identical and the channel is duplex, so how to use each side of socket pair is a choice made by IMS. Line 14 creates a Binder token to uniquely identify the pair of InputChannel
s. The structure of the native InputChannel
class is simple, the core of it is just a file descriptor pointing to a unix domain socket:
After this pair of InputChannel
s are created, WMS will register the write side to IMS using registerInputChannel
which is just a wrapper of the native implementation:
Line 6 uses the Binder token of the InputChannel
to search for an existing Connection
instance, if an InputChannel
was already registered using this token then an error code will return. IMS uses a Connection
class to manage event dispatching on an InputChannel
. Line 11 creates a new Connection
instance and adds it to mConnectionsByFd
using the socket file descriptor value as the key. The InputChannel
instance is added to mInputChannelsByToken
using the Binder token as the key. Later when IMS has an input event to dispatch, it will determine which InputChannel
to send the event to with the help of WMS. The event sending is done by writing the input event to the corresponding file descriptor in a specific format. The other side of the channel can then read and process the input event.
Now that the write side of the InputChannel
is registered to IMS, WMS needs to call the transferTo
method to move the file descriptor to the outInputChannel
so that the auto-generated Binder service code can send back the read side socket descriptor to application process:
The native implementation of transferTo
function sets the mPtr
field in target InputChannel
Java class to that of the current one. After this call, the outInputChannel
owns the native InputChannel
instance which contains the read side socket. The auto-generate Binder transaction code on WMS side will then write the outInputChannel
to a Parcel
and the application side will initialize the placeholder InputChannel
with the Parcel
. The write
and read
methods in InputChannel
class serialize and deserialize the InputChannel
to a Parcel
respectively:
Nothing surprise in these two methods. The name, file descriptor and the Binder token are serialized and deserialized respectively. So after the application process reads out the InputChannel
, a streaming channel between IMS and the application process is formed. ViewRootImpl
can then receive input events from IMS by reading from this socket descriptor.