2. AIDL 接口设计基础:语法、方向、类型与文件组织

AIDL(Android Interface Definition Language)是 Binder 跨进程通信的接口契约语言。本章从语法结构、参数方向、类型约束和文件组织四个维度,建立 AIDL 接口设计的完整知识基线。

2.1 AIDL 语法结构

一个标准的 AIDL 接口文件由包声明、导入、接口声明和方法签名四部分组成。以下是一个导航回调场景中的典型示例:

// NavigationCallback.aidl
package com.example.navigation;

import com.example.navigation.RouteInfo;

interface INavigationCallback {
    void onRouteUpdated(in RouteInfo route);
    void onNavigationError(String message, int errorCode);
    void onArrival();
}

核心语法规则:

  • 包声明必须与宿主应用的包名一致,生成路径由 AIDL 编译器自动映射。
  • 导入仅对非基本类型、非 String/CharSequence 的 parcelable 类型需要显式 import。
  • 接口名建议以大写 I 开头,增强可读性。
  • 方法签名支持 0 个或多个参数,支持返回值(void 或基本类型 / parcelable)。
命名规范提示:回调接口推荐命名为 I<业务名>Callback,如 INavigationCallback、IPositionListener,便于在 AIDL 目录中快速识别回调型接口。

2.2 参数方向:in / out / inout

方向标记是 AIDL 区别于普通 Java 接口的关键特征。它决定了 Binder 驱动在跨进程传输时数据拷贝的方式和序列化方向。三个方向的语义对比如下:

方向 数据流向 序列化行为 典型场景
in 调用方 → 服务方 调用时将数据完整写入 Parcel;服务方可读不可写回。 回调事件上报、数据查询请求
out 服务方 → 调用方 调用时写入空占位;服务方填充后写回。 服务端填充结果、获取配置
inout 双向传输 调用时写入完整数据;服务方修改后全量写回。 协商更新、状态双向同步
性能警告:inout 在传输大对象时会产生两次序列化开销。在回调场景中,非必要不使用 inout。对于只读的回调事件,始终使用 in 方向。
选择原则:如果不确定方向,优先使用 in。只有确定服务方需要修改对象并将修改返回给调用方时,才考虑 out 或 inout。在导航回调中,所有回调参数均使用 in 方向。
AIDL 参数方向数据流示意图 AIDL 参数方向数据流示意图 in 调用方 (Client) 服务方 (Server) 单向写入 不可读回 out 调用方 (Client) 服务方 (Server) 单向写回 初始空占位 inout 调用方 (Client) 服务方 (Server) 双向全量 两次序列化 回调场景推荐全部使用 in 方向,避免不必要的数据拷贝和序列化开销

2.3 参数类型限制

AIDL 支持的数据类型由严格的划分,不可直接使用任意 Java 对象。以下是完整的类型系统清单:

类别 支持类型 备注
基本类型 boolean, byte, char, short, int, long, float, double 所有基本类型均支持,无装箱要求
字符串 String, CharSequence 默认支持,无需额外导入
列表 List<T> (T 为支持类型) 接收端总是 ArrayList
映射 Map<K,V> (K,V 为支持类型) 接收端总是 HashMap
自定义 Parcelable 实现 Parcelable 接口的类 需在 AIDL 文件中使用 parcelable 声明
AIDL 接口 其他 AIDL 接口类型 用于传递 Binder 代理对象
重要限制:不支持泛型通配符(如 List<?>)、不支持枚举、不支持 Serializable、不支持 非基本类型数组(基本类型数组如 int[] 是支持的)。导航回调中常用的 RouteInfo 必须实现 Parcelable 并在 AIDL 中声明。
// RouteInfo.aidl 中的 parcelable 声明
// 方式一:在独立 .aidl 文件中声明
// RouteInfo.aidl (文件内容仅一行)
package com.example.navigation;
parcelable RouteInfo;

// 方式二:在同一接口文件中声明(适用于小型项目)
// INavigationCallback.aidl 顶部追加
package com.example.navigation;
parcelable RouteInfo;

2.4 接口文件组织规范

AIDL 文件的目录结构直接影响编译产物和跨模块引用。推荐的企业级组织方式如下:

app/src/main/aidl/
└── com/
    └── example/
        └── navigation/
            ├── INavigationCallback.aidl      # 回调接口(供远程端调用)
            ├── INavigationService.aidl       # 服务接口(供客户端调用)
            ├── RouteInfo.aidl                # 数据实体 parcelable 声明
            └── NavigationConstants.aidl      # 常量接口(可选)

文件组织三条硬规则:

  • 规则1:包路径必须与 java 源文件中的包声明完全一致,否则编译器会报 "can't resolve" 错误。
  • 规则2:每个自定义 Parcelable 类必须对应一个同名的 .aidl 声明文件,或者集中在同一个 .aidl 文件中用多个 parcelable 关键词声明。
  • 规则3:跨模块引用时,必须将依赖的 AIDL 文件连同包路径一并复制到当前模块的 aidl 目录下,不能跨模块引用 .aidl 源文件。
工程经验:在导航多模块项目中,建议将 AIDL 文件放在独立的 :navigation-aidl 模块中,其他模块通过依赖该模块共享接口定义。这样避免重复拷贝导致接口不一致。

2.5 AIDL 编译产物

AIDL 编译器会为每个接口生成对应的 .java 代理类,理解产物结构有助于调试远程调用异常:

// 由 INavigationCallback.aidl 生成的 Java 类结构
// 路径: build/generated/aidl_source_output_dir/.../
public interface INavigationCallback extends android.os.IInterface {
    public static abstract class Stub extends android.os.Binder implements INavigationCallback {
        // 本地实现只需继承 Stub 并实现接口方法
        public static INavigationCallback asInterface(android.os.IBinder obj);
        public android.os.IBinder asBinder();
    }
    public static class Proxy implements INavigationCallback {
        // Proxy 封装了 transact() 调用,对客户端透明
    }
    public void onRouteUpdated(com.example.navigation.RouteInfo route) throws RemoteException;
    public void onNavigationError(java.lang.String message, int errorCode) throws RemoteException;
    public void onArrival() throws RemoteException;
}

核心机制:Stub 是 Binder 本地对象,Proxy 是远程代理。客户端通过 asInterface() 自动区分本地/远程调用——当调用发生在同一进程时,直接执行本地方法;跨进程时走 Parcel 序列化通道。

调试技巧:在 generated 目录中查看生成的 .java 文件,可以精确跟踪方向标记如何影响 writeToParcel 和 createFromParcel 的调用顺序。如果发现方向异常,优先检查这里。

至此,AIDL 接口设计的四个核心维度——语法、方向、类型、文件组织——已全部覆盖。下一阶段将进入客户端与服务端的实际实现流程,上述知识将直接应用于导航回调的代码编写。