4. AIDL入门
AIDL(Android Interface Definition Language)是 Android 提供的接口描述语言,用于在客户端与服务端之间约定可跨进程调用的方法签名。它的本质不是“写代码”,而是“写契约”:开发者只描述接口长什么样,构建系统据此生成 Binder 代理与桩代码,把复杂的 Parcel 序列化、transact 调用和线程调度隐藏起来。理解 AIDL,关键要抓住三件事:接口如何定义、数据如何传递、方向标记如何影响内存与性能。
一、核心原理:从接口到 Binder 代理
AIDL 文件通常放在 src/main/aidl/包名/ 目录下,扩展名为 .aidl。编译时,Android Gradle 插件会调用 aidl 工具,为每个接口生成一个 Java 文件,其中包含 Stub(服务端桩)和 Proxy(客户端代理)。客户端拿到的其实是 Proxy,它把方法调用打包成 Parcel,通过 transact() 发送给 Binder 驱动;服务端的 Stub.onTransact() 解包后调用真实实现,再把结果写回。
二、接口定义与数据类型
AIDL 支持的数据类型分为四类:Java 基本类型、String/CharSequence、List/Map、以及实现了 Parcelable 的自定义类型。除基本类型外,其他类型都需要用 in、out 或 inout 标记方向。下面是一个典型的图书管理接口:
// IBookManager.aidl
package com.example.ipc;
import com.example.ipc.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
int getBookCount();
}
自定义的 Book 必须实现 Parcelable,并单独建立同名 Book.aidl 声明:
// Book.aidl
package com.example.ipc;
parcelable Book;
如果接口中使用了自定义 Parcelable,即使同包也必须显式 import,否则生成代码会找不到符号。这是初学者最常见的编译错误之一。
三、方向标记:in、out、inout 的语义
方向标记描述的是“数据相对于客户端如何流动”,而不是简单的参数修饰符。它直接决定 Parcel 的读写次数和对象拷贝行为,是 AIDL 性能与正确性的关键。
| 标记 | 数据流向 | 典型场景 | 性能影响 |
|---|---|---|---|
| in | 客户端 → 服务端 | 传入参数,服务端只读 | 一次写入、一次读取 |
| out | 服务端 → 客户端 | 服务端填充结果返回 | 客户端先创建空对象,服务端写入 |
| inout | 双向 | 既传入又需要被修改 | 两次读写,开销最大 |
四、自动生成代码解析
生成的 IBookManager.java 结构固定,理解它有助于排错。核心片段如下:
public interface IBookManager extends android.os.IInterface {
public static abstract class Stub extends Binder implements IBookManager {
private static final String DESCRIPTOR = "com.example.ipc.IBookManager";
static final int TRANSACTION_getBookList = 1;
static final int TRANSACTION_addBook = 2;
static final int TRANSACTION_getBookCount = 3;
@Override
public boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
switch (code) {
case TRANSACTION_getBookList: {
data.enforceInterface(DESCRIPTOR);
List<Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
// ...
}
return super.onTransact(code, data, reply, flags);
}
}
}
每个方法被分配一个整型 transaction code,客户端 Proxy 按 code 调用 transact(),服务端 onTransact() 按 code 分发。方法参数按顺序写入 Parcel,返回值写入 reply。若服务端抛异常,会通过 writeException() 回传,客户端 readException() 重新抛出。
五、跨进程调用实战流程
完整链路可以概括为:定义 AIDL → 实现 Service → 客户端绑定 → 获取代理 → 调用方法。服务端在 onBind() 中返回 Stub 实例:
public class BookService extends Service {
private final IBookManager.Stub binder = new IBookManager.Stub() {
private final List<Book> books = new ArrayList<>();
@Override
public List<Book> getBookList() { return books; }
@Override
public void addBook(Book book) { books.add(book); }
@Override
public int getBookCount() { return books.size(); }
};
@Override
public IBinder onBind(Intent intent) { return binder; }
}
客户端通过 ServiceConnection 拿到 IBookManager.Stub.asInterface(service),即可像调用本地方法一样调用远程接口。需要注意:远程调用默认运行在 Binder 线程池,不是主线程,耗时操作应放在子线程,否则可能触发 ANR。
六、常见误区与复盘
- 忘记 import 自定义 Parcelable:即使同包也要写 import,否则生成代码报“找不到符号”。
- 把耗时操作放在主线程调用:远程调用是同步阻塞的,主线程调用可能 ANR,应使用子线程或异步接口。
- 误用 inout:只读参数写成 inout 会多一次序列化,性能下降且语义不清。
- 忽略线程安全:Stub 方法运行在 Binder 线程池,多个客户端并发访问时,共享集合必须做同步保护。
- 忘记处理 RemoteException:服务端进程崩溃时客户端会收到 DeadObjectException,应捕获并重连。
adb shell dumpsys activity services 包名 确认 Service 是否绑定成功;再在 Stub 方法中打日志,确认调用是否真正到达服务端。若客户端拿到 null,优先检查 onBind() 是否返回了 Stub 实例。AIDL 是 Android IPC 的“标准契约工具”。掌握接口定义、数据类型、方向标记和生成代码结构后,就能把跨进程调用写得像本地调用一样自然。真正决定工程质量的是对 Binder 线程模型、异常处理和 Parcelable 序列化的理解,而不仅仅是会写一个 .aidl 文件。