Binder(一):一次方法调用,究竟怎样跨进程执行?
上一篇 JNI 文章把调用链停在了这里:
scss
BinderProxy.transact()
↓
transactNative()
↓ JNI
android_os_BinderProxy_transact()
↓
BpBinder::transact()
↓
IPCThreadState::transact()
↓
ioctl(BINDER_WRITE_READ)
↓
Binder Driver
JNI 已经解释了 Java 怎样进入当前进程的 C++,却没有回答一个更关键的问题:
ioctl(BINDER_WRITE_READ)之后,一次看起来普通的calculator.add(1, 2),为什么能在另一个进程的对象中执行?
把这次调用拆成两个执行片段,模型会清楚很多:
scss
客户端调用线程不会"穿越"到服务端。
客户端业务 Proxy 把方法调用编码成事务;
Binder Driver 根据远端引用找到目标进程和 Binder node;
服务端进程中另一条 Binder 线程接收事务;
AIDL Stub 再把 transaction code 和 Parcel 还原成 add(1, 2);
同步调用的结果通过 reply 事务返回,客户端原线程才继续执行。
Binder 建立的不是"线程穿越",而是:
diff
客户端线程等待
+
服务端 Binder 线程执行
+
reply 把两个执行片段重新接起来
源码口径沿用系列统一基线;驱动段落固定到 Android Common Kernel android16-6.12-2025-12_r1 的 commit 67fe3c9df146f5752b3cd5c69c8e0460221a8018。
一笔 add(),两段执行
最小同步接口
先用最小 add() 建立同步调用模型:
csharp
interface ICalculator {
int add(int a, int b);
}
完整协议见 BinderLab AIDL。实机证据使用同步方法 addWithRequestId(requestId, a, b, false),其中 requestId 只用于关联跨进程日志,false 表示不注入 Handler 阻塞。
客户端看到的调用只有一行:
ini
int result = calculator.add(1, 2);
但跨进程时,它至少涉及两个进程和两条 Linux 线程:
| 位置 | 进程 | 线程 | 当前任务 |
|---|---|---|---|
| 调用前 | 客户端进程 | 客户端调用线程 | 执行 calculator.add() |
| 发送事务 | 客户端进程 | 仍是客户端调用线程 | 编码 Parcel,进入 JNI、libbinder 和驱动 |
| 处理事务 | 服务端进程 | 服务端 Binder 线程 | 解码事务并调用真正的 add() |
| 等待 reply | 客户端进程 | 原客户端调用线程 | 阻塞等待返回值 |
| 返回后 | 客户端进程 | 原客户端调用线程 | 读取结果并继续运行 |
没有一条线程从进程 A 变成进程 B 的线程。真正变化的是:
执行权从客户端线程的一段调用栈
↓ 事务
交给服务端 Binder 线程的另一段调用栈
↓ reply
再恢复客户端原线程
一张图看清跨进程边界

这张图只看三件事:
横向:当前在哪个进程;
纵向:调用从 Java、Native 到 Kernel,再回到 Java;
虚线:客户端原线程正在等待,服务端另一条线程正在执行。
调用主链如下:
scss
客户端业务代码
↓
AIDL 业务 Proxy.add()
↓
BinderProxy / libbinder
↓
Binder Driver
↓
服务端 Binder 线程 + AIDL Stub
↓
服务端 add(1, 2)
↓
reply 原路返回
同一笔调用会在 Java、Native、Kernel 和服务端入口之间不断转换表示形式。
业务 Proxy 把方法变成事务
AIDL 生成的业务 Proxy 会把本地方法调用翻译成 Binder 协议:
css
add(a, b)
→ descriptor + transaction code + 参数 Parcel
→ mRemote.transact(...)
→ reply Parcel
业务 Proxy 理解 add(),Binder Driver 只处理事务和对象引用,不理解 add() 的业务语义。完整生成代码、写入顺序和异常协议见第二篇。
业务 Proxy 与 BinderProxy 各管一层
上面的 mRemote 声明为 IBinder。跨进程时,它通常是 BinderProxy:
scss
ICalculator.Proxy
负责类型化业务方法:add()
│
│ mRemote : IBinder
▼
BinderProxy
负责通用传输入口:transact()
第二篇继续解释业务 Proxy、BinderProxy、asInterface() 与 asBinder() 的关系。
沿调用栈下到驱动
JNI 只负责进入当前进程的 native 层
transactNative() 注册到 android_os_BinderProxy_transact()。核心逻辑是:
ini
Parcel* data = parcelForJavaObject(env, dataObj);
Parcel* reply = parcelForJavaObject(env, replyObj);
IBinder* target = getBPNativeData(env, obj)->mObject.get();
status_t err = target->transact(code, *data, reply, flags);
JNI 在这里完成的是:
Java Parcel → Native Parcel 指针
Java BinderProxy → Native IBinder / BpBinder
Java 参数 → C++ transact 参数
JNI 本身没有把数据送到另一个进程。它只是让客户端原线程继续进入本进程的 Native Binder 实现。
远端代理对应的 native 对象是 BpBinder。BpBinder::transact() 最终把目标 handle、code、Parcel 和 flags 交给当前线程的 IPCThreadState:
css
status = IPCThreadState::self()->transact(
binderHandle(), code, data, reply, flags);
五维坐标没有歧义:
| 维度 | IPCThreadState::transact() 所在位置 |
|---|---|
| 进程 | 客户端进程 |
| 线程 | 原客户端调用线程 |
| 层次 | Native 用户态 libbinder |
| 对象 | 当前线程的 Binder IPC 状态 |
| 数据 | handle、code、Parcel、flags,即将变成 BC_TRANSACTION |
IPCThreadState 组织并提交事务
IPCThreadState::transact() 先调用 writeTransactionData(),把事务写进线程级输出缓冲区:
diff
BC_TRANSACTION
+
binder_transaction_data
随后 talkWithDriver() 组织 binder_write_read,通过:
scss
ioctl(mProcess->mDriverFD, BINDER_WRITE_READ, &bwr)
进入 Binder Driver。
这里需要精确区分三个边界:
scss
BinderProxy.transactNative()
Java → JNI / Native,未跨进程
ioctl(BINDER_WRITE_READ)
用户态 → 内核态,仍是客户端原线程
服务端收到 BR_TRANSACTION
服务端另一条 Binder 线程开始处理
"进入内核"和"到达另一个进程"不是同一瞬间。
Driver 负责路由、转换与排队
1. 解析目标
驱动根据当前进程持有的远端引用定位目标 Binder 端点和目标进程。handle、binder_ref、binder_node 的对象关系由第三篇完整解释。
2. 准备目标事务
驱动为目标进程准备事务数据;普通值与 Binder 引用、文件描述符等特殊对象的处理方式不同,具体转换留到第四篇。
3. 排队并唤醒
事务进入明确目标线程或目标进程的工作队列,并在需要时唤醒相应 Binder 线程。该线程随后从驱动读到 BR_TRANSACTION。
Android 16 对应驱动主路径从 binder_transaction() 展开,第四篇再逐段阅读。
驱动能看到目标引用、code、flags、buffer、对象偏移和发送方凭据,却看不懂:
csharp
这是 ICalculator.add();
a 和 b 是 Java int;
这两个数字在业务上代表什么。
一句话:
驱动负责"送到哪个端点",AIDL Stub 负责"调用哪个方法"。
服务端把事务还原成 add(1, 2)
服务端 Binder 线程读到 BR_TRANSACTION 后,IPCThreadState::executeCommand() 建立接收侧 Parcel,找到本进程的 BBinder,再调用服务端 native Binder 端点。
Java 服务对应 JavaBBinder。它的 onTransact() 通过 JNI 回调 Java Binder.execTransact():
scss
env->CallBooleanMethod(
mObject,
gBinderOffsets.mExecTransact,
code,
reinterpret_cast<jlong>(&data),
reinterpret_cast<jlong>(reply),
flags);
Java 侧 Binder.execTransactInternal() 最终调用:
ini
res = onTransact(code, data, reply, flags);
AIDL 生成的 Stub.onTransact() 才真正理解接口协议:
ini
case TRANSACTION_add: {
int a = data.readInt();
int b = data.readInt();
int result = this.add(a, b);
reply.writeNoException();
reply.writeInt(result);
break;
}
到这里,事务又还原成了普通 Java 方法调用。但执行它的是服务端 Binder 线程,不是客户端线程,也通常不是服务端主线程。
reply 返回前,客户端原线程在等待
add() 没有 oneway,因此是一笔同步事务。
客户端 IPCThreadState::transact() 发送命令后进入 waitForResponse()。服务端执行 add()、写 reply,再通过 BC_REPLY 把结果交回驱动;客户端线程读到 BR_REPLY 后,才从 native 返回 Java Proxy:
scss
客户端线程
发送 BC_TRANSACTION
↓
等待 BR_REPLY ─────────────────────────┐
│
服务端 Binder 线程 │
收到 BR_TRANSACTION │
执行 Stub.onTransact() │
执行 add() │
发送 BC_REPLY ────────────────────────┘
客户端线程
reply.readException()
reply.readInt()
从 calculator.add() 返回
官方 Binder 线程模型同样把同步事务定义为"调用线程等待,目标 Binder 线程执行,reply 返回后调用线程继续",见 Binder threading。
这里描述的是普通同步路径。若服务端沿嵌套同步调用链再次调用回客户端进程,驱动可能复用这条正在等待的客户端线程执行回调,形成重入;第六篇再展开这个例外。
"返回"不是一个模糊时刻
四个容易混淆的完成里程碑
不能把下面四件事压成一句"Binder 调用成功了":
| 里程碑 | 常用可观测方式 | 能证明什么 | 不能证明什么 |
|---|---|---|---|
| 客户端完成 Parcel 编码 | AIDL Proxy 内最后一次 _data.write*() 之后、mRemote.transact() 之前的断点或埋点 |
请求已在用户态准备好 | 驱动尚未必接收 |
| 驱动接收事务 | Perfetto Binder flow、驱动 tracepoint / 内核证据 | 传输路径已进入内核 | 普通 App 日志本身看不到这一瞬间,服务端也尚未必开始执行 |
| 服务端业务方法返回 | Stub 中业务方法调用返回后的断点/埋点,或业务方法 slice 结束 | 业务实现已把控制权交回 Stub.onTransact() |
Stub 仍可能写 writeNoException() / 返回值并发送 reply;不能证明整个 Binder 入口已退出或客户端已恢复 |
| 客户端收到 reply | Proxy 返回日志 / 调用栈恢复 | 同步事务闭环 | 服务内部后续工作未必完成 |
业务层"调用前 / 调用后"埋点包住的是整笔同步事务:后一个点已经越过 reply,不能拿来标记 Parcel 刚编码完成。普通调用栈只能说明当前走到了哪个 Proxy / transact() 路径,也不能单独给出"最后一次 write*() 已完成"的精确时刻。要观察这个里程碑,断点或埋点必须落在生成 Proxy 的上述窄窗口。
需要注意:Binder 方法返回不一定代表服务内部异步工作已经完成。 post 后立即返回与同步等待两种模式由第六篇解释。
真机证据:两个进程、两端入口线程
BinderLab 的实机证据来自 Android 16 / API 36;PID、TID 和耗时只属于对应运行。
客户端调用前打印:
less
Log.i(TAG, "client"
+ " pid=" + Process.myPid()
+ " tid=" + Process.myTid()
+ " thread=" + Thread.currentThread().getName()
+ " remoteClass=" + calculator.getClass().getName());
int requestId = 1001;
int result = calculator.addWithRequestId(requestId, 1, 2, false);
服务端示例只保留 Binder 入口日志与返回值;完整实现还包含第九篇使用的 Handler 对照实验:
arduino
@Override
public int addWithRequestId(
int requestId,
int a,
int b,
boolean injectHandlerBlocker) {
Log.i(TAG, "server"
+ " requestId=" + requestId
+ " pid=" + Process.myPid()
+ " tid=" + Process.myTid()
+ " thread=" + Thread.currentThread().getName()
+ " callingUid=" + Binder.getCallingUid());
return a + b;
}
判断远端 Binder 的核心证据是:
javascript
客户端 PID != 服务端 PID
客户端拿到的是业务 Proxy,而不是本地 Stub 实现
服务方法确实在另一进程执行
客户端 TID 与服务端执行 TID 不同,可用来观察执行线程发生了变化,但不是额外的跨进程证明:PID 已经不同,而且 Linux 中不同线程本来就有各自唯一的 TID。
如果 PID 相同,后面的"Binder 线程执行"结论就不能从这组实验推出。第二篇会专门比较同进程和跨进程路径。
API 36 基线日志记录了同一 requestId 下不同的客户端/服务端 PID,客户端对象为 ICalculator$Stub$Proxy,底层 IBinder 为 android.os.BinderProxy,服务入口运行在远端 Binder 线程。这足以确认同步调用跨越了进程和线程边界;仅凭 PID/TID 仍不能定位驱动内部阶段,也不能证明服务内部异步工作已经完成。
真实 baseline 在服务端 Binder 入口后还会 post() 到 CalculatorWorker,并由 Binder 线程等待结果,因此完整实验链包含第三条 Handler 线程。这里的"两端入口线程"只描述 Binder 跨进程边界,不是说整个方法从头到尾只有两条线程。
错误模型:原线程被"切"到服务端
错误说法:
scss
客户端线程经过 Binder Driver,切换到服务端继续执行 add()。
这个模型会导致三个误判:
误以为服务端 ThreadLocal 与客户端连续;
误以为服务端锁和客户端锁属于同一线程上下文;
误以为客户端 ANR 时只需要看客户端线程。
准确模型是:
客户端线程发请求并等待;
服务端另一条 Binder 线程处理;
两边通过事务和 reply 建立同步关系。
排障时必须同时找:
css
谁在等待 reply;
谁在服务端执行;
服务端是否又等待 Handler、锁、I/O 或下一笔 Binder。
把整条调用链压缩成八步
现在可以给出不模糊的完整答案:
markdown
1. AIDL 业务 Proxy 把 add(1, 2) 编码成 transaction code 和 data Parcel;
2. BinderProxy 通过 JNI 找到 Native BpBinder;
3. IPCThreadState 把事务组织成 BC_TRANSACTION,并通过 ioctl 进入驱动;
4. 驱动根据远端引用找到目标 node 和进程,准备目标 buffer,排队并唤醒线程;
5. 服务端 Binder 线程收到 BR_TRANSACTION;
6. JavaBBinder 和 Binder.execTransactInternal 把事务送到 Stub.onTransact();
7. Stub 解码参数并调用服务端 add();
8. 同步 reply 沿事务链返回,客户端原线程读取结果后继续。
这八步解释了"调用怎样过去又回来",但暂时把 AIDL 生成代码当成了一个协议盒子。下一篇会拆开这个盒子:业务 Proxy 为什么知道 transaction code,Stub 又怎样按同一份约定读回参数和异常。