3. AIDL语言入门

AIDL(Android Interface Definition Language)是 Android 系统中用于定义跨进程通信接口的 IDL 语言。它的核心价值在于:让客户端与服务端在编译期就拥有一份“契约”,由构建工具自动生成 Binder 通信所需的桩代码与代理代码,开发者只需关注接口语义本身。本节从接口定义、数据类型、方向标记、包名规则、编译产物和基本语法六个方面,建立 AIDL 的完整入门认知。

一、核心原理:AIDL 解决什么问题

Android 的 Binder 机制在底层只传输字节流,客户端调用服务端方法时,参数需要被序列化写入 Parcel,服务端再从 Parcel 中反序列化读取。如果每个方法都手写这套读写逻辑,既繁琐又极易出错。AIDL 的作用就是把“接口声明”翻译成“Binder 通信代码”:

  • 客户端侧:生成 Stub.Proxy 代理类,把方法调用转换为 Parcel 写入、transact() 调用和结果读取。
  • 服务端侧:生成 Stub 抽象类,在 onTransact() 中根据方法编号读取参数、调用真实实现、写回结果。
  • 双方共享:方法编号(TRANSACTION_xxx)、接口描述符(DESCRIPTOR)和参数顺序,保证两端一致。
AIDL 不是运行时反射框架,而是编译期代码生成工具。接口一旦发布,方法签名、参数顺序、方向标记就构成跨进程契约,随意变更会导致 RemoteException 或数据错乱。

二、接口定义与基本语法

AIDL 文件以 .aidl 为后缀,语法接近 Java 接口,但更严格。一个典型接口定义如下:

// IUserService.aidl
package com.example.demo;

import com.example.demo.UserInfo;

interface IUserService {
    UserInfo getUser(int id);
    void setUser(in UserInfo user);
    List<UserInfo> listUsers();
    int add(int a, int b);
}

基本语法要点:

  • 文件顶部必须声明 package,且与文件所在目录路径一致。
  • 使用 import 引入其他包中的自定义类型,同包类型可省略。
  • 接口名与文件名一致,方法不能有 public、static、final 等修饰符。
  • 方法可以有返回值,也可以返回 void;不支持重载方法名相同但参数不同的情况。
  • 不支持泛型方法、异常声明、字段和常量(常量可通过 const 定义在接口内)。

三、数据类型与方向标记

AIDL 支持的数据类型分为三类,方向标记则决定参数在跨进程传输时的流向。

类型类别 具体类型 是否需要 import 方向标记
基本类型 int、long、boolean、float、double、char、byte 否 默认 in,不可写 out/inout
String / CharSequence String、CharSequence 否 默认 in,可写 out/inout
集合类型 List、Map 否 默认 in,元素必须是 AIDL 支持类型
自定义 Parcelable 实现 Parcelable 的类 是 必须显式标注 in/out/inout
其他 AIDL 接口 另一个 .aidl 接口 是 必须显式标注 in/out/inout

方向标记的含义:

  • in:数据从客户端流向服务端,服务端修改不影响客户端。
  • out:数据从服务端流回客户端,客户端传入的初始值在服务端为空。
  • inout:双向流动,服务端可读取并修改后回传,开销最大。
自定义 Parcelable 和 AIDL 接口参数如果不写方向标记,编译会直接报错。基本类型和 String 默认 in,但显式写出可读性更好。

四、包名规则与目录结构

AIDL 的包名规则与 Java 包名一致,但有一个硬性约束:package 声明必须与文件在源码集中的相对路径完全匹配。例如:

src/main/aidl/com/example/demo/IUserService.aidl
对应 package com.example.demo;

如果路径与包名不一致,AIDL 编译器会报 couldn't find import for class 或 package ... does not match directory。在 Android Gradle 项目中,AIDL 文件默认放在 src/main/aidl/ 下;使用 sourceSets 可以自定义目录,但包名与路径的对应关系不变。

五、编译产物与调用链

AIDL 文件在构建时由 aidl 工具编译为 Java 文件,最终参与 APK 打包。理解编译产物是排查跨进程问题的关键。下图展示了从 AIDL 源文件到客户端调用、服务端响应的完整链路:

AIDL 编译产物与跨进程调用链 IUserService.aidl 接口定义 + 方向标记 aidl 编译器 构建期代码生成 IUserService.java Stub / Stub.Proxy DESCRIPTOR / TRANSACTION 客户端进程 IUserService.Stub.asInterface() Proxy.getUser(1) Parcel 写入 → transact() Binder 驱动 跨进程数据传递 服务端进程 Stub.onTransact() 读取 Parcel → 调用实现 写回结果 → reply 返回结果沿原路回传:reply Parcel → Proxy 读取 → 客户端获得返回值 关键产物:DESCRIPTOR 标识接口、TRANSACTION_xxx 标识方法、Proxy 负责客户端序列化、Stub 负责服务端反序列化

编译产物通常位于 build/generated/aidl_source_output_dir/ 下,核心结构包括:

  • DESCRIPTOR:接口唯一标识字符串,通常为包名 + 接口名。
  • TRANSACTION_xxx:每个方法对应一个整型编号,onTransact() 据此分发。
  • Stub.Proxy:客户端代理,实现接口方法并完成 Parcel 读写。
  • Stub:服务端抽象类,继承 Binder 并实现 onTransact()。
  • asInterface():把 IBinder 转换为接口,同进程直接返回本地对象,跨进程返回 Proxy。

六、案例与常见误区

假设我们要实现一个用户查询服务,接口定义如下:

// IUserService.aidl
package com.example.demo;

import com.example.demo.UserInfo;

interface IUserService {
    UserInfo getUser(int id);
    void updateUser(in UserInfo user);
    void fillUser(out UserInfo user);
    List<UserInfo> queryUsers(in String keyword);
}

服务端实现时继承 IUserService.Stub,客户端通过 IUserService.Stub.asInterface(binder) 获取接口。常见误区包括:

  1. 包名与路径不一致:AIDL 文件放在 aidl/com/example/ 下,但 package 写成 com.demo,编译报错。
  2. 自定义类型未实现 Parcelable:AIDL 无法识别普通 Java 类,必须实现 Parcelable 并提供 CREATOR。
  3. 方向标记缺失:自定义 Parcelable 参数不写 in/out/inout,编译直接失败。
  4. 方法重载:AIDL 不支持同名方法重载,需要改用不同方法名。
  5. 修改接口不兼容:已发布接口增加方法、调整参数顺序或改变方向标记,会导致旧客户端调用失败。
入门阶段建议先用基本类型和 String 跑通一个最小 AIDL 服务,再逐步引入 Parcelable 和 List。每改一次接口,都重新检查包名、import、方向标记和编译产物中的 TRANSACTION 编号是否一致。