11、AIDL 编译原理:AIDL 文件如何生成 Java/C++ 存根(Stub)与代理(Proxy)代码

好,咱们今天聊点硬核的。AIDL 文件,说白了就是一张“协议单”。你写个 .aidl 文件,定义好接口和方法,然后编译系统就会自动帮你生成一堆代码。这堆代码里最重要的两个角色,就是 Stub(存根)Proxy(代理)

我刚开始接触 Binder 时,总觉得这玩意儿很玄乎。后来自己动手翻了一遍生成的代码,才发现——嗯,其实没那么复杂。今天我就带你走一遍这个编译过程,看看 AIDL 到底是怎么变成 Java 和 C++ 代码的。

11.1 AIDL 编译器的“三件套”

AIDL 编译器(aidl 工具)会根据你的 .aidl 文件,生成三个核心文件:

  • 接口文件:比如 IMyService.java,里面定义了接口方法。
  • Stub 类:一个抽象类,继承自 Binder,实现了接口。服务端用这个。
  • Proxy 类:实现了接口,内部持有 IBinder 引用。客户端用这个。

如果你用的是 C++ 端,还会生成对应的 .h 和 .cpp 文件,逻辑完全一样,只是语言不同。

核心思想:Stub 负责“接收请求并调用本地方法”,Proxy 负责“把方法调用打包成数据发出去”。

11.2 从 .aidl 到 Java 代码:一步步拆解

假设你写了这样一个 AIDL 文件:

// IMyService.aidl
package com.example;

interface IMyService {
    int add(int a, int b);
    String getName();
}

编译后,会生成 IMyService.java。咱们来看看里面长什么样。

11.2.1 接口定义

最外层是一个接口,里面定义了你的方法:

public interface IMyService extends android.os.IInterface {
    public int add(int a, int b) throws android.os.RemoteException;
    public String getName() throws android.os.RemoteException;
}

注意,所有方法都抛出了 RemoteException。为什么?因为跨进程调用可能失败,比如对方进程挂了。这个异常你必须处理。

11.2.2 Stub 类:服务端的“守门人”

Stub 是一个抽象类,继承自 android.os.Binder,实现了 IMyService 接口。它的核心工作是:

  • 把收到的 Parcel 数据解包,调用你实现的方法。
  • 把返回值打包成 Parcel 发回去。

关键代码长这样:

public static abstract class Stub extends android.os.Binder implements IMyService {
    private static final java.lang.String DESCRIPTOR = "com.example.IMyService";

    public Stub() {
        this.attachInterface(this, DESCRIPTOR);
    }

    @Override
    public boolean onTransact(int code, android.os.Parcel data, 
                              android.os.Parcel reply, int flags) 
                              throws android.os.RemoteException {
        switch (code) {
            case TRANSACTION_add: {
                data.enforceInterface(DESCRIPTOR);
                int _arg0 = data.readInt();
                int _arg1 = data.readInt();
                int _result = this.add(_arg0, _arg1);
                reply.writeNoException();
                reply.writeInt(_result);
                return true;
            }
            case TRANSACTION_getName: {
                data.enforceInterface(DESCRIPTOR);
                java.lang.String _result = this.getName();
                reply.writeNoException();
                reply.writeString(_result);
                return true;
            }
        }
        return super.onTransact(code, data, reply, flags);
    }
}

你看,onTransact 方法里,根据 code 判断是哪个方法调用,然后从 data 里读出参数,调用本地实现,再把结果写回 reply

我个人习惯:在写服务端时,我会在 Stub 的实现类里加一些日志,方便排查问题。比如每次调用都打印一下方法名和参数。这在调试跨进程问题时特别有用。

11.2.3 Proxy 类:客户端的“传话筒”

Proxy 类实现了 IMyService 接口,内部持有 IBinder 引用。它的工作正好相反:

  • 把方法调用打包成 Parcel 数据。
  • 通过 transact() 发送给服务端。
  • 从返回的 Parcel 里读出结果。

代码示例:

private static class Proxy implements IMyService {
    private android.os.IBinder mRemote;

    Proxy(android.os.IBinder remote) {
        mRemote = remote;
    }

    @Override
    public int add(int a, int b) throws android.os.RemoteException {
        android.os.Parcel _data = android.os.Parcel.obtain();
        android.os.Parcel _reply = android.os.Parcel.obtain();
        int _result;
        try {
            _data.writeInterfaceToken(DESCRIPTOR);
            _data.writeInt(a);
            _data.writeInt(b);
            mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
            _reply.readException();
            _result = _reply.readInt();
        } finally {
            _reply.recycle();
            _data.recycle();
        }
        return _result;
    }
}

这里有个细节:writeInterfaceTokenenforceInterface 是一对。它们用来验证对方是不是你想要的接口。我曾经遇到过一个问题,客户端和服务端用了不同版本的 AIDL 文件,结果 DESCRIPTOR 不匹配,调用直接失败。嗯,从那以后我每次更新 AIDL 都会重新编译两边。

11.3 C++ 端的 Stub 和 Proxy

如果你做的是 Native 服务(比如 mediaserver),那就要用 C++ 版本。逻辑完全一样,只是语法不同。

生成的 C++ 代码里,Stub 是一个类,实现了 BnInterface(Binder Native Interface),Proxy 实现了 BpInterface(Binder Proxy Interface)。

关键区别在于:

  • C++ 端用 ParcelwriteInt32writeString8 等方法。
  • 方法调用通过 remote()->transact() 完成。
  • Stub 的 onTransact 里用 switch 判断方法 ID。

注意:C++ 端的 AIDL 生成代码里,方法 ID 是枚举值,比如 TRANSACTION_add = IBinder::FIRST_CALL_TRANSACTION + 0。如果你手动修改了这些 ID,会导致客户端和服务端对不上号。我曾经踩过这个坑,调试了一整天才发现是 ID 冲突了。

11.4 编译流程全景图

下面这张图展示了从 .aidl 文件到最终代码的完整流程:

AIDL 编译流程 .aidl 文件 aidl 编译器 Java 端 IMyService.java C++ 端 IMyService.h / .cpp Stub(服务端) Proxy(客户端) BnInterface(服务端) BpInterface(客户端)

11.5 一个容易忽略的细节:DESCRIPTOR

你可能注意到了,生成的代码里有个 DESCRIPTOR 常量,值是包名加接口名。这个玩意儿有什么用?

说白了,它就是接口的唯一标识。Binder 驱动在跨进程传输时,会用这个字符串来验证对方是不是你想要的接口。如果客户端和服务端的 DESCRIPTOR 不一致,enforceInterface 会直接抛异常。

我记得有一次,我改了包名但忘了重新编译 AIDL,结果客户端一直报 SecurityException。查了半天才发现是 DESCRIPTOR 没更新。从那以后,我每次改包名都会 make clean 一下。

11.6 总结

AIDL 编译原理,说白了就是:

  • 你写接口定义(.aidl)。
  • 编译器帮你生成 Stub(服务端)和 Proxy(客户端)。
  • Stub 负责解包 + 调用本地方法。
  • Proxy 负责打包 + 发送跨进程请求。

Java 和 C++ 的逻辑完全一样,只是语法不同。理解了这一点,你就能轻松看懂生成的代码,甚至自己手写一个 Binder 服务。

避坑指南:如果你在调试时发现跨进程调用失败,先检查 DESCRIPTOR 是否一致,再检查方法 ID 是否冲突。这两个问题我各踩过一次,每次都是血泪教训。


公众号:蓝海资料掘金营,微信deep3321