AIDL 跨进程通信实战与 Binder 源码剖析
在上一篇文章中,我们从理论层面拆解了 Binder 的 mmap 一次拷贝、内核驱动调度和安全模型。但理论终究是纸上谈兵——当你真正写下第一行 AIDL 代码时,脑海中可能仍然充满疑问:编译器自动生成的那堆 Stub、Proxy 到底是什么?bindService 之后系统在幕后做了什么?客户端拿到的"远程对象"为什么能像调用本地方法一样使用?
本文将以一个完整的 AIDL 跨进程计算器服务为主线,从实战代码出发,逐层撕开 AIDL 自动生成代码的面纱,最终深入到 Binder 框架的源码内核,彻底回答上述问题。
实战场景:跨进程计算器服务
我们要构建一个最经典的跨进程通信场景:客户端 App(Activity) 通过 bindService 绑定一个运行在独立进程中的 计算器服务(Service),然后跨进程调用其 add 方法。
这就像你打电话给一个远在异地的会计师(Server),请他帮你算一笔账(add),然后等他把结果报给你。你们之间隔着千山万水(进程隔离),但电话系统(Binder)让这一切如同面对面交谈。
第一步:定义 AIDL 接口(通信契约)
在 src/main/aidl/com/example/calculator/ 目录下创建接口文件:
// ICalculator.aidl
package com.example.calculator;
// 这就是客户端和服务端之间的"通信契约"
// 编译后,Android 工具会自动生成 Stub 和 Proxy 代码
interface ICalculator {
/** 跨进程加法运算 */
int add(int a, int b);
/** 跨进程减法运算 */
int subtract(int a, int b);
}
AIDL 文件本质上是一份协议声明——它告诉编译器:"这个接口的方法需要支持跨进程调用,请帮我生成所有序列化/反序列化的样板代码。"
第二步:服务端实现(接电话的人)
// CalculatorService.java — 运行在 :remote 进程中
public class CalculatorService extends Service {
// 实现 AIDL 自动生成的 Stub 抽象类
// Stub 继承自 Binder,是服务端的"本体"
private final ICalculator.Stub mBinder = new ICalculator.Stub() {
@Override
public int add(int a, int b) throws RemoteException {
Log.d("CalculatorService",
"add() 被调用, 当前进程PID: " + Process.myPid());
return a + b;
}
@Override
public int subtract(int a, int b) throws RemoteException {
return a - b;
}
};
@Override
public IBinder onBind(Intent intent) {
// 将 Stub(Binder 本体)返回给系统
// 系统会把它的"引用"传递给客户端
return mBinder;
}
}
在 AndroidManifest.xml 中声明该 Service 运行在独立进程:
<service
android:name=".CalculatorService"
android:process=":remote"
android:exported="true" />
android:process=":remote" 是关键——它让这个 Service 运行在一个与主 App 完全不同的进程中,两者的内存空间彻此隔离。
第三步:客户端绑定(打电话的人)
// MainActivity.java — 运行在主进程中
public class MainActivity extends AppCompatActivity {
private ICalculator mCalculator;
private boolean mBound = false;
// ServiceConnection 是系统回调接口
// 当绑定成功时,系统会把服务端的 Binder 引用传过来
private final ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 核心魔法:将原始的 IBinder 转换为可直接调用的接口
// 如果是跨进程,返回的是 Proxy 对象
// 如果是同进程,返回的是 Stub 本身
mCalculator = ICalculator.Stub.asInterface(service);
mBound = true;
Log.d("MainActivity", "服务已连接");
}
@Override
public void onServiceDisconnected(ComponentName name) {
mCalculator = null;
mBound = false;
}
};
public void onBindClick(View v) {
Intent intent = new Intent(this, CalculatorService.class);
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
public void onCalculateClick(View v) {
if (mBound) {
try {
// 看起来像本地方法调用,实际上数据穿越了进程边界!
int result = mCalculator.add(42, 58);
Log.d("MainActivity",
"计算结果: " + result + ", 当前进程PID: " + Process.myPid());
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mBound) unbindService(mConnection);
}
}
运行这段代码,你会在日志中看到 add() 的调用方和执行方的 PID 是不同的——证明数据确实跨越了进程边界。
揭秘:AIDL 自动生成了什么?
当你编译项目后,Android 构建工具会为 ICalculator.aidl 自动生成一个 Java 文件。这个文件是理解 Binder 通信的钥匙。下面是其核心骨架(省略了非关键代码):
public interface ICalculator extends IInterface {
// ========== Stub:服务端的骨架 ==========
public static abstract class Stub extends Binder implements ICalculator {
// 接口的唯一身份标识,用于校验通信双方是否匹配
private static final String DESCRIPTOR = "com.example.calculator.ICalculator";
// 每个方法被分配一个唯一的事务编号
static final int TRANSACTION_add =
IBinder.FIRST_CALL_TRANSACTION + 0; // 编号 1
static final int TRANSACTION_subtract =
IBinder.FIRST_CALL_TRANSACTION + 1; // 编号 2
public Stub() {
// 将自身注册到 Binder 中,关联 DESCRIPTOR
this.attachInterface(this, DESCRIPTOR);
}
/**
* 核心桥梁方法:判断是同进程还是跨进程
* 同进程 → 直接返回 Stub 本身(零开销)
* 跨进程 → 包装为 Proxy 返回(走 IPC 链路)
*/
public static ICalculator asInterface(IBinder obj) {
if (obj == null) return null;
// 尝试在本地查找:如果同进程,这里直接命中
IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (iin != null && iin instanceof ICalculator) {
return (ICalculator) iin;
}
// 跨进程:创建 Proxy 包装器
return new Stub.Proxy(obj);
}
/**
* 服务端的"调度中心"
* 当驱动把请求送达后,这个方法被触发
* 根据事务编号分发到对应的业务方法
*/
@Override
public boolean onTransact(int code, Parcel data,
Parcel reply, int flags)
throws RemoteException {
switch (code) {
case TRANSACTION_add: {
data.enforceInterface(DESCRIPTOR); // 校验身份
int _arg0 = data.readInt(); // 反序列化参数
int _arg1 = data.readInt();
int _result = this.add(_arg0, _arg1); // 调用真实实现
reply.writeNoException();
reply.writeInt(_result); // 序列化返回值
return true;
}
case TRANSACTION_subtract: {
data.enforceInterface(DESCRIPTOR);
int _arg0 = data.readInt();
int _arg1 = data.readInt();
int _result = this.subtract(_arg0, _arg1);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
// ========== Proxy:客户端的替身 ==========
private static class Proxy implements ICalculator {
private IBinder mRemote; // 指向远程 Binder 的引用
Proxy(IBinder remote) { mRemote = remote; }
@Override
public IBinder asBinder() { return mRemote; }
@Override
public int add(int a, int b) throws RemoteException {
// 准备两个"数据包裹"
Parcel _data = Parcel.obtain(); // 请求包
Parcel _reply = Parcel.obtain(); // 响应包
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR); // 写入身份标识
_data.writeInt(a); // 序列化参数
_data.writeInt(b);
// ★ 核心调用:触发跨进程通信 ★
// 这一行会阻塞当前线程,直到远程方法执行完毕并返回
mRemote.transact(Stub.TRANSACTION_add,
_data, _reply, 0);
_reply.readException();
_result = _reply.readInt(); // 反序列化返回值
} finally {
_data.recycle();
_reply.recycle();
}
return _result;
}
@Override
public int subtract(int a, int b) throws RemoteException {
// 结构与 add 完全相同,仅事务编号不同
Parcel _data = Parcel.obtain();
Parcel _reply = Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(a);
_data.writeInt(b);
mRemote.transact(Stub.TRANSACTION_subtract,
_data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
} finally {
_data.recycle();
_reply.recycle();
}
return _result;
}
}
}
// 接口方法声明
int add(int a, int b) throws RemoteException;
int subtract(int a, int b) throws RemoteException;
}
关键设计解读
asInterface:同进程 vs 跨进程的分水岭
asInterface 是整个 AIDL 架构中最精妙的方法。它内部调用 queryLocalInterface(DESCRIPTOR) 来检测:传入的 IBinder 到底是"本地的 Stub 本体"还是"远程的 BinderProxy 引用"?
- 同进程:
queryLocalInterface命中,直接返回 Stub 本身。方法调用零开销,等价于普通的 Java 方法调用。 - 跨进程:
queryLocalInterface返回null,创建 Proxy 包装器。后续所有方法调用都会走序列化 →transact→ 驱动 →onTransact→ 反序列化的完整 IPC 链路。
这种设计让上层调用者完全不需要关心对方在哪个进程——调用方式完全一致,IPC 的复杂性被彻底封装在了底层。
Proxy 的 transact:跨进程调用的发射台
当客户端调用 mCalculator.add(42, 58) 时,实际执行的是 Proxy.add()。这个方法做了三件事:
- 序列化(Marshaling):把方法参数写入
Parcel——一种高效的二进制序列化格式 - 发射事务:调用
mRemote.transact(),这里的mRemote实际是一个BinderProxy对象 - 等待并反序列化:
transact会阻塞当前线程,直到服务端处理完毕。然后从_reply中读取结果
Stub 的 onTransact:服务端的调度中心
服务端线程被 Binder 驱动唤醒后,框架会调用 Stub.onTransact()。它根据事务编号(TRANSACTION_add = 1)执行 switch-case 分发,从 Parcel 中反序列化出参数,调用开发者实现的真实业务方法,再把结果序列化写回 reply。
源码深潜:从 transact 到内核驱动
了解了自动生成代码的结构后,我们沿着 mRemote.transact() 这条线继续向下挖掘,看看数据是如何真正穿越进程边界的。
sequenceDiagram
participant App as 客户端 Proxy.add()
participant BP as BinderProxy (Java)
participant JNI as android_util_Binder.cpp
participant IPC as IPCThreadState (C++)
participant Driver as /dev/binder (内核)
participant SIPC as IPCThreadState (Server)
participant Stub as Stub.onTransact()
App->>BP: transact(TRANSACTION_add, data, reply, 0)
BP->>JNI: transactNative() [JNI 调用]
JNI->>IPC: BpBinder::transact()
IPC->>IPC: writeTransactionData() 写入 mOut 缓冲区
IPC->>Driver: ioctl(BINDER_WRITE_READ)
Note over Driver: copy_from_user 拷贝数据<br/>查找目标进程 binder_node<br/>构建 binder_transaction<br/>挂入 Server todo 队列并唤醒
Driver->>SIPC: ioctl 返回 BR_TRANSACTION
SIPC->>Stub: BBinder::onTransact() → Java onTransact()
Note over Stub: 反序列化参数,执行 add(),序列化结果
Stub-->>SIPC: 写入 reply Parcel
SIPC-->>Driver: ioctl(BC_REPLY)
Driver-->>IPC: 唤醒 Client,返回 reply 数据
IPC-->>JNI: 返回
JNI-->>BP: 返回
BP-->>App: 返回 result = 100
第一层:Java → Native(JNI 穿越)
BinderProxy.transact() 内部调用了 transactNative()——一个 native 方法。通过 JNI 桥接,控制权从 Java 虚拟机转移到了 C++ 的 android_os_BinderProxy_transact 函数。
在 Native 层,BinderProxy 对应的是 BpBinder(Binder Proxy 的 C++ 实现)。BpBinder::transact() 进一步调用 IPCThreadState::self()->transact()。
第二层:IPCThreadState(Native 通信引擎)
IPCThreadState 是线程级单例,是 Native 层的通信核心引擎。它的 transact() 方法做两件事:
写入请求:调用 writeTransactionData(BC_TRANSACTION, ...) 将事务数据(方法编号、Parcel 数据)封装成 binder_transaction_data 结构体,追加到内部的 mOut 写缓冲区。
与驱动对话:调用 waitForResponse() → talkWithDriver()。后者构造一个 binder_write_read 结构体,通过 ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr) 系统调用,将数据送入内核。此时,当前线程被挂起,等待服务端的回复。
// IPCThreadState.cpp 核心逻辑简化
status_t IPCThreadState::talkWithDriver(bool doReceive) {
binder_write_read bwr;
bwr.write_buffer = (uintptr_t)mOut.data();
bwr.write_size = mOut.dataSize();
bwr.read_buffer = (uintptr_t)mIn.data();
bwr.read_size = mIn.dataCapacity();
// 这个 ioctl 调用会阻塞,直到内核返回数据
ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr);
return NO_ERROR;
}
第三层:Binder 内核驱动(数据搬运的终极执行者)
ioctl 系统调用进入内核后,Binder 驱动(drivers/android/binder.c)接管一切。核心函数 binder_transaction() 执行以下步骤:
1. 寻址目标进程:根据客户端传入的 handle(句柄),在驱动维护的红黑树中查找到目标 Server 进程对应的 binder_node,进而定位到目标 binder_proc 结构体。
2. 分配接收缓冲区:在 Server 进程预先通过 mmap 映射的内核缓冲区中,申请一块空间用于存放本次事务数据。
3. 一次拷贝:调用 copy_from_user() 将 Client 用户空间的数据拷贝到上述缓冲区。由于该缓冲区同时被映射到了 Server 的用户空间(mmap 的魔法),Server 可以直接读取——这就是"一次拷贝"的实现。
4. 安全凭据注入:驱动从当前进程的 task_struct 中提取真实的 UID 和 PID,强制写入事务结构体。这一步由内核自动完成,用户态无法伪造。
5. 入队并唤醒:将 binder_transaction 对象挂载到 Server 线程的 todo 工作队列,调用 wake_up_interruptible() 唤醒阻塞在 ioctl 上的 Server 线程。
第四层:Server 端的觉醒与响应
Server 端的 Binder 线程一直阻塞在 IPCThreadState::talkWithDriver() 的 ioctl 调用上。被驱动唤醒后:
IPCThreadState从mIn缓冲区读取到BR_TRANSACTION命令- 调用
executeCommand(BR_TRANSACTION)→BBinder::transact()→JavaBBinder::onTransact() - 通过 JNI 回到 Java 层,触发
Stub.onTransact() onTransact根据事务编号调用开发者实现的add()方法- 将结果写入
replyParcel,通过BC_REPLY命令原路返回给驱动 - 驱动唤醒阻塞的 Client 线程,Client 从
reply中读取结果
整个过程就此完成,客户端的 mCalculator.add(42, 58) 调用正式返回 100。
bindService 的底层链路
了解了数据如何跨越进程后,还有一个关键问题:客户端是如何拿到服务端 Binder 引用的? 这需要追踪 bindService 的完整链路。
sequenceDiagram
participant Client as 客户端 Activity
participant CI as ContextImpl
participant AMS as AMS (system_server)
participant AS as ActiveServices
participant AT as ApplicationThread (Server进程)
participant Svc as CalculatorService
Client->>CI: bindService(intent, conn, flags)
CI->>CI: 将 ServiceConnection 包装为 IServiceConnection (AIDL)
CI->>AMS: AMS.bindService() [跨进程 Binder 调用]
AMS->>AS: bindServiceLocked() 查找/启动目标Service
AS->>AT: scheduleBindService() [跨进程调用 Server 进程]
AT->>Svc: handleBindService() → onBind()
Svc-->>AT: 返回 IBinder (Stub 本体)
AT->>AMS: publishService(IBinder) [将 Binder 发布给 AMS]
AMS->>CI: IServiceConnection.connected(IBinder) [回调客户端]
CI->>Client: onServiceConnected(name, BinderProxy)
Note over Client: 调用 Stub.asInterface(BinderProxy)<br/>得到 Proxy 对象,开始跨进程调用
关键步骤解读:
- 包装回调:
ContextImpl将客户端的ServiceConnection包装成IServiceConnection(一个 AIDL 接口),使其自身也能被跨进程回调 - 通知 AMS:通过 Binder IPC 调用
system_server中的 AMS,请求绑定服务 - AMS 调度:AMS 找到目标 Service 所在进程,通过该进程的
ApplicationThread(也是 Binder)通知它执行onBind() - 发布 Binder:Service 的
onBind()返回 Stub 对象后,通过publishService()将其发布回 AMS - 回调客户端:AMS 调用之前保存的
IServiceConnection.connected(),将 Binder 引用传递给客户端。客户端收到的是一个BinderProxy对象(因为跨进程) - 创建 Proxy:客户端在
onServiceConnected中调用Stub.asInterface(service),此时queryLocalInterface返回null(跨进程),于是创建Proxy对象
至此,客户端拿到了可以透明地跨进程调用的 ICalculator 接口。整个绑定过程本身就涉及了多次 Binder IPC 调用——这充分说明了 Binder 是 Android 系统的通信神经中枢。
Binder 对象在内核中的生命周期
一个容易被忽视但极其重要的问题是:Binder 对象是如何在跨进程传递时保持"存活"的? 答案藏在内核驱动的引用计数机制中。
当 Server 端的 Stub(BBinder)第一次通过 Binder 驱动被传递给另一个进程时,驱动会在内核中创建一个 binder_node 节点来代表这个实体。同时,为接收方创建一个 binder_ref 引用。驱动通过维护 binder_node 的强引用计数和弱引用计数来管理其生命周期——只有当所有进程都释放了对它的引用后,这个节点才会被销毁。
这就像一个共享文档——只要还有人持有链接,文档就不会被删除。Binder 驱动在内核中充当了这个"文档管理系统"的角色。
总结:一次 add 调用的全景回顾
回顾 mCalculator.add(42, 58) 这一行代码背后发生的一切:
| 层级 | 发生了什么 |
|---|---|
| 客户端 Java 层 | Proxy.add() 将参数序列化到 Parcel,调用 mRemote.transact() |
| JNI 层 | BinderProxy.transactNative() 穿越到 C++ 的 BpBinder::transact() |
| Native 层 | IPCThreadState 将数据封装为 binder_transaction_data,通过 ioctl 送入内核 |
| 内核驱动 | binder_transaction() 执行 copy_from_user(唯一一次数据拷贝),注入安全凭据,唤醒 Server 线程 |
| 服务端 Native 层 | IPCThreadState 从 ioctl 返回,调用 BBinder::transact() |
| 服务端 Java 层 | Stub.onTransact() 反序列化参数,调用 add() 业务实现,将结果 100 写入 reply |
| 返回链路 | reply 数据原路返回:驱动 → Client Native → JNI → Java,add() 调用正式返回 |
这就是 AIDL 和 Binder 的全部秘密。表面上看,mCalculator.add(42, 58) 和一个普通的本地方法调用毫无区别;但在底层,它穿越了 Java 框架、JNI 桥接、C++ 通信引擎和 Linux 内核驱动四个层级。AIDL 自动生成的 Proxy-Stub 架构,将这一切复杂性完美封装,让开发者可以像调用本地方法一样进行跨进程通信——这正是 Binder 设计的终极优雅之处。