AIDL接口定义与自动生成的Stub/Proxy
聊到Binder Mock回放,绕不开AIDL。很多人觉得AIDL就是个自动生成代码的工具,用起来挺方便,但真要理解它生成的Stub和Proxy是什么鬼,往往会一头雾水。我记得几年前刚接手一个跨进程通信的项目,同事丢给我一个.aidl文件,说“你跑一下生成代码看看”。我盯着那一大堆生成的Java文件,心里直嘀咕——这玩意儿到底干了啥?
说白了,AIDL(Android Interface Definition Language)就是帮你定义跨进程接口的语言。编译器吃进去一个.aidl文件,吐出来一个Java接口文件,外加两个内部类:Stub(服务端骨架)和Proxy(客户端代理)。整个过程就像工厂流水线,你只管画图纸,他帮你造零件。
AIDL文件长什么样?
我们先看一个最简单的例子,定义一个图书管理接口:
// IBookManager.aidl
package com.example.binder;
import com.example.binder.Book;
interface IBookManager {
List<Book> getBookList();
void addBook(in Book book);
}
注意几个细节:
- 包名必须和Android项目中的包名一致,不然生成代码会找不到类。
- 自定义的Parcelable类(比如这里的Book)需要单独import,并且要有一个同名的.aidl文件声明它为parcelable。
- 方法参数可以用in、out、inout修饰,默认是in,跨进程时序列化行为不同。
个人建议:如果你只是单向传递数据,尽量用in,性能最好。我曾经在一个高频调用的场景里用了inout,结果每次序列化都多拷贝一遍对象,卡得用户直骂娘。
自动生成的Stub和Proxy
编译完.aidl后,系统会生成一个对应的Java接口文件。还是以IBookManager为例,它内部会嵌套两个静态类:Stub 和 Proxy。
| 类名 | 职责 | 核心方法 |
|---|---|---|
| Stub | 服务端骨架,继承Binder,实现接口方法 | onTransact(int code, Parcel data, Parcel reply, int flags) |
| Proxy | 客户端代理,封装了mRemote Binder引用 | transact(int code, Parcel data, Parcel reply, int flags) |
Stub的核心工作就是反序列化客户端发来的请求,然后调用你实际实现的接口方法,再把返回值序列化回去。Proxy正好相反:它把你的参数序列化好,通过Binder驱动发给服务端,然后反序列化结果。
交互流程:从客户端到服务端
你可以把整个过程想象成两个人通过电话沟通:
- 客户端(Proxy):把你要说的话(方法名、参数)写在纸(Parcel)上,然后通过电话线(Binder驱动)传过去。
- 服务端(Stub):收到纸条,读一下是什么事(onTransact根据code匹配方法),然后执行你的业务逻辑,把结果写回纸条。
- 客户端:再读出纸条上的结果。
这里面的魔法就是transact和onTransact,它们都接收一个int型的code,这个code就是在生成代码时自动分配的方法编号。AIDL编译器会从FIRST_CALL_TRANSACTION(0x00000001)开始依次递增。
避坑指南:我曾经遇到过一个bug,服务端改了AIDL接口的顺序,客户端没更新,结果code错乱了,调用getBookList实际执行了addBook。这种问题极难排查,因为不会崩,只是行为诡异。解决办法:尽量把接口版本号写到Binder的transaction code中做校验,或者强制要求客户端和服务端代码同步升级。
生成的代码片段长啥样?
下面我贴一段简化版Stub的代码,让你直观感受一下:
public static abstract class Stub extends android.os.Binder implements IBookManager {
private static final String DESCRIPTOR = "com.example.binder.IBookManager";
static final int TRANSACTION_getBookList = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
static final int TRANSACTION_addBook = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
@Override
protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) throws RemoteException {
switch (code) {
case TRANSACTION_getBookList: {
data.enforceInterface(DESCRIPTOR);
List<Book> _result = this.getBookList();
reply.writeNoException();
reply.writeTypedList(_result);
return true;
}
case TRANSACTION_addBook: {
data.enforceInterface(DESCRIPTOR);
Book _arg0;
_arg0 = data.readTypedParcelable(Book.CREATOR);
this.addBook(_arg0);
reply.writeNoException();
return true;
}
}
return super.onTransact(code, data, reply, flags);
}
}
而Proxy那边就更简单了,就是调用mRemote.transact,把data和reply打包好。这里不多说,后面章节讲Mock回放的时候会重点分析Proxy的调用链路。
知识结构速览
我把AIDL接口定义到最终跨进程调用的核心流程画成了一张图,方便你记忆:
嗯,这张图基本覆盖了从.aidl文件到最终Binder驱动的完整路线。你在做Mock回放的时候,重点要关注的就是Stub和Proxy这两个类——因为它们分别承担了“序列化/反序列化”和“跨进程传输”的核心职责。只有理解了它们是怎么配合工作的,你才能精准地构建Mock对象,模拟真实的调用过程。
我的习惯:每当我需要写一个跨进程接口的单元测试,我会先手动看一遍生成的Stub代码,把每个方法对应的transaction code记下来。这样在Mock的时候,我就可以直接用那些code去模拟onTransact的调用,而不需要真的启动两个进程。效率高很多。