Binder(六):AIDL 生成的 Java 类细节:接口、Stub 与 Proxy

文章内容

一、面试题:AIDL 生成的 Java 类细节

1.1 题目背景

这道题问的是编译产物:写完 .aidl 文件点一下构建,生成的那个 Java 文件里到底有什么。不少人做过跨进程需求,却从没打开过 build/generated 目录下的那份代码,回答就只能停在「AIDL 用来跨进程」这一层。

1.2 AIDL 是什么

AIDL(Android Interface Definition Language,Android 接口定义语言)是一门只用来描述接口的小语言。你按它的语法声明方法,编译器把这份声明翻译成一整套符合 Binder 规则的 Java 代码。

它的角色像房产中介。手续本身并不神秘,跑一遍也能办完,但流程繁琐且每次都一样,于是交给中介代办。Binder 同理——按它的规则做一次跨进程调用,需要写的模板代码又多又固定,AIDL 把这部分包办了。既然是模板,就完全可以手写;很多讲解为了看清结构,干脆不用 AIDL,直接手敲这三个类。

二、AIDL 生成的三个类

AIDL 生成的三个类:接口定契约,Stub 服务端,Proxy 客户端

2.1 接口:声明跨进程方法

生成文件的最外层是一个接口,继承自 IInterface(Binder 体系里所有可跨进程接口的父接口,只要求实现 asBinder())。跨进程方法都声明在这里,客户端只依赖它,不关心对面是谁在实现。

这和普通的面向接口编程是同一套思路,区别在于调用会跨越进程边界,所以每个方法都要声明 throws RemoteException。

2.2 Stub:服务端基类

接口的真正实现写在服务类里,比如某个 RemoteService。问题是服务端得把这个实现「交给」客户端,而对象没法跨进程传递,能传的只有 Binder 引用。

Stub(存根类,服务端的桩)解决的就是这件事:它继承 Binder,让服务端具备跨进程被调用的能力;同时实现那个接口,但把业务方法留空——Stub 是抽象类,方法实现由具体服务类补全。这样一来,服务端返回的是一个 Binder 实体,而这个实体知道怎么把请求路由到实现代码上。

2.3 Proxy:客户端代理

客户端拿到的从来不是服务端的真实对象。Proxy(代理类)是一个普通类,实现同一个接口,内部持有一个 IBinder(Binder 通信句柄的抽象,客户端侧通常是驱动给的代理引用)。

客户端调接口方法时,实际执行的是 Proxy 里的同名方法,Proxy 负责打包参数、发起通信、解包结果。代理模式在这里的价值很直接:调用方写的代码和调用本地对象没有区别,跨进程的复杂度全被 Proxy 吞掉了。

三、一次跨进程调用的完整链路

一次跨进程调用链路:Proxy → 驱动 → onTransact → 服务实现类

3.1 两个 Parcel:data 与 reply

两个 Parcel:data 发出去,reply 带回来

Proxy 方法的第一件事是取两个 Parcel(Binder 通信的数据容器,负责把参数序列化成可跨进程传输的字节)。data 装客户端发出去的内容,reply 留给服务端写返回值。

像出差打两个包:一个装自己要带过去的东西,一个空箱子留着装特产带回来。两个 Parcel 都从对象池 obtain() 取,用完必须 recycle() 归还。

3.2 transact 与同步/异步

参数写好后调 remote.transact(code, data, reply, flags)——transact 是 IBinder 上发起一次事务的方法,remote 就是 Proxy 持有的那个 Binder 引用。

flags 传 0 表示同步调用,客户端线程在这里挂起,一直等到服务端处理完、reply 被填好才继续往下走;传 FLAG_ONEWAY 则是异步,发出去就返回,拿不到返回值。日常业务大多是同步调用,所以主线程里做跨进程调用会卡 UI,这一点在写代码时要留心。

3.3 onTransact 与方法 ID 分发

请求进入 Binder 驱动,驱动按目标把事务投递到服务端进程,服务端的 Binder 线程被唤醒,进入 onTransact()——Stub 侧接收事务的统一入口。

服务端怎么知道客户端要调哪个方法?靠方法 ID。编译器把接口里的方法按声明顺序编号,从 IBinder.FIRST_CALL_TRANSACTION(值为 1)开始递增,生成 TRANSACTION_xxx 常量。这和布局文件里的控件编译后拿到一串 ID 是一个套路:名字给人看,编号给机器用。客户端 transact 时带上编号,onTransact 里一个 switch 按编号分发到对应分支。

3.4 返回值与 reply 打包

分发到具体分支后,data 里的参数被读出来,接着调用本类的接口方法。由于 Stub 是抽象的,这一调就落到了真正的服务实现类上,业务逻辑在这里执行。

有返回值的方法把结果写进 reply,驱动带回客户端,Proxy 那边从 reply 里读出来交给调用方;返回 void 的方法就不写。异常也走这条路:服务端 writeNoException() 或写入异常信息,客户端 readException() 时在本地重新抛出。

3.5 asInterface:怎么拿到 Binder 和 Proxy

asInterface:同进程返回 Stub,跨进程包装成 Proxy

客户端绑定服务后,onServiceConnected 回调里给的是一个 IBinder,需要用 Xxx.Stub.asInterface(binder) 转成接口类型。

这个静态方法里藏着一个容易被忽略的判断:它先用 queryLocalInterface(DESCRIPTOR) 查一下这个 Binder 是不是本进程的实体。如果服务和客户端在同一个进程,查得到,直接返回 Stub 本身,方法调用就是普通的 Java 调用,一次驱动都不用进;查不到才说明是跨进程,包装成 Proxy 返回。面试里能把这层判断讲出来,比只背「asInterface 返回代理」要值钱。

四、代码验证

4.1 三个类的最小骨架

把生成代码里的异常处理和边界分支去掉,剩下的骨架就是下面这些。看它是为了确认三个类的从属关系:Stub 和 Proxy 都是接口的内部类,一个在服务端一个在客户端。

java
// 示意代码:AIDL 生成(或手写)的三件套,仅保留结构public interface IPerson extends IInterface {                       // 跨进程接口,继承 IInterface 才被 Binder 体系认可    String DESCRIPTOR = "com.demo.IPerson";                         // 接口标识,两端用它确认在聊同一个接口    int TRANSACTION_getName = IBinder.FIRST_CALL_TRANSACTION;       // 方法 ID,第一个方法编号为 1    String getName() throws RemoteException;                        // 声明跨进程方法,跨进程调用都可能失败     abstract class Stub extends Binder implements IPerson {         // 服务端基类:继承 Binder 才具备被跨进程调用的能力        public Stub() {                                             // 构造时做一次绑定            attachInterface(this, DESCRIPTOR);                      // 把自己与描述符关联,供本地查询使用        }                                                           // 构造结束        @Override public IBinder asBinder() { return this; }        // 服务端自身就是 Binder 实体        public static IPerson asInterface(IBinder binder) {         // 把客户端拿到的 IBinder 转成接口            if (binder == null) return null;                        // 服务尚未连接时没有可用对象            IInterface local = binder.queryLocalInterface(DESCRIPTOR); // 查询是否为本进程内的实现            if (local instanceof IPerson) return (IPerson) local;    // 同进程:直接返回 Stub,不走 Binder 通信            return new Proxy(binder);                               // 跨进程:包装成代理返回        }                                                           // asInterface 结束    }                                                               // Stub 结束,getName() 故意不实现,留给服务类     class Proxy implements IPerson {                                // 客户端代理:实现同一接口,调用方无感知        private final IBinder remote;                               // 持有驱动给的 Binder 引用,通信出口        Proxy(IBinder remote) { this.remote = remote; }             // 由 asInterface 注入        @Override public IBinder asBinder() { return remote; }      // 需要 IBinder 时交出持有的引用        @Override public String getName() { return null; }          // 打包与发送逻辑见下一段    }                                                               // Proxy 结束}                                                                   // 接口结束

三个类各自的位置到这里就清楚了:接口定契约,Stub 站在服务端等调用,Proxy 站在客户端发调用。

4.2 一次调用在两端的走向

上面留空的 getName() 补全后,就是一次完整的跨进程调用。这段代码把「客户端打包」和「服务端分发」两头对起来看,方法 ID 的作用一眼就明白。

java
// 示意代码:一次调用的两端实现// ===== 客户端:Proxy 中的方法 =====@Override public String getName() throws RemoteException {          // 客户端调接口方法,实际落到这里    Parcel data = Parcel.obtain();                                  // 取一个 Parcel 装发出去的参数    Parcel reply = Parcel.obtain();                                 // 再取一个准备接收返回值    String result;                                                  // 保存最终结果    try {                                                           // 用 try/finally 确保 Parcel 被归还        data.writeInterfaceToken(DESCRIPTOR);                       // 写入接口标识,服务端会校验        remote.transact(TRANSACTION_getName, data, reply, 0);       // 发起事务:方法 ID + 入参 + 出参,0 表示同步        reply.readException();                                      // 服务端若抛了异常,在客户端重新抛出        result = reply.readString();                                // 读出服务端写回的返回值    } finally {                                                     // 无论成功失败都要收尾        reply.recycle();                                            // 归还 reply        data.recycle();                                             // 归还 data    }                                                               // 收尾结束    return result;                                                  // 交给调用方,用起来和本地方法一样}                                                                   // 方法结束 // ===== 服务端:Stub 中的分发入口 =====@Override protected boolean onTransact(int code, Parcel data,       // 驱动唤醒服务端线程后进入这里                                       Parcel reply, int flags) throws RemoteException {  // code 即方法 ID    switch (code) {                                                 // 按客户端带来的编号分发        case TRANSACTION_getName:                                   // 命中 getName 的编号            data.enforceInterface(DESCRIPTOR);                      // 校验接口标识,不一致直接失败            String name = this.getName();                           // 调到具体服务类里的真正实现            reply.writeNoException();                               // 写入「无异常」标记            reply.writeString(name);                                // 把返回值写进 reply 带回客户端            return true;                                            // 事务已处理    }                                                               // switch 结束    return super.onTransact(code, data, reply, flags);              // 其余编号交给父类处理}                                                                   // 方法结束

客户端和服务端靠两样东西对齐:DESCRIPTOR 保证接口一致,TRANSACTION_getName 保证方法一致。data 单向送过去,reply 单向带回来,中间那段由 Binder 驱动完成。

五、总结

5.1 三个类各司其职

接口负责声明方法并定义描述符与方法编号;Stub 继承 Binder,作为服务端的接收端,把 onTransact 收到的编号分发到实现类;Proxy 实现同一接口,替客户端把方法调用翻译成一次 Binder 事务。

5.2 一次调用的完整路径

客户端调接口方法 → Proxy 取 data/reply 两个 Parcel 并写入参数 → remote.transact 带上方法 ID 发起事务(同步调用时线程挂起)→ 驱动投递到服务端 → onTransact 按方法 ID switch 分发 → 服务类执行真正的逻辑 → 返回值写进 reply → 客户端读出结果返回。

想确认这套结构不用背,写一个 .aidl 文件构建一次,去 app/build/generated/aidl_source_output_dir/ 下打开生成的 Java 文件,对照本文的骨架逐段看。再在服务端 onTransact 的 switch 上打个断点,从客户端发一次调用,就能看清 code 的取值和它落到哪个分支。


本文由 AgentPlanFlow 生成

觉得不错?给作者点个赞吧

分类:Android系统

评论 (0)

无需注册,留言经站长审核后公开。

加载评论中…

Binder(六):AIDL 生成的 Java 类细节:接口、Stub 与 Proxy · Yinchen 的博客