Binder(一):一次方法调用,究竟怎样跨进程执行?

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 对象是 BpBinderBpBinder::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 端点和目标进程。handlebinder_refbinder_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,底层 IBinderandroid.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 又怎样按同一份约定读回参数和异常。

源码与官方文档

相关推荐
壮哥_icon2 小时前
【Android 系统开发】使用 BAT 脚本高效自动化管理 /system/priv-app/ 系统应用(安装与卸载)
android·运维·自动化
用户69371750013843 小时前
Claude Code终端日志Token占用实测:一个过滤器砍掉60%-90%
android·前端·后端
蝉蜕日记3 小时前
AccumuPDF高级版 v2.57 | 视图文转换PDF,合并分割压缩
android·智能手机·pdf·生活·软件需求
吐了啊取名字太难3 小时前
美颜系统AI修图本地跑并支持Mac、win、安卓、iOS不卡顿
android·人工智能·windows·数码相机·mac·ai编程
阿pin5 小时前
Android随笔-Retrofit
android·retrofit
想取一个与众不同的名字好难5 小时前
安卓自定义颜色选择器
android
我命由我123456 小时前
Android 开发问题:java.lang.NoSuchMethodError:No virtual method readAllBytes()
android·java·java-ee·kotlin·android studio·android-studio·android runtime
Coffeeee6 小时前
ios零基础的Android开发能否靠AI让老板省一笔人工费呢
android·ios·ai编程
Joey_friends6 小时前
指纹authenticate流程图
android·java·c++