The IPC you already use
Intents, broadcasts, ContentProviders and Messenger are Binder underneath. Each one is a different level of abstraction over the same driver, chosen for a different kind of conversation.
Most Android developers use Binder every day without writing a line of AIDL. startActivity, a
broadcast, a ContentResolver query and a bound service all cross process boundaries; the platform
just puts a friendlier interface in front of the driver.
TL;DR#
- Intents are addressed messages: "this action, this data, this category". They travel through
system_serverand are resolved to a component before anything is started. - Broadcasts are intents delivered to many receivers. Most are now restricted, so the API reflects policy as much as mechanism.
- ContentProvider is a Binder service with a URI-shaped API, generated from a small contract
class;
ContentResolveris the client. - Messenger is a
Handlerwrapped in a Binder object: a message queue in another process with no.aidlfile. - All four are layers over Binder, and the layer you choose decides how much of the transaction you see.1
Mental model#
Think of the postal system and the telephone.
An intent is a letter: it has an address, it may be delivered to one recipient or to everyone on a list, and the sender does not know who will open it.
A broadcast is a notice pinned to a public board: anyone who has registered to read that board sees it.
A ContentProvider is a filing cabinet with a catalogue: you ask for a drawer by its label (the URI) and get structured records back.
A Messenger is a telephone with voicemail: you leave a message and the other side's queue plays it back when it is free.
Direct AIDL is a meeting: both sides are present, the call blocks until there is an answer, and you can read the room.
Where the analogy breaks#
The postal analogy makes delivery sound asynchronous and lossy. An Intent sent with
startActivity is neither: it is a synchronous Binder transaction to system_server that returns
once the activity has been scheduled. What makes intents feel loose is resolution — the sender
does not name a component, so the system decides who receives it.
The big picture#
Text description of this diagram
A Binder transaction sequence with two modes. The synchronous mode shows an app calling a proxy, the transaction entering the driver, the service invoking the method and replying, and the client waking. The oneway mode shows the same inbound path but the driver returning to the caller immediately, with no reply stage. Intents, broadcasts, provider queries and Messenger all use one of these two shapes underneath their APIs.
Notice that both modes are already familiar. startActivity behaves like the synchronous mode —
it calls into system_server and returns a result. sendBroadcast behaves like the oneway mode:
it hands the intent to the system and continues, with no reply.
Deep dive#
Intents are resolved, then transacted#
An Intent carries an action string, a data URI, a category set, extras, and optionally an explicit
component. When you call startActivity, the call crosses Binder to ActivityTaskManagerService;
the system resolves the intent to a component using the PackageManager's index of
<intent-filter> declarations.
Two consequences follow, and both are easy to miss:
- Resolution happens in system_server, not in your process. The list of candidate components is built from installed packages and can change while your app runs.
- The extras are a Bundle, and the Bundle is a Parcel. A
Parcelableextra is serialised by the same machinery an AIDL argument uses, and a large extra can hit the same transaction limit.
That is why TransactionTooLargeException shows up when passing large bitmaps in intents, and why
the recommended pattern is to pass a URI and let the receiver open the file.
Broadcasts: one intent, many receivers#
A broadcast is an intent delivered to every receiver registered for the action. The mechanics are
Binder all the way down — system_server holds the receiver list, and delivery is a series of
transactions into the receiver processes — but the visible API is sendBroadcast.
The history here is mostly about restriction. Implicit broadcasts were once a free-for-all; Android
8.0 restricted most of them for apps targeting API 26, because a broadcast wakes every registered
process and is a poor fit for a battery-constrained device. The replacements are JobScheduler,
WorkManager and explicit broadcasts to a named component.
ContentProvider is a Binder service with a URI#
A ContentProvider is a component that exposes structured data through query, insert, update,
delete and call. Underneath, it is a Binder service: the provider is registered with the
ActivityManager, and a client gets a Binder reference to it the first time it uses a
ContentResolver for that authority.
The client never sees the Binder object. ContentResolver is a facade that:
- Resolves the authority part of the URI to a provider.
- Acquires the provider's Binder reference.
- Calls
IContentProvider.query(...)with the URI, projection, selection and sort order. - Returns a
Cursor— which, for a remote provider, is itself backed by a Binder object that streams rows in pages.
That last point is the interesting one. A Cursor from a remote provider is a windowed view:
it does not copy the whole result set into your process. Each moveToNext across a window boundary
is a Binder call. It is why iterating a large cursor is slower than iterating a local list, and why
the cursor window size is a tuning knob.
Messenger: a queue in another process#
Messenger is the smallest useful IPC abstraction. You give it a Handler; it wraps the handler's
IMessenger Binder object; a client gets a Messenger from a bound service and calls send with a
Message. The message is parcelled, crosses Binder, and is posted to the handler's queue.
There is no .aidl file to write because the platform ships one for you. The trade-off is that you
get exactly one method — send(Message) — and the payload is a Message with an optional Bundle.
No return values, no typed methods, no interface evolution beyond what Message.what means to you.
Messenger is the right choice when:
- The conversation is message-shaped rather than call-shaped.
- You want the receiver's
Looperto serialise work, so you do not have to think about thread safety in the service. - You do not want to maintain an interface definition.
It is the wrong choice when you need a return value, a callback with typed arguments, or more than a handful of message types. At that point AIDL is cheaper than encoding a protocol in integers.
Choosing a layer#
| Need | Use | Why |
|---|---|---|
| Launch a screen, possibly in another app | Intent | Resolution and lifecycle are the system's job |
| Tell many components something happened | Broadcast (explicit) | Fan-out without knowing receivers |
| Share structured data | ContentProvider | URI addressing, permissions, paging |
| Send messages to a service | Messenger | No interface file, queue semantics |
| Typed calls with return values | AIDL | The compiler does the marshalling |
| Hand-rolled protocol over a stream | Socket | Binder is not a stream |
The pattern is a ladder of abstraction. Every rung above AIDL adds convenience and removes control; every rung below adds control and removes safety.
What they all have in common#
Strip away the API names and the same four properties recur:
- A kernel-enforced identity attached to each message, so the receiver can make a permission decision without trusting the payload.
- A bounded payload, inherited from the Parcel and the transaction buffer. This is why large
bitmaps do not belong in an intent, a broadcast or a
Message. - A serialisation step that turns objects into bytes and back. Every abstraction above the driver still pays for it.
- A failure mode when the other side is gone.
DeadObjectException, a missing activity, a provider that will not start — all are the same problem at different levels.
Once you see those four, the API differences become a matter of ergonomics. The transport is not what you are choosing between; you are choosing how much of it you want to see.
How it evolved#
The broadcast restrictions in Android 8.0 and the foreground-service rules in later releases are the clearest examples of the platform using policy to shape IPC. The mechanism did not change; what the system is willing to deliver did.
Under the hood#
- Intent.javathe intent model and its Parcel format
- ContentResolver.javathe client facade over a provider's Binder interface
- Messenger.javathe handler-backed Binder wrapper
- BroadcastQueue.javahow broadcasts are dispatched
Curiosity corner#
- A remote Cursor is paged over Binder. The whole result set is not copied, which is why cursor window size exists.
Messengeris AIDL you never see. The platform ships the interface, which is why there is no generated file in your project.- Explicit broadcasts are still cheap. The restriction was on implicit broadcasts, which require resolving against every installed package.
Try it#
adb shell dumpsys activity providers | grep -A 2 "CONTENT PROVIDER" | head -20Provider names and count vary by device and installed apps.
adb shell dumpsys activity broadcasts | grep -c "Receiver"The count is device-specific and changes while the system runs.
Check yourself#
Check yourself
Why does a Binder transaction copy the payload only once?
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsIntents and intent filters | App architecturedeveloper.android.comaccessed 2026-10-27↩
- docsBroadcasts overview | Background workdeveloper.android.comaccessed 2026-10-27↩
- docsContent provider | App data and filesdeveloper.android.comaccessed 2026-10-27↩
- docsServices overview | Background workdeveloper.android.comaccessed 2026-10-27↩
- docsBinder overviewsource.android.comaccessed 2026-10-27↩