文章内容
一、面试题:AIDL 生成的 Java 类细节
1.1 题目背景
这道题问的是编译产物:写完 .aidl 文件点一下构建,生成的那个 Java 文件里到底有什么。不少人做过跨进程需求,却从没打开过 build/generated 目录下的那份代码,回答就只能停在「AIDL 用来跨进程」这一层。
1.2 AIDL 是什么
AIDL(Android Interface Definition Language,Android 接口定义语言)是一门只用来描述接口的小语言。你按它的语法声明方法,编译器把这份声明翻译成一整套符合 Binder 规则的 Java 代码。
它的角色像房产中介。手续本身并不神秘,跑一遍也能办完,但流程繁琐且每次都一样,于是交给中介代办。Binder 同理——按它的规则做一次跨进程调用,需要写的模板代码又多又固定,AIDL 把这部分包办了。既然是模板,就完全可以手写;很多讲解为了看清结构,干脆不用 AIDL,直接手敲这三个类。
二、AIDL 生成的三个类

2.1 接口:声明跨进程方法
生成文件的最外层是一个接口,继承自 IInterface(Binder 体系里所有可跨进程接口的父接口,只要求实现 asBinder())。跨进程方法都声明在这里,客户端只依赖它,不关心对面是谁在实现。
这和普通的面向接口编程是同一套思路,区别在于调用会跨越进程边界,所以每个方法都要声明 throws RemoteException。
2.2 Stub:服务端基类
接口的真正实现写在服务类里,比如某个 RemoteService。问题是服务端得把这个实现「交给」客户端,而对象没法跨进程传递,能传的只有 Binder 引用。
Stub(存根类,服务端的桩)解决的就是这件事:它继承 Binder,让服务端具备跨进程被调用的能力;同时实现那个接口,但把业务方法留空——Stub 是抽象类,方法实现由具体服务类补全。这样一来,服务端返回的是一个 Binder 实体,而这个实体知道怎么把请求路由到实现代码上。
2.3 Proxy:客户端代理
客户端拿到的从来不是服务端的真实对象。Proxy(代理类)是一个普通类,实现同一个接口,内部持有一个 IBinder(Binder 通信句柄的抽象,客户端侧通常是驱动给的代理引用)。
客户端调接口方法时,实际执行的是 Proxy 里的同名方法,Proxy 负责打包参数、发起通信、解包结果。代理模式在这里的价值很直接:调用方写的代码和调用本地对象没有区别,跨进程的复杂度全被 Proxy 吞掉了。
三、一次跨进程调用的完整链路

3.1 两个 Parcel:data 与 reply

Proxy 方法的第一件事是取两个 Parcel(Binder 通信的数据容器,负责把参数序列化成可跨进程传输的字节)。data 装客户端发出去的内容,reply 留给服务端写返回值。
像出差打两个包:一个装自己要带过去的东西,一个空箱子留着装特产带回来。两个 Parcel 都从对象池 obtain() 取,用完必须 recycle() 归还。
3.2 transact 与同步/异步
参数写好后调 remote.transact(code, data, reply, flags)——transact 是 IBinder 上发起一次事务的方法,remote 就是 Proxy 持有的那个 Binder 引用。
flags 传 0 表示同步调用,客户端线程在这里挂起,一直等到服务端处理完、reply 被填好才继续往下走;传 FLAG_ONEWAY 则是异步,发出去就返回,拿不到返回值。日常业务大多是同步调用,所以主线程里做跨进程调用会卡 UI,这一点在写代码时要留心。
3.3 onTransact 与方法 ID 分发
请求进入 Binder 驱动,驱动按目标把事务投递到服务端进程,服务端的 Binder 线程被唤醒,进入 onTransact()——Stub 侧接收事务的统一入口。
服务端怎么知道客户端要调哪个方法?靠方法 ID。编译器把接口里的方法按声明顺序编号,从 IBinder.FIRST_CALL_TRANSACTION(值为 1)开始递增,生成 TRANSACTION_xxx 常量。这和布局文件里的控件编译后拿到一串 ID 是一个套路:名字给人看,编号给机器用。客户端 transact 时带上编号,onTransact 里一个 switch 按编号分发到对应分支。
3.4 返回值与 reply 打包
分发到具体分支后,data 里的参数被读出来,接着调用本类的接口方法。由于 Stub 是抽象的,这一调就落到了真正的服务实现类上,业务逻辑在这里执行。
有返回值的方法把结果写进 reply,驱动带回客户端,Proxy 那边从 reply 里读出来交给调用方;返回 void 的方法就不写。异常也走这条路:服务端 writeNoException() 或写入异常信息,客户端 readException() 时在本地重新抛出。
3.5 asInterface:怎么拿到 Binder 和 Proxy

客户端绑定服务后,onServiceConnected 回调里给的是一个 IBinder,需要用 Xxx.Stub.asInterface(binder) 转成接口类型。
这个静态方法里藏着一个容易被忽略的判断:它先用 queryLocalInterface(DESCRIPTOR) 查一下这个 Binder 是不是本进程的实体。如果服务和客户端在同一个进程,查得到,直接返回 Stub 本身,方法调用就是普通的 Java 调用,一次驱动都不用进;查不到才说明是跨进程,包装成 Proxy 返回。面试里能把这层判断讲出来,比只背「asInterface 返回代理」要值钱。
四、代码验证
4.1 三个类的最小骨架
把生成代码里的异常处理和边界分支去掉,剩下的骨架就是下面这些。看它是为了确认三个类的从属关系:Stub 和 Proxy 都是接口的内部类,一个在服务端一个在客户端。
// 示意代码:AIDL 生成(或手写)的三件套,仅保留结构public interface IPerson extends IInterface { // 跨进程接口,继承 IInterface 才被 Binder 体系认可 String DESCRIPTOR = "com.demo.IPerson"; // 接口标识,两端用它确认在聊同一个接口 int TRANSACTION_getName = IBinder.FIRST_CALL_TRANSACTION; // 方法 ID,第一个方法编号为 1 String getName() throws RemoteException; // 声明跨进程方法,跨进程调用都可能失败 abstract class Stub extends Binder implements IPerson { // 服务端基类:继承 Binder 才具备被跨进程调用的能力 public Stub() { // 构造时做一次绑定 attachInterface(this, DESCRIPTOR); // 把自己与描述符关联,供本地查询使用 } // 构造结束 @Override public IBinder asBinder() { return this; } // 服务端自身就是 Binder 实体 public static IPerson asInterface(IBinder binder) { // 把客户端拿到的 IBinder 转成接口 if (binder == null) return null; // 服务尚未连接时没有可用对象 IInterface local = binder.queryLocalInterface(DESCRIPTOR); // 查询是否为本进程内的实现 if (local instanceof IPerson) return (IPerson) local; // 同进程:直接返回 Stub,不走 Binder 通信 return new Proxy(binder); // 跨进程:包装成代理返回 } // asInterface 结束 } // Stub 结束,getName() 故意不实现,留给服务类 class Proxy implements IPerson { // 客户端代理:实现同一接口,调用方无感知 private final IBinder remote; // 持有驱动给的 Binder 引用,通信出口 Proxy(IBinder remote) { this.remote = remote; } // 由 asInterface 注入 @Override public IBinder asBinder() { return remote; } // 需要 IBinder 时交出持有的引用 @Override public String getName() { return null; } // 打包与发送逻辑见下一段 } // Proxy 结束} // 接口结束三个类各自的位置到这里就清楚了:接口定契约,Stub 站在服务端等调用,Proxy 站在客户端发调用。
4.2 一次调用在两端的走向
上面留空的 getName() 补全后,就是一次完整的跨进程调用。这段代码把「客户端打包」和「服务端分发」两头对起来看,方法 ID 的作用一眼就明白。
// 示意代码:一次调用的两端实现// ===== 客户端:Proxy 中的方法 =====@Override public String getName() throws RemoteException { // 客户端调接口方法,实际落到这里 Parcel data = Parcel.obtain(); // 取一个 Parcel 装发出去的参数 Parcel reply = Parcel.obtain(); // 再取一个准备接收返回值 String result; // 保存最终结果 try { // 用 try/finally 确保 Parcel 被归还 data.writeInterfaceToken(DESCRIPTOR); // 写入接口标识,服务端会校验 remote.transact(TRANSACTION_getName, data, reply, 0); // 发起事务:方法 ID + 入参 + 出参,0 表示同步 reply.readException(); // 服务端若抛了异常,在客户端重新抛出 result = reply.readString(); // 读出服务端写回的返回值 } finally { // 无论成功失败都要收尾 reply.recycle(); // 归还 reply data.recycle(); // 归还 data } // 收尾结束 return result; // 交给调用方,用起来和本地方法一样} // 方法结束 // ===== 服务端:Stub 中的分发入口 =====@Override protected boolean onTransact(int code, Parcel data, // 驱动唤醒服务端线程后进入这里 Parcel reply, int flags) throws RemoteException { // code 即方法 ID switch (code) { // 按客户端带来的编号分发 case TRANSACTION_getName: // 命中 getName 的编号 data.enforceInterface(DESCRIPTOR); // 校验接口标识,不一致直接失败 String name = this.getName(); // 调到具体服务类里的真正实现 reply.writeNoException(); // 写入「无异常」标记 reply.writeString(name); // 把返回值写进 reply 带回客户端 return true; // 事务已处理 } // switch 结束 return super.onTransact(code, data, reply, flags); // 其余编号交给父类处理} // 方法结束客户端和服务端靠两样东西对齐:DESCRIPTOR 保证接口一致,TRANSACTION_getName 保证方法一致。data 单向送过去,reply 单向带回来,中间那段由 Binder 驱动完成。
五、总结
5.1 三个类各司其职
接口负责声明方法并定义描述符与方法编号;Stub 继承 Binder,作为服务端的接收端,把 onTransact 收到的编号分发到实现类;Proxy 实现同一接口,替客户端把方法调用翻译成一次 Binder 事务。
5.2 一次调用的完整路径
客户端调接口方法 → Proxy 取 data/reply 两个 Parcel 并写入参数 → remote.transact 带上方法 ID 发起事务(同步调用时线程挂起)→ 驱动投递到服务端 → onTransact 按方法 ID switch 分发 → 服务类执行真正的逻辑 → 返回值写进 reply → 客户端读出结果返回。
想确认这套结构不用背,写一个 .aidl 文件构建一次,去 app/build/generated/aidl_source_output_dir/ 下打开生成的 Java 文件,对照本文的骨架逐段看。再在服务端 onTransact 的 switch 上打个断点,从客户端发一次调用,就能看清 code 的取值和它落到哪个分支。
本文由 AgentPlanFlow 生成