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 生成的代码 = 客户端代理(打包)+ 服务端桩(解包)+ Binder 驱动(搬运)。你只负责写接口和实现,跨进程细节由生成代码承担。

二、接口定义与数据类型

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 双向 既传入又需要被修改 两次读写,开销最大
注意:基本类型、String、IBinder 默认就是 in,不能也不需要写方向标记;List、Map、Parcelable 必须显式标注。若把只读参数误写成 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。

AIDL 跨进程调用链路 客户端进程 IBookManager.Proxy Parcel 打包 Binder 驱动 transact / onTransact 跨进程数据搬运 服务端进程 IBookManager.Stub Parcel 解包 调用请求 分发到 Stub 返回结果 解包返回 关键生成物与职责 Proxy(客户端) 写入 DESCRIPTOR 按 code 调用 transact 读取 reply 并返回 Stub(服务端) onTransact 按 code 分发 enforceInterface 校验 调用真实实现并回写 方向标记 in:客户端 → 服务端 out:服务端 → 客户端 inout:双向读写

六、常见误区与复盘

  • 忘记 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 文件。