AIDL 基础与接口定义:从语法到跨进程通信接口
说到 Binder 通信,AIDL(Android Interface Definition Language)是绕不开的第一道门槛。我刚开始接触 AOSP 时,总觉得 AIDL 这层有点多余 —— 直接写 Binder 不就行了?后来踩过坑才明白,AIDL 就像给跨进程通信加了一层「语法糖」,让复杂的事情变得规整。今天咱们就来聊聊怎么用 AIDL 定义一个可靠的跨进程通信接口。
说白了,AIDL 就是一套描述文件。你用 .aidl 文件声明接口和方法,编译系统会自动生成对应的 Binder 骨架代码。你想想看,如果没有这层抽象,你得手写 Stub、Proxy、Parcel 序列化…… 嗯,那画面太美我不敢看。
1. AIDL 文件的关键语法
先看一个最简的 AIDL 接口长什么样。假设我们做音乐播放器,需要让远程客户端控制播放进度:
// IMusicPlayer.aidl
package com.example.music.player;
interface IMusicPlayer {
void play(String songPath);
void stop();
int getCurrentPosition();
String getCurrentSong();
}
这里面有几个要点:
- 包名必须一致:AIDL 文件的 package 要与生成代码的包名严格对应。我见过有人因为包名拼错,编译半天找不到生成的 Java 类。
- 接口名以 I 开头:虽然语法不强制,但这是 AOSP 内部的约定,从 IActivityManager 到 IMediaPlayer 都这么写,咱们跟着走准没错。
- 方法签名:只支持基本类型、String、CharSequence、List、Map 以及实现了 Parcelable 的类型。参数默认方向是 in,如果需要 out 或 inout 得显式声明。
2. 自定义数据类型的传递
如果接口需要传复杂对象(比如音乐信息 MusicInfo),就得让它实现 Parcelable 接口。AIDL 中原生类型 + Parcelable 的组合,基本覆盖了 90% 的场景。来看例子:
// MusicInfo.aidl
package com.example.music.player;
parcelable MusicInfo {
int id;
String title;
String artist;
long duration;
int coverResId;
}
对应的 Java 实现需要写两个部分:一是实现 Parcelable 接口,二是创建一个同名的 AIDL 文件只包含 parcelable 声明。注意,Parcelable 的读写顺序必须和 AIDL 中字段顺序完全一致。我曾经因为调换了两个 int 的读写位置,结果跨进程传过来的数据变成了乱码 —— 那个 bug 查了我一整天。
3. 定向标签:in / out / inout 的坑
AIDL 中方法的参数需要指定方向,用 in、out 或 inout 修饰。不写的话默认是 in。我来解释下区别:
| 标签 | 行为 | 适用场景 | 性能影响 |
|---|---|---|---|
in | 从客户端拷贝到服务端,服务端修改不影响客户端 | 传查询条件、配置参数 | 小 |
out | 从服务端拷贝到客户端,客户端传递的对象内容被忽略 | 获取计算结果、返回数据 | 中等 |
inout | 双向拷贝,客户端对象传给服务端,服务端修改后传回 | 需要修改客户端对象的情况 | 大(两次序列化) |
这里有个坑:用 out 时,客户端传入的对象即使有初始值也没用,服务端会新建一个空对象覆盖。我在早期写音乐播放器时,想把一个预填充的 MusicInfo 传进去,结果服务端拿到的是空对象,播放列表死活不对。后来才发现是方向标签写成了 out。
4. 服务端实现与客户端调用
AIDL 文件写完后,编译会生成对应的 .java 文件,里面包含一个 Stub 抽象类和一个 Proxy 类。服务端继承 Stub,客户端通过 Proxy 调用。典型实现如下:
// 服务端 - 继承 Stub 并注册到 Service
public class MusicPlayerService extends Service {
private final IMusicPlayer.Stub binder = new IMusicPlayer.Stub() {
@Override
public void play(String songPath) throws RemoteException {
// 实际播放逻辑
}
@Override
public void stop() throws RemoteException { /* ... */ }
// 其他方法
};
@Override
public IBinder onBind(Intent intent) {
return binder;
}
}
// 客户端 - 绑定服务并获取代理
private IMusicPlayer playerService;
private ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
playerService = IMusicPlayer.Stub.asInterface(service);
try {
playerService.play("/sdcard/1.mp3");
} catch (RemoteException e) {
e.printStackTrace();
}
}
// ...
};
注意:onServiceConnected 中获取的 IBinder 必须通过 Stub.asInterface() 转换,不能直接强转。我记得有次急着赶版本,直接 (IMusicPlayer) service 就用了,运行直接 ClassCastException —— 因为 IBinder 和 AIDL 接口是两条继承路径。
5. AIDL 接口定义的完整流程
总结一下从零到一定义跨进程通信接口的步骤:
- 定义 AIDL 文件 – 放在 build/src/aidl 目录下,包名与项目保持一致。
- 声明 Parcelable 类型 – 如果需要传复杂对象,额外编写 parcelable 的 .aidl 文件。
- 编译生成 Java 代码 – Gradle 或 Soong 会自动生成 Stub 和 Proxy 类。
- 实现 Stub 抽象类 – 在 Service 或 Server 端实现具体逻辑。
- 注册 Service – 在 AndroidManifest 中声明 Service,并提供
android:process或使用 Binder 连接池。 - 客户端绑定 – 通过 bindService 获取 IBinder,转为接口代理进行调用。
这一套流程走下来,其实就是把跨进程调用包装成了本地调用。但你想想看,背后 Binder 驱动做了多少事?消息排队、权限校验、内存拷贝…… 嗯,这些我们后面章节会逐步揭开。