41. AIDL 原理:AIDL 工具如何生成 Binder 通信的 Stub 与 Proxy 代码?
好,我们直接切入正题。AIDL 这玩意儿,说白了就是 Android 帮你写 Binder 代码的“代码生成器”。你写个 .aidl 接口文件,它就能给你生成一套完整的 C/S 通信骨架。很多新手觉得 AIDL 很神秘,其实它就是个模板引擎,干的是“翻译”的活。
我个人习惯把 AIDL 生成的代码分成三块来看:接口、Stub、Proxy。理解了这三块,你就看透了 AIDL 的本质。
1. 从 .aidl 到 .java:编译器的“翻译”过程
假设你写了一个简单的 AIDL 接口:
// IMyService.aidl
package com.example;
interface IMyService {
int add(int a, int b);
String getMessage();
}
当你点击 Build 或者执行 Gradle 编译时,Android SDK 中的 aidl.exe 工具就会登场。它会解析这个文件,生成一个同名的 Java 接口文件 IMyService.java。
这个生成的 Java 文件,结构大致如下:
public interface IMyService extends android.os.IInterface {
// 从 AIDL 方法映射过来的抽象方法
public int add(int a, int b) throws android.os.RemoteException;
public String getMessage() throws android.os.RemoteException;
// 内部静态抽象类 Stub
public static abstract class Stub extends android.os.Binder implements IMyService {
// ... 核心实现
}
// 内部代理类 Proxy
private static class Proxy implements IMyService {
// ... 核心实现
}
}
嗯,这里要注意:生成的接口继承了 IInterface,这是所有 Binder 接口的根接口。Stub 和 Proxy 都实现了这个接口。
2. Stub 类:服务端的“骨架”
Stub 是服务端代码的核心。它继承了 android.os.Binder,并实现了你的 AIDL 接口。说白了,它就是一个 Binder 本地对象。
生成的 Stub 类里,最关键的是 onTransact() 方法。这个方法就是 Binder 驱动回调服务端的地方。我们来看看它长什么样:
@Override
public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
String descriptor = DESCRIPTOR;
switch (code) {
case TRANSACTION_add: {
data.enforceInterface(descriptor);
int _arg0;
_arg0 = data.readInt();
int _arg1;
_arg1 = data.readInt();
int _result = this.add(_arg0, _arg1);
reply.writeNoException();
reply.writeInt(_result);
return true;
}
case TRANSACTION_getMessage: {
data.enforceInterface(descriptor);
String _result = this.getMessage();
reply.writeNoException();
reply.writeString(_result);
return true;
}
default: {
return super.onTransact(code, data, reply, flags);
}
}
}
这段代码的逻辑非常清晰:
- 反序列化:从
dataParcel 中读取参数(比如readInt())。 - 调用本地方法:调用你在 Stub 子类中实现的
add()或getMessage()。 - 序列化结果:将返回值写入
replyParcel。
我在项目中遇到过一个问题:有人直接在 onTransact 里做耗时操作,导致 UI 线程卡死。记住,onTransact 默认运行在 Binder 线程池中,但如果你在服务端主线程调用了 Binder 方法,那它还是跑在主线程上。这个坑我曾经踩过,后来养成了习惯:服务端方法里一律开线程或使用 Handler。
3. Proxy 类:客户端的“代理”
客户端不能直接调用服务端的方法,它只能通过 Binder 驱动发送数据。Proxy 就是干这个的。它持有一个 IBinder 引用(即 mRemote),所有调用都通过它来转发。
生成的 Proxy 代码大概是这样:
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;
}
@Override
public String getMessage() throws android.os.RemoteException {
// 类似实现,略
}
}
你看,Proxy 做的事情正好和 Stub 相反:
- 序列化参数:把方法参数写入
_dataParcel。 - 发起 transact 调用:调用
mRemote.transact(),把数据包发给 Binder 驱动。 - 反序列化结果:从
_replyParcel 中读取返回值。
这里有个细节:transact 的最后一个参数 flags 传的是 0,表示这是一个同步调用。如果你传 FLAG_ONEWAY,那就是异步调用,客户端不会等待服务端返回。我建议你在不需要返回值时使用 oneway,能显著提升性能。
4. 核心逻辑流程图
为了让你更直观地理解整个流程,我画了一张图。它展示了从客户端调用到服务端响应的完整数据流。
5. 关键设计要点
AIDL 工具生成的代码里,有几个设计模式值得注意:
| 设计模式 | 体现位置 | 说明 |
|---|---|---|
| 代理模式 | Proxy 类 | 客户端通过 Proxy 间接访问服务端,Proxy 封装了跨进程通信的细节。 |
| 模板方法模式 | Stub 类 | onTransact() 定义了通信骨架,子类只需实现业务方法。 |
| 静态工厂方法 | Stub.asInterface() |
根据是否跨进程,返回 Proxy 或本地对象。这是 Binder 的经典用法。 |
核心要点: AIDL 生成的代码本质上是一个“序列化/反序列化”的适配层。它把 Java 方法调用转换成 Parcel 数据,再通过 Binder 驱动传输。你写的业务逻辑,被包裹在 Stub 和 Proxy 之间,完全感知不到跨进程的存在。
个人技巧: 我习惯在生成代码后,打开 IMyService.java 看一眼。重点关注 TRANSACTION_ 常量的值,它们是方法编号。如果 AIDL 接口方法很多,这些编号会从 1 开始递增。我曾经在修改 AIDL 时不小心删了一个方法,导致后面的编号全部错位,客户端和服务端版本不一致时直接崩溃。所以,永远不要手动修改生成的代码,也尽量不要在已发布的 AIDL 接口中间插入新方法。
避坑指南: 我曾经遇到过一个线上问题:客户端调用服务端方法时,偶尔会抛出 TransactionTooLargeException。查了半天发现,是 AIDL 方法里传了一个很大的 Bitmap。Binder 事务缓冲区默认只有 1MB,超过就会抛异常。解决方案是改用文件描述符或者分片传输。记住,Binder 不是用来传大数据的。
最后总结一下:AIDL 工具生成的 Stub 和 Proxy,就是 Binder 通信的“翻译官”。Stub 在服务端负责“收包并执行”,Proxy 在客户端负责“打包并发送”。你只需要关注业务逻辑,剩下的脏活累活,AIDL 都帮你干了。
公众号:蓝海资料掘金营,微信deep3321