AIDL:Proxy 与 Stub 如何生成
AIDL:Proxy 与 Stub 如何生成
上一章我们通过 asInterface() 把句柄变成了类型化接口。这一章回答:这个“类型化接口” 是从哪来的,Proxy 和 Stub 的胶水代码到底怎么生成?
一份 .aidl 文件在构建期生成接口、Stub 和 Proxy。运行时没有 AIDL 解释器:Proxy 负责 写 Parcel 并调用 transact(),Stub 负责按事务码读参数、调用实现,再写回结果。
AIDL 生成代码,不执行 IPC
| 常见误解 | 正确理解 |
|---|---|
| AIDL 是运行时框架,负责跨进程调用 | AIDL 是编译期代码生成器,生成的代码才使用 Binder |
| 每次跨进程调用 AIDL 都“解释执行” | 生成的 Proxy/Stub 是普通代码,方法编号和 Parcel 读写都在编译期固定 |
in、out、inout 只是语法糖 | 它们决定参数是否写进请求、是否从回复读回,直接影响跨进程数据量 |
AIDL 是 Android 的接口定义语言。asInterface() 先通过 queryLocalInterface() 判断对象 是否同进程:本地对象直接返回接口实现,远端对象才包装为 Proxy。因此,不是每次 AIDL 方法调用都会产生一次跨进程事务。
核心结论
- 一份
.aidl接口会生成:接口(继承IInterface)、Stub(继承Binder并实现接口)、Proxy(持有远端IBinder并实现接口)。 Stub.asInterface()用queryLocalInterface()判断:同进程返回本地对象,跨进程创建 Proxy。- 每个方法有固定的事务 code;Proxy 按顺序写参数并发起
transact(),Stub 的onTransact()按 code 分发并读取参数。 in表示参数只进不出,out表示只出不进,inout表示双向,后者跨进程成本最高。- 同进程调用走本地路径,不进驱动;跨进程调用才有序列化和线程切换成本。
从 .aidl 到可运行接口
逐步解释:
- 编写接口:在
.aidl里声明方法与参数方向。 - 编译生成:
aidl编译器产出接口、Stub、Proxy 三份代码。 - 服务端:实现
Stub的抽象方法,把Stub通过addService()或onBind()暴露。 - 客户端:拿到
IBinder后调用Stub.asInterface(),自动判断本地还是远端。 - 调用:Proxy 组装 Parcel 事务,Stub 在
onTransact()里拆包后回调实现。
asInterface() 为什么能判断本地还是远端
Binder.queryLocalInterface(descriptor) 只在当前对象是本地 Binder 时返回实现,否则返回 null。Stub 的 asInterface() 正是用它:本地返回 this,远端才包一层 Proxy。 同一个服务,同进程调用零拷贝直达实现,跨进程调用才走驱动——这正是 开篇 说“Binder 也用于进程内优化”的依据。
源码证据
阅读目标:确认生成代码的骨架,以及 transact/onTransact 如何成对工作。
正文指针:IInterface.java;生成代码位于构建输出目录(例如 out/soong/.intermediates/...),源码树内对应编译器在 system/tools/aidl。
// 生成代码骨架(伪代码,行号以实际生成产物为准)
public interface IMyService extends IInterface {
public static abstract class Stub extends Binder implements IMyService {
public static IMyService asInterface(IBinder obj) {
IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (iin instanceof IMyService) return (IMyService) iin;
return new Proxy(obj);
}
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
// 按 code 分发并读取参数
}
}
private static class Proxy implements IMyService {
private final IBinder mRemote;
// 每个方法:写参数 → transact(code, data, reply) → 读返回值
}
}这段代码证明: “接口 → Stub → Proxy”是生成产物的固定结构;跨进程调用只有 transact() 这一条路,方法编号 code 由编译器固定分配,顺序不能改动(改动即破坏旧版本 兼容)。
验证与排障
环境:任意开发设备即可。
adb shell service list | grep <服务名>
adb logcat -v threadtime | grep -E "AIDL|TransactionTooLarge|DeadObject"常见问题:接口方法签名或 code 顺序修改后,新客户端调用旧服务端会分发错误;参数方向 漏写 out 时回传值丢失。先在服务端 onTransact 打日志确认 code 与参数,再查客户端 是否用的同一份接口版本。
常见误区
- “AIDL 是运行时框架”:它是编译期代码生成器,运行的是生成的普通代码。
- “同进程调用也走 Binder 驱动”:
asInterface()本地判断后直接调用,不进驱动。 - “
inout和in没区别”:inout双向读写,跨进程数据量最大,能用in就不要用inout。 - “改方法顺序没关系”:code 是编译期固定的,顺序变更会破坏新旧版本互调。
延伸问题
- AIDL 的
@nullable、@Union等注解对生成代码有什么影响? - Kotlin AIDL 与 Java AIDL 生成代码的差异在哪里?
- NDK 侧 AIDL(C++ 接口)与 Java 侧如何互通?
- 为什么频繁的跨进程 AIDL 调用要合并接口或批量请求?
源码入口
| 文件 | 关键位置 | 作用 |
|---|---|---|
IInterface.java | asBinder() | 接口基类 |
Binder.java | queryLocalInterface() | 本地/远端判断 |
| 生成代码 | Stub.onTransact() / Proxy.transact() | 事务分发与发起 |
system/tools/aidl/ | 编译器 | 生成 Java/Kotlin/C++ 胶水代码 |
公共路径:Java 侧 frameworks/base/core/java/android/os/;AIDL 编译器在 system/tools/aidl/;生成产物按构建输出目录检索。行号以 r75 检索为准。
生成代码运行在哪些线程,见 Binder 线程模型。