Binder(三):客户端拿到的 BinderProxy,到底指向远端什么对象?
前两篇已经完成:
第一篇:一次调用怎样跨进程并返回。
第二篇:方法、参数和返回值怎样变成 AIDL 协议。
现在剩下整个 Binder 对象模型最关键的问题:
客户端明明不可能直接持有另一个进程中的 Java/C++ 对象地址,为什么一个本地
BinderProxy却能代表远端服务?
沿着这个代理逐层向下看,得到的不是远端地址,而是一组映射:
客户端持有的不是服务端对象地址,而是一条分层远端引用链。
Java:业务 Proxy → BinderProxy
Native:BinderProxy → BpBinder(handle)
Kernel:当前进程的 binder_ref(handle) → 服务端 binder_node
服务端:事务到达拥有进程后,通过 ptr/cookie 恢复 BBinder / JavaBBinder → Java Stub
BinderProxy 只是这条链在客户端 Java 层的入口。真正让“远端对象引用”成立的是:用户态代理、进程内 handle、驱动 binder_ref 和服务端 binder_node 共同维护的映射。
源码按导读第四节“系列统一基线”定位,驱动对象使用其中固定的 Android Common Kernel commit。
远端引用为什么不能是一段地址
地址只在所属进程里有意义
假设服务端创建一个已经实现全部 AIDL 方法的 Stub。下面一行是只强调“本地对象属于服务端地址空间”的伪代码,不是 BinderLab CalculatorService 的构造方式:
ICalculator.Stub calculatorBinder = ...;
这个 Java 对象、对应的 native JavaBBinder,以及它们使用的内存地址都属于服务端进程地址空间。
即使客户端得到一个数值相同的地址:
0x7a12340000
它在客户端地址空间也可能:
没有映射;
映射到完全无关的数据;
在下一次运行中变化;
不具备访问权限。
因此 Binder 不能把普通对象指针直接交给客户端。它必须建立一种“可跨进程引用、但不暴露远端内存”的对象模型。
三层对象映射
这张图按层次阅读:
第一行:业务接口对象;
第二行:Java Binder 端点;
第三行:Native Binder 对象;
第四行:Kernel 中的远端引用和本地实体。
不要把图里所有对象都简称为“Binder 代理”。它们的职责不同:
| 对象 | 所在位置 | 代表什么 |
|---|---|---|
ICalculator.Proxy | 引用持有进程 Java | 类型化业务接口代理 |
BinderProxy | 引用持有进程 Java | 通用远端 IBinder 代理 |
BpBinder | 引用持有进程 Native | 持有目标 handle 的 native 代理 |
binder_ref | 引用持有进程驱动上下文 | 当前进程对远端 node 的引用 |
| handle | 引用持有进程用户态 | 当前进程查找 binder_ref 的 key |
binder_node | 驱动 | node 拥有进程本地 Binder 实体的内核表示 |
BBinder / JavaBBinder | node 拥有进程 Native | 本地 Binder 实体 |
AIDL Stub | node 拥有进程 Java | 接收事务并分发业务方法 |
从 BinderProxy 追到 handle
业务 Proxy 与 BinderProxy 不是同一个对象。沿下面这条链继续向 Native 和 Kernel 追踪:
ICalculator.Proxy
↓ mRemote.transact()
BinderProxy
用户态代理与驱动引用怎样接起来
BinderProxy 在 native 层持有 IBinder
Java BinderProxy 的 native 数据中保存一个 sp<IBinder>。Android 16 的 javaObjectForIBinder() 会区分两种情况:
如果 native IBinder 是本进程 JavaBBinder:
返回原来的 Java Binder 对象。
否则:
为这个 native IBinder 创建或复用 Java BinderProxy。
关键结构可以整理成:
BinderProxyNativeData* nativeData = new BinderProxyNativeData();
nativeData->mObject = val; // sp<IBinder>
对 kernel Binder 远端端点来说,这个 IBinder 通常是 BpBinder。
BpBinder::transact() 最终取出:
binderHandle()
交给 IPCThreadState::transact()。
因此更准确的说法不是:
BinderProxy 指向服务端 Java 对象。
而是:
BinderProxy 持有一个 native 远端 IBinder;
对 kernel Binder 来说通常是 BpBinder;
BpBinder 用当前进程中的 handle 标识远端 Binder node。
BBinder / BpBinder:本地端点与远端代理
可以先把名字记成两个方向:
BBinder:本进程拥有的本地 Binder 实体。
BpBinder:当前进程持有的远端 Binder 代理。
Java 服务端 Binder 进入 native 后由 JavaBBinder 承接;JavaBBinder 继承本地 Binder 实体路径。远端端点进入 Java 时则包装为 BinderProxy,native 侧由 BpBinder 提供 transact()。
这不是固定的“服务端类”和“客户端类”。同一个进程可以同时:
拥有本地 BBinder / JavaBBinder;
持有其他进程的 BpBinder;
对某笔事务是服务端;
对下一笔事务又是客户端。
binder_node 归属于端点拥有进程
驱动中的 binder_node 记录本地 Binder 实体的内核表示,其中关键关系包括:
node->proc
这个 node 由哪个 binder_proc 承载
node->ptr / node->cookie
事务到达拥有进程时,用于还原本地 BBinder 的用户态标识
node->refs
哪些外部进程持有对它的 binder_ref
binder_node 不是 Java 对象本身,也不理解 AIDL 接口。它是驱动维护路由、引用和生命周期所需的端点表示。
binder_ref 归属于引用持有进程
驱动中的 binder_ref 同时保存:
ref->proc
哪个进程持有这条引用
ref->node
引用指向哪个服务端 binder_node
ref->data.desc
用户态看到的 handle
源码注释直接把它描述为:
从 procA 指向 procB 中目标 node 的引用结构。
所以对象关系是:
客户端 binder_proc
└─ binder_ref(desc = handle)
└─ target binder_node
└─ 服务端 binder_proc
handle 只是当前进程的索引
handle 是某个客户端进程查找自己 binder_ref 的编号。
同一个服务端 binder_node:
在客户端进程 A 中可能是 handle 3;
在客户端进程 C 中可能是 handle 7;
在 node 自己的服务端进程中不是靠这两个 handle 找本地对象。
驱动处理事务时会在“发送方进程的 ref 表”中解析 handle。Android Common Kernel 的 binder_transaction() 清楚表现了这个范围:
ref = binder_get_ref_olocked(
proc,
tr->target.handle,
true);
target_node = binder_get_node_refs_for_txn(
ref->node,
&target_proc,
&return_error);
这里的 proc 是发送方 binder_proc。因此 handle 不能脱离进程上下文解释,更不是全系统唯一对象 ID。
ProcessState 复用同一 handle 的 BpBinder
Native 用户态的 ProcessState::getStrongProxyForHandle() 会在进程级表中查 handle:
handle_entry* e = lookupHandleLocked(handle);
if (e->binder == nullptr || oldProxyCannotBePromoted) {
sp<BpBinder> bp = BpBinder::create(handle);
e->binder = bp.get();
}
这个缓存让同一进程中同一远端 handle 尽量复用 BpBinder 身份,而不是每次读取 Parcel 都创建毫无关联的新代理。
进一步到 Java,javaObjectForIBinder() 也会尝试复用仍存活的 BinderProxy。
但“本地代理对象身份被复用”仍不等于“远端进程永远存活”。第八篇会解释这个生命周期差异。
引用可以继续传播,也可以反向调用
Binder 引用写进 Parcel 时会被转换
普通 int 可以复制其值,Binder 引用不能复制远端对象内存。概念上只需要知道:Parcel 会把 Binder 引用作为特殊对象记录,驱动再按接收进程视角传播对同一个 node 的引用。
传递本地 Binder:接收进程获得自己的远端引用;
转发远端 Binder:新接收进程获得指向同一 node 的自己的引用;
引用回到 node 拥有进程:可以恢复为本地端点语义。
BINDER_TYPE_BINDER、BINDER_TYPE_HANDLE 与驱动转换函数统一放在第四篇“特殊对象转换”章节。
接收方把新 handle 包回 BinderProxy
接收侧最终得到两类结果之一:
引用回到 node 拥有进程:恢复本地 Binder;
接收进程只持有远端引用:创建或复用 BinderProxy。
最后 IResultCallback.Stub.asInterface() 等类型化桥接函数,再把通用 IBinder 包成业务接口。具体 flattenBinder() / unflattenBinder() 与内核对象翻译仍由第四篇负责。
callback 会反转引用持有关系
Binder 的 client/server 是“一笔事务中的角色”,不是进程永久身份。
加入 callback:
interface IResultCallback {
oneway void onResult(int requestId, int value);
}
interface ICalculator {
void registerCallback(IResultCallback callback);
}
BinderLab 的异步 callback可以验证 Binder 引用传播和 client/server 角色反转,但不形成同步嵌套事务,也不能证明客户端原等待线程被复用;第六篇使用非 oneway 的 ISyncResultCallback 验证重入。
注册时:
App:
ICalculator 的客户端
IResultCallback 的服务端
远端进程:
ICalculator 的服务端
IResultCallback 的客户端
调用 registerCallback(callback) 时,App 把自己的本地 Binder 实体作为 Parcel 特殊对象发给远端。驱动为远端建立对这个 callback node 的引用。之后远端调用:
callback.onResult(requestId, 3);
方向就变成:
远端 callback Proxy
→ Binder Driver
→ App Binder 线程
→ IResultCallback.Stub.onTransact()
这说明 ServiceManager 不是获得 Binder 引用的唯一方式。找到根服务之后,更多 Binder 引用可以作为参数和返回值继续传播。
Binder 引用也是一项可传递能力
当一个进程持有 Binder 引用时,可以把它理解为:
当前进程获得了向这个 Binder node 发起事务尝试的能力。
但这不代表:
拥有服务端对象内存;
继承服务端进程权限;
自动通过业务权限检查;
能访问服务端所有内部状态。
这里要把“取得引用”和“使用引用”拆成两个授权面,否则很容易把能力模型误写成“一旦拿到就能调用”:
取得引用:
ServiceManager 查询时的 find 权限;
上游服务是否愿意把某个 Binder 引用作为参数或返回值交给当前调用方。
使用或继续传递引用:
SELinux binder call / transfer 规则;
服务端基于 calling UID、可用时的 calling SID、声明权限等做的授权;
接口参数、对象状态、用户范围和业务规则校验。
因此,“持有引用”只说明本地已经有一个引用入口,可以尝试发起 transaction。事务能否通过传输层与 MAC 检查、服务端是否接受请求,仍由各层独立决定;不能把“拿到引用”缩写成“已经获准访问端点”。
真机证据:对象类型与 callback 方向
客户端拿到接口后打印:
Log.i(TAG, "calculator=" + calculator.getClass().getName());
Log.i(TAG, "binder=" + calculator.asBinder().getClass().getName());
服务端注册 callback 时打印:
@Override
public void registerCallback(IResultCallback callback) {
Log.i(TAG, "callback=" + callback.getClass().getName());
Log.i(TAG, "callbackBinder="
+ callback.asBinder().getClass().getName());
}
跨进程时应观察:
App 中 calculator 是 ICalculator$Stub$Proxy;
App 中 calculator.asBinder() 是 BinderProxy;
远端进程中 callback 是 IResultCallback$Stub$Proxy;
远端调用 callback 后,App 的 IResultCallback.Stub 在 App Binder 线程执行。
如果服务和客户端同进程,类型可能直接是本地 Stub,不能套用远端对象链。
API 36 async-callback 日志同时记录了正向 ICalculator$Stub$Proxy / BinderProxy 和反向 IResultCallback$Stub$Proxy / BinderProxy,并用 PID、TID、方向和 requestId 关联 App callback。它证明了正反向事务中的引用持有角色发生反转;Java class、对象相等或 hash 仍不能证明远端 binder_node 的身份、存活性或 generation。
错误排障:跨进程比较 handle 数字
错误排障记录:
进程 A 日志里目标 handle=3;
进程 C 日志里目标 handle=7;
所以它们不是同一个服务。
这个结论不成立,因为 handle 的作用域是“当前持有引用的进程”。
正确核对方式需要结合:
handle 属于哪个 binder_proc;
该 binder_ref 指向哪个 binder_node;
node 由哪个服务端 binder_proc 承载。
只比较两个进程中的整数 handle,没有跨进程对象身份意义。
BinderProxy 代表的是一条引用链
更准确的答案是:
BinderProxy 不直接指向服务端 Java 对象。
它在客户端 Java 层包装一个 native IBinder;
对 kernel Binder 远端端点,这个对象通常是 BpBinder;
BpBinder 持有当前进程中的 handle;
handle 对应当前进程的 binder_ref;
binder_ref 指向服务端 binder_node;
binder_node 不直接持有 Java/C++ 对象;事务交付到拥有进程时,接收侧通过 ptr/cookie 恢复本地 BBinder / JavaBBinder。
现在可以把“代理指向远端对象”改写成更精确的话:代理持有当前进程的远端引用入口,驱动再把这条引用解析到目标 node。至于一笔事务如何沿这张对象图准备 buffer、转换特殊对象并进入目标工作队列,就是下一层问题。