AIDL 详解:语法、接口定义与自动生成的 Stub/Proxy

AIDL,全称 Android Interface Definition Language,是 Android 内部进程通信(IPC)的核心工具。说白了,它就是用来在不同进程之间「传话」的。你写一个 .aidl 文件,然后编译工具帮你生成一堆 Java 代码。我自己做 Binder Mock 回放时,最先摸透的就是这一层——因为如果不理解 Stub 和 Proxy 在干嘛,后期 Mock 回放根本无从下手。

AIDL 的语法要点

先看一个完整的 .aidl 接口定义:

// IStudentManager.aidl
package com.example.binder.demo;

import com.example.binder.demo.Student;
import com.example.binder.demo.IScoreCallback;

interface IStudentManager {
    // 默认是阻塞调用
    void registerCallback(IScoreCallback callback);
    void unregisterCallback(IScoreCallback callback);

    // 非阻塞调用,用 oneway 修饰
    oneway void notifyScoreChanged(String studentId, int score);

    // 输入输出方向
    void getStudentInfo(in String id, out Student info);
    void updateScores(inout List<Student> students);
}

语法部分我挑几个常用的说一下:

  • 包声明:和 Java 一样,package 必须写,且最终生成的 Java 代码会放到对应包下。
  • 导入:所有非基本类型、非 String、非 List/Map 的自定义 Parcelable 类型都需要显式 import。
  • 方法定义:返回值类型 + 方法名 + 参数列表。参数可以带方向标识:in、out、inout。
  • oneway 关键字:如果方法用 oneway 修饰,调用端会立即返回,不会等待服务端处理完成。适合通知类场景。
经验之谈: 我个人习惯在定义接口时,尽量把复杂对象的参数加上明确的方向标识。我曾经有一次在接口里没写方向,结果传对象时服务端收到的是空对象,排查了半天才发现是默认 in 导致 Parcel 只把数据写过去,读回来时自然拿不到结果。

自动生成的 Stub 与 Proxy

当你写完 .aidl 文件,Android SDK 的 aidl.exe 工具会帮你生成一个 .java 文件。以 IStudentManager 为例,生成的文件里会包含两个核心内部类:Stub 和 Proxy。

角色所在端核心职责
Stub服务端继承自 Binder,实现接口方法,提供 onTransact 分发逻辑
Proxy客户端封装 transact 调用,把参数打包到 Parcel,并处理返回数据

Stub 的原理

Stub 本身是一个抽象类,你需要在服务端写一个具体实现类继承它。它的核心方法是 onTransact:

@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
    switch (code) {
        case TRANSACTION_registerCallback: {
            // 1. 读取参数
            data.enforceInterface(DESCRIPTOR);
            IScoreCallback _arg0 = IScoreCallback.Stub.asInterface(data.readStrongBinder());
            // 2. 调用开发者实现的方法
            this.registerCallback(_arg0);
            // 3. 写回结果(如果需要)
            reply.writeNoException();
            return true;
        }
        // ... 其他方法
    }
    return super.onTransact(code, data, reply, flags);
}

这里有个关键点:onTransact 运行在 Binder 线程池中,不是你主线程。所以如果你的接口方法里要更新 UI,记得自己切回主线程。嗯,这个坑我踩过——服务端直接抛了 CalledFromWrongThreadException。后来我在 Stub 的实现里加了 Handler 转发。

Proxy 的原理

当你拿到服务端的 IBinder 对象(比如通过 bindService 返回的),其实你拿到的是 Stub 的一个代理引用。客户端调用时,实际上调用的是 Proxy 的同名方法:

public void registerCallback(IScoreCallback callback) throws RemoteException {
    Parcel _data = Parcel.obtain();
    Parcel _reply = Parcel.obtain();
    try {
        _data.writeInterfaceToken(DESCRIPTOR);
        _data.writeStrongBinder(callback != null ? callback.asBinder() : null);
        // 发起跨进程调用
        mRemote.transact(Stub.TRANSACTION_registerCallback, _data, _reply, 0);
        _reply.readException();
    } finally {
        _reply.recycle();
        _data.recycle();
    }
}

注意:transact 默认是同步阻塞的。如果你搞了 oneway 方法,那么 flags 参数会传 FLAG_ONEWAY,调用后立即返回,_reply 为空 parcel。

避坑指南: 我曾经在 Proxy 端忘了检查 _reply.readException(),服务端抛了异常,结果客户端一直傻等。血泪教训——务必在调用后检查异常,否则可能引发 ANR。

数据方向标识 in / out / inout

AIDL 支持三种参数方向,影响序列化和反序列化的行为:

  • in(默认):服务端只收到客户端传过来的数据,服务端修改参数不会回传给客户端。
  • out:客户端不传数据,服务端负责构造对象并填充,然后回传给客户端。适用于服务端生成数据的场景。
  • inout:双向传输,客户端传过去,服务端改完后返回。

这个方向标识会直接影响生成代码中对 Parcel 的读写顺序。我建议在接口设计时尽量用 in,因为 inout 会多一次 Parcel 操作,性能稍微差一点。

核心结论: AIDL 是 Binder 机制的“契约文件”。Stub 和 Proxy 则是编译器帮我们生成的桥梁。理解它们的工作方式,才能去手动 Mock 或回放 IPC 数据。

AIDL 编译生成与 IPC 调用流程 .aidl 接口定义文件 aidl 编译器 Stub(服务端基类) 继承 Binder + onTransact 分发 Proxy(客户端代理) 封装 transact 调用 具体实现类 继承 Stub,实现业务逻辑 客户端调用 通过 Proxy 发起跨进程请求 Binder 驱动(内核层) 👆 从 .aidl 文件生成 Stub + Proxy,客户端通过 Proxy 调用,经 Binder 驱动到达服务端 Stub 的实现类

总结一个实用 checklist

  1. 写 AIDL 前先确定哪些类型需要 Parcelable 化(自定义类必须实现 Parcelable 接口)。
  2. 接口方法尽量少用 inout,能用 in 就用 in,否则容易造成不必要的数据拷贝。
  3. Stub 中的 onTransact 运行在 Binder 线程,非主线程,注意线程安全。
  4. 客户端 Proxy 调用务必检查 reply.readException(),否则远程异常永远不会被发现。
  5. 如果你要 Mock 回放 IPC 调用,最直接的方式就是替换掉 Stub 或 Proxy 的实现,直接绕过 Binder 驱动。
曾经踩过的坑: 我设计多进程摄像头回调接口时,把回调接口定义成了 oneway,但忘记处理 Binder 引用计数导致服务端端回调一直没释放。后来在 onTransact 里加了 linkToDeath 监听才搞定。所以说,Binder 的生命管理一定要小心。

好了,AIDL 的核心内容就这些。你只要吃透 Stub 和 Proxy 的生成逻辑,后面做 Mock 回放就相当于在“替换”这套桥梁——把真实的 Binder 调用替换成你预设的数据包。这部分我们后面章节会手把手带。