11、AIDL 编译原理:AIDL 文件如何生成 Java/C++ 存根(Stub)与代理(Proxy)代码
好,咱们今天聊点硬核的。AIDL 文件,说白了就是一张“协议单”。你写个 .aidl 文件,定义好接口和方法,然后编译系统就会自动帮你生成一堆代码。这堆代码里最重要的两个角色,就是 Stub(存根) 和 Proxy(代理)。
我刚开始接触 Binder 时,总觉得这玩意儿很玄乎。后来自己动手翻了一遍生成的代码,才发现——嗯,其实没那么复杂。今天我就带你走一遍这个编译过程,看看 AIDL 到底是怎么变成 Java 和 C++ 代码的。
11.1 AIDL 编译器的“三件套”
AIDL 编译器(aidl 工具)会根据你的 .aidl 文件,生成三个核心文件:
- 接口文件:比如 IMyService.java,里面定义了接口方法。
- Stub 类:一个抽象类,继承自 Binder,实现了接口。服务端用这个。
- Proxy 类:实现了接口,内部持有 IBinder 引用。客户端用这个。
如果你用的是 C++ 端,还会生成对应的 .h 和 .cpp 文件,逻辑完全一样,只是语言不同。
核心思想:Stub 负责“接收请求并调用本地方法”,Proxy 负责“把方法调用打包成数据发出去”。
11.2 从 .aidl 到 Java 代码:一步步拆解
假设你写了这样一个 AIDL 文件:
// IMyService.aidl
package com.example;
interface IMyService {
int add(int a, int b);
String getName();
}
编译后,会生成 IMyService.java。咱们来看看里面长什么样。
11.2.1 接口定义
最外层是一个接口,里面定义了你的方法:
public interface IMyService extends android.os.IInterface {
public int add(int a, int b) throws android.os.RemoteException;
public String getName() throws android.os.RemoteException;
}
注意,所有方法都抛出了 RemoteException。为什么?因为跨进程调用可能失败,比如对方进程挂了。这个异常你必须处理。
11.2.2 Stub 类:服务端的“守门人”
Stub 是一个抽象类,继承自 android.os.Binder,实现了 IMyService 接口。它的核心工作是:
- 把收到的
Parcel数据解包,调用你实现的方法。 - 把返回值打包成
Parcel发回去。
关键代码长这样:
public static abstract class Stub extends android.os.Binder implements IMyService {
private static final java.lang.String DESCRIPTOR = "com.example.IMyService";
public Stub() {
this.attachInterface(this, DESCRIPTOR);
}
@Override
public boolean onTransact(int code, android.os.Parcel data,
android.os.Parcel reply, int flags)
throws android.os.RemoteException {
switch (code) {
case TRANSACTION_add: {
data.enforceInterface(DESCRIPTOR);
int _arg0 = data.readInt();
int _arg1 = data.readInt();
int _result = this.add(_arg0, _arg1);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
case TRANSACTION_getName: {
data.enforceInterface(DESCRIPTOR);
java.lang.String _result = this.getName();
reply.writeNoException();
reply.writeString(_result);
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
}
你看,onTransact 方法里,根据 code 判断是哪个方法调用,然后从 data 里读出参数,调用本地实现,再把结果写回 reply。
我个人习惯:在写服务端时,我会在 Stub 的实现类里加一些日志,方便排查问题。比如每次调用都打印一下方法名和参数。这在调试跨进程问题时特别有用。
11.2.3 Proxy 类:客户端的“传话筒”
Proxy 类实现了 IMyService 接口,内部持有 IBinder 引用。它的工作正好相反:
- 把方法调用打包成
Parcel数据。 - 通过
transact()发送给服务端。 - 从返回的
Parcel里读出结果。
代码示例:
private static class Proxy implements IMyService {
private android.os.IBinder mRemote;
Proxy(android.os.IBinder remote) {
mRemote = remote;
}
@Override
public int add(int a, int b) throws android.os.RemoteException {
android.os.Parcel _data = android.os.Parcel.obtain();
android.os.Parcel _reply = android.os.Parcel.obtain();
int _result;
try {
_data.writeInterfaceToken(DESCRIPTOR);
_data.writeInt(a);
_data.writeInt(b);
mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
_reply.readException();
_result = _reply.readInt();
} finally {
_reply.recycle();
_data.recycle();
}
return _result;
}
}
这里有个细节:writeInterfaceToken 和 enforceInterface 是一对。它们用来验证对方是不是你想要的接口。我曾经遇到过一个问题,客户端和服务端用了不同版本的 AIDL 文件,结果 DESCRIPTOR 不匹配,调用直接失败。嗯,从那以后我每次更新 AIDL 都会重新编译两边。
11.3 C++ 端的 Stub 和 Proxy
如果你做的是 Native 服务(比如 mediaserver),那就要用 C++ 版本。逻辑完全一样,只是语法不同。
生成的 C++ 代码里,Stub 是一个类,实现了 BnInterface(Binder Native Interface),Proxy 实现了 BpInterface(Binder Proxy Interface)。
关键区别在于:
- C++ 端用
Parcel的writeInt32、writeString8等方法。 - 方法调用通过
remote()->transact()完成。 - Stub 的
onTransact里用switch判断方法 ID。
注意:C++ 端的 AIDL 生成代码里,方法 ID 是枚举值,比如 TRANSACTION_add = IBinder::FIRST_CALL_TRANSACTION + 0。如果你手动修改了这些 ID,会导致客户端和服务端对不上号。我曾经踩过这个坑,调试了一整天才发现是 ID 冲突了。
11.4 编译流程全景图
下面这张图展示了从 .aidl 文件到最终代码的完整流程:
11.5 一个容易忽略的细节:DESCRIPTOR
你可能注意到了,生成的代码里有个 DESCRIPTOR 常量,值是包名加接口名。这个玩意儿有什么用?
说白了,它就是接口的唯一标识。Binder 驱动在跨进程传输时,会用这个字符串来验证对方是不是你想要的接口。如果客户端和服务端的 DESCRIPTOR 不一致,enforceInterface 会直接抛异常。
我记得有一次,我改了包名但忘了重新编译 AIDL,结果客户端一直报 SecurityException。查了半天才发现是 DESCRIPTOR 没更新。从那以后,我每次改包名都会 make clean 一下。
11.6 总结
AIDL 编译原理,说白了就是:
- 你写接口定义(.aidl)。
- 编译器帮你生成 Stub(服务端)和 Proxy(客户端)。
- Stub 负责解包 + 调用本地方法。
- Proxy 负责打包 + 发送跨进程请求。
Java 和 C++ 的逻辑完全一样,只是语法不同。理解了这一点,你就能轻松看懂生成的代码,甚至自己手写一个 Binder 服务。
避坑指南:如果你在调试时发现跨进程调用失败,先检查 DESCRIPTOR 是否一致,再检查方法 ID 是否冲突。这两个问题我各踩过一次,每次都是血泪教训。