From Android Basics to Binder Mastery: A Journey Through IPC Mechanisms.
In the previous episode we began exploring the components of the Android operating system from the foundations, with an in-depth analysis of the Android Runtime, including its historical evolution (3.).
In this episode, we will discuss the Binder mechanism, which is the primary Android-specific feature of the Linux kernel and serves as the foundation for all IPC mechanisms within the Android framework.
The Binder mechanism is not only involved in calls to system and bounded services; it also forms the basis of Android's data and event exchange mechanisms, such as Intents and Content Providers.
But before diving into the mechanisms of the binder let's start by understanding the basics. We will examine multitasking and processes, and explain why inter-process communication (IPC) is necessary.
Multitasking is the ability to execute more programs at the same time. An operating system creates for every binary executable a certain memory frame with its own stack, heap, data and shared libraries. It also assigns special internal management structures. This is called a process.
Since one process can use only one CPU (this is true both for single processor and for a multiprocessor/multicore system comprising two or more CPUs) at a time, the operating system must ensure a proper usage (Linux is a SMP, symmetrical Multiprocessor Operating System). It interrupts processes and schedules their execution in turns. This task is handled by a scheduler, which allocates optimal time slots for each process. (Linux Scheduler insights)
A thread is the processing unit of a process that runs within the process’s memory space. A process is made of at least of one thread. Threads share the memory space with the parent process. A process can have multiple threads.
For security reasons, one process cannot alter another process's data. The operating system uses process isolation to enforce this. In Linux, virtual memory achieves this by giving each process its own linear and continuous memory space. This space is mapped to physical memory by the operating system.
Each process has its own virtual memory space, so that a process cannot manipulate the data of another process. Only the operating system can access all physical memory. The process isolation ensures for each processes memory security, but in many cases the communication between process is wanted and needed. The operating system must provide mechanisms for inter-process communication (IPC).
Processes run normally in an unprivileged operation mode, meaning they have no access to physical memory or devices. This operation mode is called "user space" in Linux. More abstractly, the concept of security boundaries in an operating system introduces the term "ring". Note that this must be a hardware-supported feature of the platform. A certain group of rights is assigned to each ring (1.).
Intel hardware supports four rings, but only two rings are used by Linux: ring 0 with full rights and ring 3 with the least rights. System processes run in ring 0 (kernel space), while user processes run in ring 3. If a process needs higher privileges, it must perform a transition from ring 3 to ring 0. This transition passes through a gateway that performs security checks on arguments. This transition is called a system call and incurs a certain amount of processing overhead.
The image below shows privilege levels of processors: on the left for X86 architectures and on the right for ARM architectures .
Returning to the topic of IPC, if a process exchanges data with another process, this is referred to as inter-process communication (IPC). Linux offers a variety of mechanisms for IPC, which are listed below:
Signals. A process can send signals to processes with the same uid and gid or in the same process group.
Pipes are unidirectional byte-streams that connect the standard output from one process with the standard input of another process.
Sockets, a socket is an endpoint of bidirectional communication. Two processes can communicate with byte-streams by opening the same socket.
Message queues, processes can write a message to a message queue that is readable for other Processes.
Semaphores, a semaphore is a shared variable that can be read and written by many processes.
Shared Memory is a location in system memory mapped into virtual address spaces of two processes, that each process can fully access.
We saw in the previous article that when we talk about Android we refer to a very particular Linux that does not support all the features of the traditional one.
In the documentation related to the Android libc (named bionic) it is explained that Android does not support the classic System V IPCs that we saw above. Instead a new mechanism called Binder is used to do IPC in Android
The decision to implement the Binder IPC mechanism in Android, rather than using the traditional IPC mechanisms provided by Linux, was driven by several key factors. Let's explore these reasons:
Although powerful, sockets are relatively heavy in terms of overhead. They involve significant context switching and data copying, making them less suitable for the frequent and lightweight communication needed in Android.
Pipes are simpler than sockets but still involve context switching and can be limited in terms of features and flexibility.
Message Queues provide a way to exchange messages between processes, but they can be complex to manage and do not offer the same level of integration and ease of use as Binder.
Shared Memory while efficient in terms of data transfer, shared memory requires careful synchronization to prevent race conditions and other concurrency issues, making it more challenging to use correctly.
On the other hand, there are many features of Binder that make it interesting for mobile systems where resources are less than those of PCs.
Let's see them quickly, then we will return to analyze them in depth in this and the next article.
Efficiency and Performance: Binder is designed to be lightweight, with minimal overhead compared to other IPC mechanisms. Traditional Linux IPC methods, such as sockets, pipes, and message queues, can incur significant performance costs due to context switching, copying data between user space and kernel space, and managing connections. Binder supports zero-copy transactions, meaning data can be shared between processes without unnecessary copying, reducing CPU usage and improving performance. This is particularly important for mobile devices with limited resources.
Security: Binder supports permissions and access control. Each Binder transaction includes information about the calling process and user ID, enabling the system to enforce permissions at a very granular level. This helps with maintaining the security of the system by ensuring that only authorized processes can perform specific actions. By using Binder, Android can isolate processes more effectively, reducing the risk of one compromised process affecting others. This is crucial for maintaining the security of user data and the stability of the system.
Simplified Development: Binder provides a unified framework for IPC, simplifying development for Android developers. Rather than dealing with multiple IPC mechanisms, developers can use a consistent API for all inter-process communication needs. The Binder framework abstracts the complexities of IPC, making it easier for developers to write robust and efficient code. It handles the details of marshaling and unmarshaling data, making IPC more straightforward and less error-prone.
Robustness and Stability: Binder supports strongly-typed interfaces defined in the Android Interface Definition Language (AIDL). This ensures that the data exchanged between processes is well-structured and conforms to defined interfaces, reducing the likelihood of errors. Binder handles reference counting for objects passed between processes, preventing issues like memory leaks and dangling pointers. This contributes to the overall stability of the system.
Support for Asynchronous Calls: Binder supports asynchronous communication, allowing processes to send messages without blocking. This is particularly useful for maintaining responsive user interfaces and efficient background processing.
Before diving deeper into Binder, it’s important to briefly recall the main elements of the IPC Android Application Framework.
Higher-level IPC abstractions in Android, such as Intents, ContentProviders, and Messengers, are built on top of Binder.
An Intent is a messaging object used for asynchronous communication between Android components. These components can run within the same app or across different apps (and thus different processes). At a high level, Intents can be thought of as either a point-to-point link or a publish-subscribe pattern. An Intent represents a message containing a description of the operation to be performed, the data to be passed, and the two endpoints.
Intents can be either explicit or implicit. Explicit Intents include the name (full package name) of the component that started it, a set of data (called Extras, which are accessed through a Bundle), and the name of the target component. Implicit Intents, on the other hand, declare a general action to be performed. Given a set of data, an implicit Intent requests an operation (such as sharing a file with a set of contacts or sending a message). This type of Intent can cause runtime errors if the data type passed by a component is not what the receiving component expects.
Content Providers are components that manage access to a structured set of data and expose a cross-process data management interface. They allow applications to share data with other applications in a secure and standardized way. The Content Resolvers are components that communicate synchronously with Content Providers. Content Resolvers generally run in different applications and use a CRUD (Create, Read, Update, Delete) query/write system.
An Android Messenger represents a reference to a Handler that can be sent to a remote process via an Intent. This reference is passed using the IPC mechanism. A message is similar to an Intent in that it contains information on what to do and the data to act on, but it is used directly as message.what and message.getData(). Messengers enable message-based communication across processes.
So Intents, Content Providers and Messenger are high-level abstractions of the Binder.
Since a process cannot directly invoke operations on other processes, due to the isolation between processes provided by the system, they must both go through the kernel and do so via a Binder driver, this driver is mapped to "/dev/binder" and is the central component of the Binder framework, and all IPC calls go through it. It uses several system calls to manage IPC like ioctl and mmap.
The ioctl system call is used for various Binder operations. The BINDER_WRITE_READ command is one of the most commonly used, handling both sending and receiving of data.
struct binder_write_read bwr;
bwr.write_size = ...;
bwr.write_buffer = ...;
bwr.read_size = ...;
bwr.read_buffer = ...;
ioctl(binder_fd, BINDER_WRITE_READ, &bwr);
The mmap system call is used to create a shared memory region that the Binder driver uses to efficiently transfer data between processes.
mmap(NULL, BINDER_BUFFER_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, binder_fd, 0);
But How is data actually passed between processes?
The Binder driver manages part of the address space for each process. This chunk of memory is read-only to the process, with all writing performed by the kernel module. When a process sends a message to another process, the kernel allocates some space in the destination process's memory and copies the message data directly from the sending process. It then queues a short message to the receiving process, indicating where the received message is located. The recipient can then access that message directly, as it resides in its own memory space. When a process finishes with the message, it notifies the Binder driver to mark the memory as free.
Here is a simplified illustration of the Binder IPC architecture (2.).
The Binder API and its transaction mechanism are at the heart of Android's IPC system. The high-level Binder API allows applications and services to communicate seamlessly, while the low-level system calls and kernel driver manage the details of data transfer and process synchronization. This architecture ensures efficient, secure, and reliable communication between processes in the Android environment.
The key components of Binder Architecture are:
These components are the basis of the transaction mechanism.
The transaction mechanism is the process by which data is sent from one process to another using the Binder driver. Here’s a detailed look at the flow:
STEP 1. Initiating a Transaction:
When a client process wants to communicate with a service, it initiates a transaction. This typically involves the following steps:
The client constructs a Parcel object containing the data it wants to send and calls the transact method on an IBinder instance to send the parcel.
Parcel data = Parcel.obtain();
data.writeInterfaceToken("com.example.IMyService");
data.writeString("Hello, service!");
Parcel reply = Parcel.obtain();
binder.transact(TRANSACTION_CODE, data, reply, 0);
The transact method triggers an ioctl system call to communicate with the Binder driver in the kernel.
ioctl(binder_fd, BINDER_WRITE_READ, &bwr);
The key structure used here is binder_transaction_data.
struct binder_transaction_data {
__u32 handle; /* target descriptor of command transaction */
__u32 code;
__u32 flags;
pid_t sender_pid;
uid_t sender_euid;
binder_size_t data_size;
binder_size_t offsets_size;
union {
struct {
binder_uintptr_t buffer;
binder_uintptr_t offsets;
} ptr;
__u8 buf[8];
} data;
};
STEP 2. Handling the Transaction in the Kernel
The Binder driver in the kernel handles the transaction:
Receiving the Request. The ioctl system call sends the transaction data to the Binder driver, which parses it and identifies the target service. The driver reads the binder_transaction_data structure to get details of the transaction, including the handle, code, flags, and data.
Target Identification. The handle field in binder_transaction_data is crucial. It specifies the target binder object. The handle is an index or identifier that the Binder driver uses to look up the target object in its internal tables. The Binder driver maintains a context for each process, which includes a mapping of handles to Binder objects (nodes). Using the handle, the driver locates the corresponding binder_node structure, which represents the target service in the kernel.
struct binder_node {
binder_uintptr_t ptr;
binder_uintptr_t cookie;
struct rb_node rb_node;
struct list_head async_todo;
struct hlist_node dead_node;
struct hlist_node deferred_node;
struct binder_ref *refs;
int internal_strong_refs;
int local_weak_refs;
int local_strong_refs;
int tmp_refs;
int weak_refs;
int strong_refs;
int pending_async;
int has_async_work;
int accept_fds;
int min_priority;
struct binder_proc *proc;
struct binder_context *context;
};
Delivering the Transaction. Once the driver identifies the target binder_node, it enqueues the transaction data into the target process's transaction queue.
Wakeup Target. The target process is woken up by the Binder driver to handle the transaction.
STEP 3. Processing the Transaction in the Target Process
The target process receives the transaction and processes it:
It retrieves the Parcel from the Binder driver.
It processes the data and prepares a response.
It writes the response into a Parcel and calls the reply method.
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
switch (code) {
case TRANSACTION_CODE:
data.enforceInterface("com.example.IMyService");
String message = data.readString();
reply.writeNoException();
reply.writeString("Received: " + message);
return true;
}
return super.onTransact(code, data, reply, flags);
}
STEP 4. Returning the Response to the Client
The target process uses another ioctl system call to send the response back to the Binder driver.
ioctl(binder_fd, BINDER_WRITE_READ, &bwr);
The Binder driver receives the response and queues it for the client process.
The client process is woken up by the Binder driver and retrieves the response Parcel and processes the data.
String response = reply.readString();
Here's a simplified sequence diagram illustrating the Binder transact mechanism.
And now let's see how to implement IBinder in a real situation, like a Service.
In Android, services are often used to perform background operations. A service can implement the IBinder interface to allow clients to bind to it and perform IPC.
Here’s an example of how a service in Android implements the IBinder interface step by step:
<service
android:name=".MyService"
android:process=":remote" />
Here is the code of the service:
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
public class MyService extends Service {
// Binder given to clients
private final IBinder binder = new LocalBinder();
// Class used for the client Binder. Because we know this service always runs
// in the same process as its clients, we don't need to deal with IPC.
public class LocalBinder extends Binder {
MyService getService() {
// Return this instance of MyService so clients can call public methods
return MyService.this;
}
}
@Override
public IBinder onBind(Intent intent) {
return binder;
}
// Example of a method that clients might call
public void exampleMethod() {
Log.d("MyService", "Example method called");
}
}
Explanation of MyService Class:
Here is the code of the client activity:
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
MyService myService;
boolean isBound = false;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
// We've bound to MyService, cast the IBinder and get MyService instance
MyService.LocalBinder binder = (MyService.LocalBinder) service;
myService = binder.getService();
isBound = true;
myService.exampleMethod(); // Call a method from MyService
}
@Override
public void onServiceDisconnected(ComponentName arg0) {
isBound = false;
}
};
@Override
protected void onStart() {
super.onStart();
// Bind to MyService
Intent intent = new Intent(this, MyService.class);
bindService(intent, connection, Context.BIND_AUTO_CREATE);
}
@Override
protected void onStop() {
super.onStop();
// Unbind from the service
if (isBound) {
unbindService(connection);
isBound = false;
}
}
}
Explanation of MainActivity Class:
Here is the sequence diagram for the example of an Activity binding to a Service in another process. This sequence diagram captures the interactions between the client activity, the Android framework, and the remote service during the process of binding to a service that runs in a separate process.
Now that we have a basic understanding of the Binder mechanisms, we can dive into the detailed IPC mechanism with Intents and Content Providers in Android, focusing on the roles of the Android framework and the Binder driver in the kernel.
We will analyze the following two examples in order: Querying a Content Provider and Broadcasting an Intent.
We will explore these examples in depth, step by step, from the application level to the framework level, to the system services level, and to the kernel level.
All the source code I will show is taken from the Android Open Source Project, version 12.1.0_r11.
First example: Querying a Content Provider.
Let's start with the first scenario: "An application wants to query a Content Provider in another process".
Just a few words about Content Providers. Content Providers facilitate data sharing between applications. They provide an interface for CRUD (Create, Read, Update, Delete) operations.
We will explore the detailed steps involved in performing a query on an Android Content Provider. We will cover all levels, from the application layer to the framework, system service, native SQLite library, and down to the kernel level with the Binder driver.
Here’s a visual representation in a sequence diagram of this scenario.
This diagram shows the flow of a query request from the application level through the framework, system services, native library, and down to the kernel via Binder, then return the result. Let’s see it step by step:
领英推荐
1. Client Process - Application Level:
The application initiates a query using the ContentResolver.query() method.
// MainActivity.java
Uri uri = Uri.parse("content://com.example.provider/table");
String[] projection = {"column1", "column2"};
String selection = "column1=?";
String[] selectionArgs = {"value"};
Cursor cursor = getContentResolver().query(uri, projection, selection, selectionArgs, null);
2. Framework Level (Client Process):
ContentResolver acquires a reference to the IContentProvider through the acquireProvider method (IContentProvider is an AIDL-generated interface that declares the query method).
// ContentResolver.java
public final Cursor query(final Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder, CancellationSignal cancellationSignal) {
IContentProvider provider = acquireProvider(uri);
if (provider == null) {
throw new IllegalArgumentException("Unknown URI " + uri);
}
try {
Cursor cursor = provider.query(mPackageName, uri, projection, selection,
selectionArgs, sortOrder, cancellationSignal);
...
return cursor;
} catch (RemoteException e) {
...
}
}
public final IContentProvider acquireProvider(Uri uri) {
...
final String auth = uri.getAuthority();
if (auth != null) {
return acquireProvider(mContext, auth);
}
return null;
}
public IContentProvider acquireProvider(Context context, String auth) {
...
IContentProvider provider = null;
try {
provider = ActivityManager.getService().getContentProviderExternal(
auth, context.getUserId(), null, context.getBasePackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
return provider;
}
public interface IContentProvider extends IInterface {
Cursor query(String callingPkg, Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder, ICancellationSignal cancellationSignal)
throws RemoteException;
}
In order to Requesting the provider this code calls acquireProvider method to get a reference to the content provider for the specified authority. (We will delve deeper into the "acquireProvider" method later, to get more information on how this method interacts with the rest of the Android system).
This involves a Binder IPC to the ActivityManagerService to get the content provider for the specified URI.
The ActivityManagerService returns a reference to the IContentProvider which is actually a proxy object (ContentProviderProxy).
The proxy object (ContentProviderProxy) implements the IContentProvider interface. It packages the query request into a Parcel and sends it to the ContentProviderNative via the transact method.
To get more insight into how this method interacts with the rest of the Android system, here is a deeper look at the relevant methods:
public class ContentProviderProxy implements IContentProvider {
private IBinder mRemote;
@Override
public Cursor query(String callingPkg, Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder, ICancellationSignal cancellationSignal)
throws RemoteException {
Parcel data = Parcel.obtain();
Parcel reply = Parcel.obtain();
data.writeInterfaceToken(IContentProvider.descriptor);
data.writeString(callingPkg);
uri.writeToParcel(data, 0);
data.writeStringArray(projection);
data.writeString(selection);
data.writeStringArray(selectionArgs);
data.writeString(sortOrder);
mRemote.transact(ContentProviderStub.TRANSACTION_query, data, reply, 0);
reply.readException();
Cursor cursor = CursorWindow.newFromParcel(reply);
data.recycle();
reply.recycle();
return cursor;
}
}
3. Binder IPC (First Interaction):
The transact method of the IContentProvider involves sending a Parcel to the ActivityManagerService.
This results in a system-call to the Binder driver using ioctl.
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
switch (cmd) {
case BINDER_WRITE_READ:
return binder_ioctl_write_read(filp, arg);
// ...
}
}
static int binder_ioctl_write_read(struct file *filp, unsigned long arg) {
struct binder_write_read bwr;
if (copy_from_user(&bwr, (void __user *)arg, sizeof(bwr)))
return -EFAULT;
// ...
binder_thread_write(proc, thread, bwr.write_buffer, bwr.write_size);
binder_thread_read(proc, thread, bwr.read_buffer, bwr.read_size);
return 0;
}
The Binder driver processes the request and forwards it to the ActivityManagerService in the server process.
4. Server Process (System Service Layer):
The request is received by the ActivityManagerService.
ActivityManagerService invokes the actual query method on the ContentProvider.
5. Content Provider Layer:
The ContentProvider interacts with the SQLiteDatabase to perform the query. The query is performed using the SQLite library. In Android, the SQLite library is primarily a C library, but it is wrapped with Java code to provide a convenient interface for developers to use within their applications. The Java classes such as SQLiteDatabase, SQLiteQueryBuilder, and others that I show below are part of the Android framework and provide the necessary abstractions to interact with the SQLite C library. These classes are used by Android developers to interact with the database in a high-level, object-oriented manner. The Java classes interact with the native SQLite C library using the Java Native Interface (JNI). JNI is a framework that allows Java code running in the Java Virtual Machine (JVM) to call and be called by native applications and libraries written in other languages such as C or C++.
// SQLiteDatabase.java
public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(table);
return qb.query(this, columns, selection, selectionArgs, groupBy, having, orderBy);
}
// SQLiteQueryBuilder.java
public Cursor query(SQLiteDatabase db, String[] columns, String selection, String[] selectionArgs, String groupBy, String having, String orderBy) {
String sql = buildQuery(columns, selection, groupBy, having, orderBy, null);
return db.rawQueryWithFactory(null, sql, selectionArgs, null);
}
// SQLiteDatabase.java
public Cursor rawQueryWithFactory(SQLiteCursorFactory cursorFactory, String sql, String[] selectionArgs, String editTable) {
SQLiteDirectCursorDriver driver = new SQLiteDirectCursorDriver(this, sql, editTable);
return driver.query(cursorFactory, selectionArgs);
}
// SQLiteDirectCursorDriver.java
public Cursor query(SQLiteCursorFactory cursorFactory, String[] selectionArgs) {
SQLiteQuery query = new SQLiteQuery(mDatabase, mSql, 0, selectionArgs);
return new SQLiteCursor(this, query);
}
6. Binder IPC (Second Interaction)
The result (Cursor) is sent back through another Binder IPC transaction.
This again involves a system call to the Binder driver using ioctl.
7. Framework Layer (Client Process)
The ContentResolver receives the Cursor result from the IContentProvider.
8. Client Process (Application Layer)
MainActivity processes the Cursor data.
Before moving on to the next example let's take a closer look at the acquireProvider method of the ContentResolver class to get more insight into how this method interacts with the rest of the Android system. Here is the snippet code.
// ContentResolver.java
public abstract class ContentResolver {
...
/** @hide */
public IContentProvider acquireProvider(Context context, String auth) {
...
IContentProvider provider = null;
try {
provider = ActivityManager.getService().getContentProviderExternal(
auth, context.getUserId(), null, context.getBasePackageName());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
return provider;
}
...
}
In order to Requesting the provider this code calls the "ActivityManager.getService(). getContentProviderExternal" method to get a reference to the content provider for the specified authority.
Here is a deeper look at the relevant methods:
This method retrieves the IActivityManager interface, which is used to communicate with the activity manager service.
ActivityManager.getService()
This method retrieves the IActivityManager interface, which is used to communicate with the activity manager service.
The ActivityManagerService (the implementation of IActivityManager) has the getContentProviderExternal method:
// ActivityManagerService.java
public final class ActivityManagerService extends IActivityManager.Stub {
...
@Override
public IContentProvider getContentProviderExternal(String name, int userId, IBinder token, String callingPackage) { enforceCallingPermission(android.Manifest.permission.ACCESS_CONTENT_PROVIDERS_EXTERNALLY, "getContentProviderExternal");
// Logic to get the ContentProvider
}
...
}
When the getContentProviderExternal is called, it triggers a Binder IPC call to the system service process, where the ActivityManagerService runs.
The ActivityManagerService looks up the requested content provider and returns an IContentProvider reference.
The IContentProvider interface is a remote interface that represents the content provider. The actual implementation is provided by the content provider component in the target application.
In summary, the acquireProvider method in ContentResolver involves multiple layers, including the application, framework, and system service layers, and it uses Binder IPC to communicate with the ActivityManagerService to get the content provider.
Second example: Broadcasting an Intent.
In this second scenario we will explore the intricate journey of an Intent in Android, from the moment it is broadcasted by an application to when it is received by a registered BroadcastReceiver in another application. We will delve into the details at the application level, the Android framework, and the kernel level.
Just a few words about an Intent. An Intent in Android is a messaging object used to request an action from another app component. It can be used for various purposes such as starting an activity, starting a service, or delivering a broadcast. We will focus on broadcast intents.
Broadcasting an Intent in Android involves several layers of the system, from the application layer down to the kernel, leveraging the Binder IPC mechanism.
Here’s the flow of what happens from the moment an application broadcasts an Intent to when a BroadcastReceiver in another application receives it.
1. Application Level (Broadcasting an Intent):
An application can send a broadcast intent using the sendBroadcast() method of the Activity.
// MainActivity.java
Intent intent = new Intent("com.example.CUSTOM_ACTION");
sendBroadcast(intent);
2. Framework Level (Handling the Broadcast):
When sendBroadcast() is called, it involves multiple components in the Android framework.
The sendBroadcast method in the ContextImpl class calls "ActivityManager.getService(). broadcastIntent()", where ActivityManager.getService() returns a reference to IActivityManager, this is a proxy that communicates with the system services.
The IActivityManager.broadcastIntent() is a remote method call that utilizes Binder to communicate with the ActivityManagerService.
// ContextImpl.java
public void sendBroadcast(Intent intent) {
ActivityManager.getService().broadcastIntent(...);
}
3. System Service Layer (Handling Broadcast in ActivityManagerService):
The ActivityManagerService receives the broadcast Intent and processes it in the broadcastIntent method.
public int broadcastIntent(...) {
synchronized(this) {
...
// Schedule the broadcast
BroadcastQueue queue = getBroadcastQueue(intent);
queue.enqueueBroadcastLocked(...);
...
}
}
ActivityManagerService identifies all the registered BroadcastReceivers that should receive this broadcast and schedules the delivery of the Intent.
The ActivityManagerService enqueues the broadcast in the BroadcastQueue, which handles different types of broadcasts (e.g., foreground, background).
void enqueueBroadcastLocked(BroadcastRecord r) {
...
// Enqueue the broadcast record
mParallelBroadcasts.add(r);
scheduleBroadcastsLocked();
...
}
The BroadcastQueue processes the next broadcast in the queue and sends it to the registered receivers.
// BroadcastQueue.java
void processNextBroadcast(boolean fromMsg) {
BroadcastRecord r = mOrderedBroadcasts.get(0);
deliverToRegisteredReceiverLocked(r);
}
The system iterates through the list of registered receivers and delivers the intent.
// BroadcastQueue.java
void deliverToRegisteredReceiverLocked(BroadcastRecord r) {
for (int i = 0; i < r.receivers.size(); i++) {
BroadcastReceiver receiver = r.receivers.get(i);
performReceiveLocked(receiver, r.intent, r.resultCode, r.resultData, r.resultExtras, r.ordered, r.sticky, r.userId);
}
}
ActivityManagerService uses the Binder IPC mechanism to deliver the broadcast.
4. Binder IPC (deliver the broadcast)
ActivityManagerService makes a system call to the Binder driver to deliver the Intent to the target application's process.
The Binder driver, part of the kernel, facilitates the communication between ActivityManagerService and the target application's process by handling the IPC.
When a process sends a message, the Binder driver allocates memory in the receiving process's address space and copies the message data directly.
// binder.c (Kernel driver)
static long binder_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) {
switch (cmd) {
case BINDER_WRITE_READ:
return binder_ioctl_write_read(filp, arg);
...
}
}
static int binder_ioctl_write_read(struct file *filp, unsigned long arg) {
struct binder_write_read bwr;
...
if (copy_from_user(&bwr, (const void __user *)arg, sizeof(bwr)))
return -EFAULT;
...
ret = binder_thread_write(proc, thread, bwr.write_buffer, bwr.write_size, bwr.write_consumed);
...
ret = binder_thread_read(proc, thread, bwr.read_buffer, bwr.read_size, bwr.read_consumed, 0);
...
if (copy_to_user((void __user *)arg, &bwr, sizeof(bwr)))
return -EFAULT;
return 0;
}
5. Framework Layer (Receiving the Broadcast)
public void handleReceiver(ReceiverData data) {
...
BroadcastReceiver receiver = data.intent.getReceiver();
receiver.onReceive(this, data.intent);
...
}
6. Application Level: Receiving the Broadcast.
The registered BroadcastReceiver in another application receives the intent via the onReceive callback of the BroadcastReceiver object registered on the intent's action.
Registering the BroadcastReceiver.
// Manifest file
<receiver android:name=".MyBroadcastReceiver">
<intent-filter>
<action android:name="com.example.CUSTOM_ACTION" />
</intent-filter>
</receiver>
Implementing the BroadcastReceiver.
// MyBroadcastReceiver.java
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
if ("com.example.CUSTOM_ACTION".equals(intent.getAction())) {
// Handle the received intent
}
}
}
By following these steps, the broadcast Intent travels from the broadcasting application, through the system services and the Binder driver, to the receiving application's BroadcastReceiver, ensuring secure and efficient IPC in Android.
To summarize, we could say that:
This multi-layered approach ensures that Android's IPC mechanism is both robust and efficient, providing a seamless experience for application developers.
We have reached the end of the second episode of our journey into the Android operating system.
In this episode, we explored the mechanisms of InterProcess Communication (IPC) in Android. Starting with Intents and Content Providers, we gradually delved into the various levels of Android until we reached the Kernel. What we observed is that, despite appearing different, the IPC methods essentially share the same mechanism: Binder. All IPC methods in Android rely on a kernel driver, the Binder driver, which enables a secure and efficient IPC mechanism.
In the next episode, we will continue discussing Binder, but this time in relation to another distinctive element of Android: the "system services".
I remind you my newsletter "Sw Design & Clean Architecture"? : https://lnkd.in/eUzYBuEX where you can find my previous articles and where you can register, if you have not already done, so you will be notified when I publish new articles.
Thanks for reading my article, and I hope you have found the topic useful,
Feel free to leave any feedback.
Your feedback is very appreciated.
Thanks again.
Stefano
References:
1. Patterson, Hennessy, “Computer Organization and Design:The Hardware/Software Interface”, MK Pubblication, fifth edition.
2. Nikolay Elenkov, “Android Security Internals” No Starch Press (October 2014). pp 5-10.
3. S.Santilli: "https://www.dhirubhai.net/pulse/android-unique-linux-distribution-stefano-santilli-dqipf"
.
Senior Architect at Bosch India
2 个月1). The Binder driver manages part of the address space for each process---> This means whenever user process is forked in android , a reserved user space read-only area is created . This also means that linux kernel's memory map is extended for for binder support. Right?