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驱动发给服务端,然后反序列化结果。

交互流程:从客户端到服务端

你可以把整个过程想象成两个人通过电话沟通:

  1. 客户端(Proxy):把你要说的话(方法名、参数)写在纸(Parcel)上,然后通过电话线(Binder驱动)传过去。
  2. 服务端(Stub):收到纸条,读一下是什么事(onTransact根据code匹配方法),然后执行你的业务逻辑,把结果写回纸条。
  3. 客户端:再读出纸条上的结果。

这里面的魔法就是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 编译与跨进程调用流程 .aidl 文件 AIDL 编译器 (aidl.exe) Stub (服务端骨架) onTransact() 分发 Proxy (客户端代理) transact() 调用 Binder 驱动 (内核) 数据拷贝 + 线程管理 关键点:Stub/Proxy 通过 Parcel 完成参数的序列化与反序列化,Binder驱动负责跨进程拷贝

嗯,这张图基本覆盖了从.aidl文件到最终Binder驱动的完整路线。你在做Mock回放的时候,重点要关注的就是Stub和Proxy这两个类——因为它们分别承担了“序列化/反序列化”和“跨进程传输”的核心职责。只有理解了它们是怎么配合工作的,你才能精准地构建Mock对象,模拟真实的调用过程。

我的习惯:每当我需要写一个跨进程接口的单元测试,我会先手动看一遍生成的Stub代码,把每个方法对应的transaction code记下来。这样在Mock的时候,我就可以直接用那些code去模拟onTransact的调用,而不需要真的启动两个进程。效率高很多。