Binder(二):AIDL 生成的 Proxy、Stub 和 Parcel 到底在做什么?
上一篇完成了端到端闭环:
text
业务 Proxy
→ BinderProxy
→ JNI / libbinder
→ Binder Driver
→ 服务端 Binder 线程
→ Stub.onTransact()
→ 业务实现
但其中最靠近业务的一段还没有拆开:
java
int result = calculator.add(1, 2);
为什么双方能约定:
text
这笔事务调用 add;
第一个 int 是 a;
第二个 int 是 b;
返回的 int 是 result;
服务端异常应该在客户端重新抛出?
答案是:AIDL 编译器把接口定义生成了一份双方共同遵守的协议实现。
text
业务 Proxy:本地方法 → transaction code + data Parcel
AIDL Stub:transaction code + data Parcel → 本地方法
reply Parcel:服务端结果 / 异常 → 客户端返回值 / 异常
源码和生成工具沿用导读第四节"系列统一基线"。下面展示 Build Tools 36.0.0 生成结构的等价主干;生成片段和哈希见 generated-reference。
一份接口,怎样变成两端协议
AIDL 文件本身不会跨进程
用同步 add() 和一笔 oneway 比较生成协议:
aidl
interface ICalculator {
int add(int a, int b);
oneway void notifyValue(int requestId, int value);
}
完整协议见 BinderLab AIDL 目录,其中还包含异步 callback、同步 callback 与不同 node 对照接口。
.aidl 文件只定义协议:
text
接口全名
方法集合
参数顺序和类型
返回值类型
方法是否 oneway
真正让 Java 代码可调用的是生成结果。最关键的结构可以压缩为:
java
public interface ICalculator extends IInterface {
int add(int a, int b) throws RemoteException;
abstract class Stub extends Binder implements ICalculator {
static ICalculator asInterface(IBinder obj) { ... }
boolean onTransact(int code, Parcel data, Parcel reply, int flags) { ... }
private static class Proxy implements ICalculator {
private IBinder mRemote;
int add(int a, int b) { ... }
}
}
}
AIDL 不是 Binder Driver,也不是 Parcel 本身。它负责生成把"业务接口语言"翻译成"Binder 事务语言"的胶水代码。
Proxy 编码,Stub 解码

这张图按中间的"协议与传输边界"阅读:
text
左边只看 Proxy 怎样写;
中间先区分 AIDL 接口协议与 Binder 传输上下文;
AIDL 区域只包含 descriptor、transaction code 和 Parcel schema;
handle 只出现在下层 remote IBinder / BpBinder 传输上下文;
右边只看 Stub 怎样读;
下方比较本地直调与跨进程路径。
核心关系只有两行:
text
Proxy.add() --编码--> code + data Parcel
Stub.onTransact() --解码--> service.add()
IInterface 与 IBinder 不在同一抽象层
这是理解 AIDL 生成代码最关键的桥梁。
1. IInterface 表达"它能做什么"
Android 的 IInterface 只有一个桥接方法:
java
public interface IInterface {
IBinder asBinder();
}
ICalculator extends IInterface 后,业务代码获得的是类型化能力:
text
add()
notifyValue()
notifyValueViaHandler()
registerCallback()
addAndCallback()
getAsyncWorker()
addWithRequestId()
2. IBinder 表达"怎样把它作为 Binder 端点调用和传递"
IBinder 提供的是通用 Binder 能力:
text
transact()
queryLocalInterface()
linkToDeath()
isBinderAlive()
getInterfaceDescriptor()
可以压缩成:
IInterface表达业务接口,IBinder表达可远程调用端点。
3. asInterface() 和 asBinder() 是两座反向桥
text
IBinder
↓ Stub.asInterface()
类型化 ICalculator
text
ICalculator
↓ asBinder()
通用 IBinder
前者让拿到通用 Binder 引用的客户端恢复业务接口;后者让业务接口能作为 Binder 对象继续注册、传递或调用通用能力。
生成代码里的两条分支
Stub 同时连接事务入口与业务接口
生成结构是:
java
public abstract static class Stub
extends Binder
implements ICalculator {
}
两个父类型承担不同职责:
text
extends Binder:
让 Stub 成为本地 Binder 实体,能够接收 transact。
implements ICalculator:
让 Stub 或其子类提供 add() 等业务方法。
构造函数把业务接口与 Binder 端点关联起来:
java
public Stub() {
attachInterface(this, DESCRIPTOR);
}
Android 16 的 Binder.attachInterface() 保存:
text
mOwner = this
mDescriptor = "com.example.binderdemo.ICalculator"
这为同进程快速路径准备了条件。
Stub.asInterface() 先判断本地还是远端
按生成结构整理后的等价主干是:
java
public static ICalculator asInterface(IBinder obj) {
if (obj == null) {
return null;
}
IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (iin instanceof ICalculator) {
return (ICalculator) iin;
}
return new Proxy(obj);
}
Binder.queryLocalInterface() 会检查 descriptor,并在匹配时直接返回构造 Stub 时保存的本地 owner,见 Binder.java。
因此:
| 场景 | queryLocalInterface() |
asInterface() 返回 |
调用路径 |
|---|---|---|---|
| 同进程、同后端、本地 Binder | 找到本地 owner | Stub/业务实现对象 | 普通 Java 直接调用 |
| 跨进程远端 Binder | 返回 null |
新建业务 Proxy | Parcel + transact + Driver |
官方 AIDL overview 明确说明:同进程且使用同一后端时不存在 Proxy,也不需要打包和解包。
所以三句常见说法都是错的:
text
写了 AIDL 就一定跨进程。
asInterface() 一定返回 Proxy。
服务端方法一定在 Binder 线程执行。
是否跨进程,取决于实际 Binder 引用指向本地端点还是远端端点。
两种 Proxy 分别保存业务协议与通用传输
业务 Proxy 的结构是:
java
private static class Proxy implements ICalculator {
private IBinder mRemote;
Proxy(IBinder remote) {
mRemote = remote;
}
@Override
public IBinder asBinder() {
return mRemote;
}
}
对象关系是:
text
ICalculator.Stub.Proxy
│
│ 实现 add(),理解业务协议
│ mRemote
▼
IBinder
│
└─ 跨进程时通常由 BinderProxy 实现
只提供通用 transact 能力
因此不要写:
text
BinderProxy.add() 把参数写进 Parcel。
准确说法是:
text
AIDL 业务 Proxy.add() 写 Parcel;
它再调用 mRemote,也就是 BinderProxy.transact()。
跟一笔 add() 走完编解码
Proxy.add() 写入请求
Build Tools 36.0.0 生成结构的等价主干是:
固定生成物使用无参
Parcel.obtain(),没有生成enforceNoDataAvail()。AIDL 输出会随工具版本变化;精确结构见生成主干摘录和四个文件的 SHA-256。
java
@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(TRANSACTION_add, data, reply, 0);
reply.readException();
result = reply.readInt();
} finally {
reply.recycle();
data.recycle();
}
return result;
}
写入顺序就是协议的一部分:
text
1. interface token
2. 参数 a
3. 参数 b
服务端必须按相同顺序读取。
descriptor、transaction code 与 handle 各有作用域
Binder 中的多个"编号"必须放进各自坐标系:
| 标识 | 识别什么 | 作用范围 |
|---|---|---|
| Service name | 去哪里发现根服务 | ServiceManager 注册表 |
| Binder handle | 当前进程引用哪个远端 node | 当前引用持有进程 |
| Interface descriptor | Binder 端点实现什么接口 | 接口协议 |
| Transaction code | 调用接口中的哪个方法 | 当前接口 |
| UID / PID | 谁发起当前事务 | 调用身份 |
| TID | 当前哪条线程在执行 | 当前进程线程空间 |
ICalculator 示例中:
java
DESCRIPTOR = "com.example.binderdemo.ICalculator";
TRANSACTION_add = IBinder.FIRST_CALL_TRANSACTION + 0;
TRANSACTION_notifyValue = IBinder.FIRST_CALL_TRANSACTION + 1;
TRANSACTION_notifyValueViaHandler = IBinder.FIRST_CALL_TRANSACTION + 2;
TRANSACTION_registerCallback = IBinder.FIRST_CALL_TRANSACTION + 3;
TRANSACTION_addAndCallback = IBinder.FIRST_CALL_TRANSACTION + 4;
TRANSACTION_getAsyncWorker = IBinder.FIRST_CALL_TRANSACTION + 5;
TRANSACTION_addWithRequestId = IBinder.FIRST_CALL_TRANSACTION + 6;
这组数值来自 AIDL 的声明顺序,接口重排后可能变化,不应在外部协议中手写依赖它。
因此:
text
descriptor 不是 ServiceManager 注册名;
transaction code 不是远端对象编号;
handle 也不是方法编号。
interface token 守住接口边界
Proxy 先写:
java
data.writeInterfaceToken(DESCRIPTOR);
Stub 在处理用户事务前执行:
java
data.enforceInterface(DESCRIPTOR);
Android 16 的 Parcel.writeInterfaceToken() / enforceInterface() 会写入并校验接口头。
它回答的是:
text
发送方和接收方是否在使用同一份接口 descriptor?
它不负责:
text
定位远端进程;
选择 add 方法;
验证调用者是否拥有业务权限。
Stub.onTransact() 读回参数并调用实现
服务端收到事务后,生成的分发结构是:
java
@Override
public boolean onTransact(
int code, Parcel data, Parcel reply, int flags)
throws RemoteException {
if (code >= FIRST_CALL_TRANSACTION
&& code <= LAST_CALL_TRANSACTION) {
data.enforceInterface(DESCRIPTOR);
}
switch (code) {
case TRANSACTION_add: {
int a = data.readInt();
int b = data.readInt();
int result = this.add(a, b);
reply.writeNoException();
reply.writeInt(result);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
这里完成了四步反向翻译:
text
校验接口 token
↓
用 transaction code 选择方法
↓
按协议顺序读取参数
↓
调用本地业务实现并写 reply
驱动只把 code 和 buffer 交到端点;Stub.onTransact() 才知道 code 对应 add()。
协议不只包含参数和返回值
reply 同时承载异常状态
正常路径:
java
// 服务端
reply.writeNoException();
reply.writeInt(result);
// 客户端
reply.readException();
return reply.readInt();
AIDL/Binder 支持的异常或状态类别可以编码进 reply;任意自定义 Java 异常不会自动变成稳定的跨进程协议。如果传输层失败,例如远端死亡,transact() 会以 RemoteException 或其子类暴露失败。
需要区分:
| 失败层次 | 例子 | 客户端观察 |
|---|---|---|
| 接口/业务失败 | 非法参数、服务自定义错误 | reply 中的异常或状态 |
| 协议失败 | descriptor 或 code 不匹配 | SecurityException、false 或未知事务 |
| 传输失败 | 远端死亡、驱动事务失败 | RemoteException / DeadObjectException 等 |
官方 AIDL backends 的错误处理 还强调:发生错误时,返回值和 out/inout 参数可能处于不确定状态,不能继续当作有效结果使用。
Parcel 还携带 Binder 对象语义
对 add(int, int) 来说,Parcel 看起来只是顺序写入几个值。但 Binder 协议还允许写入:
text
字符串和 Parcelable
Binder 接口引用
文件描述符
异常状态
返回值
例如 callback 参数会生成:
java
data.writeStrongInterface(callback);
服务端读取:
java
IResultCallback callback =
IResultCallback.Stub.asInterface(data.readStrongBinder());
Binder 引用和文件描述符不能仅靠原始字节复制保持语义。Parcel 还维护特殊对象位置,驱动会在跨进程时转换它们。第三、四篇再分别展开对象和驱动视角。
oneway 改变 flags 与 reply 约定
notifyValue() 的 Proxy 主干是:
java
Parcel data = Parcel.obtain();
try {
data.writeInterfaceToken(DESCRIPTOR);
data.writeInt(requestId);
data.writeInt(value);
mRemote.transact(
TRANSACTION_notifyValue,
data,
null,
IBinder.FLAG_ONEWAY);
} finally {
data.recycle();
}
与同步 add() 对比:
text
没有 reply Parcel;
flags 带 FLAG_ONEWAY;
调用方不等待业务 reply。
这不代表新建线程,也不代表服务端已经执行完成。调用方仍要完成 Parcel 编码、进入驱动并面对 buffer 压力;"不等业务 reply"不能扩写成"绝对零等待、绝不失败"。线程模型留到第六篇。
本地与远端分支的对照证据
实验 A:服务和客户端同进程
text
Stub.asInterface(localBinder)
↓ queryLocalInterface 命中
返回本地 Stub / 业务对象
↓
调用线程直接执行 add()
预期证据:
text
clientPid == serverPid
clientTid == add() 内的 tid
calculator.getClass() 是本地实现类
实验 B:服务声明为远端进程
xml
<service
android:name=".CalculatorService"
android:process=":remote" />
预期证据:
text
clientPid != serverPid
clientTid != serverTid
calculator.getClass() 是 ICalculator$Stub$Proxy
calculator.asBinder().getClass() 是 BinderProxy
实验时应打印实际 class、PID、TID 和线程名。只看到"方法成功返回"不能证明经过 Binder Driver。
Build Tools / AIDL 36.0.0 的生成代码已用 SDK Platform 36.1 构建。API 36 基线日志记录了不同的 App/:remote PID、ICalculator$Stub$Proxy 和 android.os.BinderProxy,证明该次 asInterface() 选择了远端 Proxy 路径。证据包没有同进程对照,因此本地分支仍是源码路径,不是该次运行的观测结果。
协议错位:token 或 transaction code 不匹配
下面列出的是可能表现 。最终 Java 层看到 SecurityException、false、未知事务或其他异常,取决于具体 onTransact()、transact() 返回值、是否存在 reply、JNI 映射以及生成代码版本,不能从"协议不匹配"直接推出唯一异常类型。
假设手写客户端把 TRANSACTION_notifyValue 当成 add() 发送:
text
客户端以为两个 int 分别是 a、b,code 却指向 notifyValue;
服务端把相同两个字段解释成 requestId、value,并执行 notifyValue 的副作用;
notifyValue 是 oneway,不会按 add 协议写回 int reply;
客户端若继续按 add 协议读取 reply,方法语义和 reply 协议都已经分裂。
或者客户端写错 descriptor:
text
data.writeInterfaceToken("wrong.interface");
服务端 enforceInterface() 会拒绝这笔事务。
字段数量碰巧相同不代表协议兼容;transaction code + Parcel schema + oneway/reply 约定 才共同定义一笔调用。这说明 AIDL 的价值不只是"少写代码",而是让两端从同一接口定义生成对称协议,降低手写 code、顺序、类型和异常路径不一致的风险。
把协议角色重新放回调用链
text
AIDL:
定义接口,并生成双方一致的协议胶水。
业务 Proxy:
把类型化方法调用编码成 descriptor、transaction code 和 data Parcel。
IBinder / BinderProxy:
提供通用 transact 能力。
AIDL Stub:
校验 descriptor,用 code 选择方法,解码参数并调用本地实现。
reply Parcel:
把返回值或异常状态送回客户端。
到这里,AIDL 的边界很明确:它负责生成对称的协议胶水,不负责解释 mRemote 为什么能代表远端对象。沿着 mRemote 再往下,才会进入 BinderProxy、BpBinder、handle、binder_ref 和 binder_node 的对象映射。