Binder(二):AIDL 生成的 Proxy、Stub 和 Parcel 到底在做什么?

Binder(二):AIDL 生成的 Proxy、Stub 和 Parcel 到底在做什么?

上一篇完成了端到端闭环:

text 复制代码
业务 Proxy
    → BinderProxy
    → JNI / libbinder
    → Binder Driver
    → 服务端 Binder 线程
    → Stub.onTransact()
    → 业务实现

但其中最靠近业务的一段还没有拆开:

java 复制代码
int result = calculator.add(1, 2);

为什么双方能约定:

text 复制代码
这笔事务调用 add;
第一个 int 是 a;
第二个 int 是 b;
返回的 int 是 result;
服务端异常应该在客户端重新抛出?

答案是:AIDL 编译器把接口定义生成了一份双方共同遵守的协议实现。

text 复制代码
业务 Proxy:本地方法 → transaction code + data Parcel
AIDL Stub:transaction code + data Parcel → 本地方法
reply Parcel:服务端结果 / 异常 → 客户端返回值 / 异常

源码和生成工具沿用导读第四节"系列统一基线"。下面展示 Build Tools 36.0.0 生成结构的等价主干;生成片段和哈希见 generated-reference

一份接口,怎样变成两端协议

AIDL 文件本身不会跨进程

用同步 add() 和一笔 oneway 比较生成协议:

aidl 复制代码
interface ICalculator {
    int add(int a, int b);
    oneway void notifyValue(int requestId, int value);
}

完整协议见 BinderLab AIDL 目录,其中还包含异步 callback、同步 callback 与不同 node 对照接口。

.aidl 文件只定义协议:

text 复制代码
接口全名
方法集合
参数顺序和类型
返回值类型
方法是否 oneway

真正让 Java 代码可调用的是生成结果。最关键的结构可以压缩为:

java 复制代码
public interface ICalculator extends IInterface {
    int add(int a, int b) throws RemoteException;

    abstract class Stub extends Binder implements ICalculator {
        static ICalculator asInterface(IBinder obj) { ... }
        boolean onTransact(int code, Parcel data, Parcel reply, int flags) { ... }

        private static class Proxy implements ICalculator {
            private IBinder mRemote;
            int add(int a, int b) { ... }
        }
    }
}

AIDL 不是 Binder Driver,也不是 Parcel 本身。它负责生成把"业务接口语言"翻译成"Binder 事务语言"的胶水代码。

Proxy 编码,Stub 解码

这张图按中间的"协议与传输边界"阅读:

text 复制代码
左边只看 Proxy 怎样写;
中间先区分 AIDL 接口协议与 Binder 传输上下文;
AIDL 区域只包含 descriptor、transaction code 和 Parcel schema;
handle 只出现在下层 remote IBinder / BpBinder 传输上下文;
右边只看 Stub 怎样读;
下方比较本地直调与跨进程路径。

核心关系只有两行:

text 复制代码
Proxy.add()  --编码-->  code + data Parcel
Stub.onTransact()  --解码-->  service.add()

IInterfaceIBinder 不在同一抽象层

这是理解 AIDL 生成代码最关键的桥梁。

1. IInterface 表达"它能做什么"

Android 的 IInterface 只有一个桥接方法:

java 复制代码
public interface IInterface {
    IBinder asBinder();
}

ICalculator extends IInterface 后,业务代码获得的是类型化能力:

text 复制代码
add()
notifyValue()
notifyValueViaHandler()
registerCallback()
addAndCallback()
getAsyncWorker()
addWithRequestId()
2. IBinder 表达"怎样把它作为 Binder 端点调用和传递"

IBinder 提供的是通用 Binder 能力:

text 复制代码
transact()
queryLocalInterface()
linkToDeath()
isBinderAlive()
getInterfaceDescriptor()

可以压缩成:

IInterface 表达业务接口,IBinder 表达可远程调用端点。

3. asInterface()asBinder() 是两座反向桥
text 复制代码
IBinder
    ↓ Stub.asInterface()
类型化 ICalculator
text 复制代码
ICalculator
    ↓ asBinder()
通用 IBinder

前者让拿到通用 Binder 引用的客户端恢复业务接口;后者让业务接口能作为 Binder 对象继续注册、传递或调用通用能力。

生成代码里的两条分支

Stub 同时连接事务入口与业务接口

生成结构是:

java 复制代码
public abstract static class Stub
        extends Binder
        implements ICalculator {
}

两个父类型承担不同职责:

text 复制代码
extends Binder:
    让 Stub 成为本地 Binder 实体,能够接收 transact。

implements ICalculator:
    让 Stub 或其子类提供 add() 等业务方法。

构造函数把业务接口与 Binder 端点关联起来:

java 复制代码
public Stub() {
    attachInterface(this, DESCRIPTOR);
}

Android 16 的 Binder.attachInterface() 保存:

text 复制代码
mOwner = this
mDescriptor = "com.example.binderdemo.ICalculator"

这为同进程快速路径准备了条件。

Stub.asInterface() 先判断本地还是远端

按生成结构整理后的等价主干是:

java 复制代码
public static ICalculator asInterface(IBinder obj) {
    if (obj == null) {
        return null;
    }

    IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
    if (iin instanceof ICalculator) {
        return (ICalculator) iin;
    }

    return new Proxy(obj);
}

Binder.queryLocalInterface() 会检查 descriptor,并在匹配时直接返回构造 Stub 时保存的本地 owner,见 Binder.java

因此:

场景 queryLocalInterface() asInterface() 返回 调用路径
同进程、同后端、本地 Binder 找到本地 owner Stub/业务实现对象 普通 Java 直接调用
跨进程远端 Binder 返回 null 新建业务 Proxy Parcel + transact + Driver

官方 AIDL overview 明确说明:同进程且使用同一后端时不存在 Proxy,也不需要打包和解包。

所以三句常见说法都是错的:

text 复制代码
写了 AIDL 就一定跨进程。
asInterface() 一定返回 Proxy。
服务端方法一定在 Binder 线程执行。

是否跨进程,取决于实际 Binder 引用指向本地端点还是远端端点。

两种 Proxy 分别保存业务协议与通用传输

业务 Proxy 的结构是:

java 复制代码
private static class Proxy implements ICalculator {
    private IBinder mRemote;

    Proxy(IBinder remote) {
        mRemote = remote;
    }

    @Override
    public IBinder asBinder() {
        return mRemote;
    }
}

对象关系是:

text 复制代码
ICalculator.Stub.Proxy
    │
    │ 实现 add(),理解业务协议
    │ mRemote
    ▼
IBinder
    │
    └─ 跨进程时通常由 BinderProxy 实现
          只提供通用 transact 能力

因此不要写:

text 复制代码
BinderProxy.add() 把参数写进 Parcel。

准确说法是:

text 复制代码
AIDL 业务 Proxy.add() 写 Parcel;
它再调用 mRemote,也就是 BinderProxy.transact()。

跟一笔 add() 走完编解码

Proxy.add() 写入请求

Build Tools 36.0.0 生成结构的等价主干是:

固定生成物使用无参 Parcel.obtain(),没有生成 enforceNoDataAvail()。AIDL 输出会随工具版本变化;精确结构见生成主干摘录和四个文件的 SHA-256

java 复制代码
@Override
public int add(int a, int b) throws RemoteException {
    Parcel data = Parcel.obtain();
    Parcel reply = Parcel.obtain();
    int result;
    try {
        data.writeInterfaceToken(DESCRIPTOR);
        data.writeInt(a);
        data.writeInt(b);

        mRemote.transact(TRANSACTION_add, data, reply, 0);

        reply.readException();
        result = reply.readInt();
    } finally {
        reply.recycle();
        data.recycle();
    }
    return result;
}

写入顺序就是协议的一部分:

text 复制代码
1. interface token
2. 参数 a
3. 参数 b

服务端必须按相同顺序读取。

descriptor、transaction code 与 handle 各有作用域

Binder 中的多个"编号"必须放进各自坐标系:

标识 识别什么 作用范围
Service name 去哪里发现根服务 ServiceManager 注册表
Binder handle 当前进程引用哪个远端 node 当前引用持有进程
Interface descriptor Binder 端点实现什么接口 接口协议
Transaction code 调用接口中的哪个方法 当前接口
UID / PID 谁发起当前事务 调用身份
TID 当前哪条线程在执行 当前进程线程空间

ICalculator 示例中:

java 复制代码
DESCRIPTOR = "com.example.binderdemo.ICalculator";

TRANSACTION_add = IBinder.FIRST_CALL_TRANSACTION + 0;
TRANSACTION_notifyValue = IBinder.FIRST_CALL_TRANSACTION + 1;
TRANSACTION_notifyValueViaHandler = IBinder.FIRST_CALL_TRANSACTION + 2;
TRANSACTION_registerCallback = IBinder.FIRST_CALL_TRANSACTION + 3;
TRANSACTION_addAndCallback = IBinder.FIRST_CALL_TRANSACTION + 4;
TRANSACTION_getAsyncWorker = IBinder.FIRST_CALL_TRANSACTION + 5;
TRANSACTION_addWithRequestId = IBinder.FIRST_CALL_TRANSACTION + 6;

这组数值来自 AIDL 的声明顺序,接口重排后可能变化,不应在外部协议中手写依赖它。

因此:

text 复制代码
descriptor 不是 ServiceManager 注册名;
transaction code 不是远端对象编号;
handle 也不是方法编号。

interface token 守住接口边界

Proxy 先写:

java 复制代码
data.writeInterfaceToken(DESCRIPTOR);

Stub 在处理用户事务前执行:

java 复制代码
data.enforceInterface(DESCRIPTOR);

Android 16 的 Parcel.writeInterfaceToken() / enforceInterface() 会写入并校验接口头。

它回答的是:

text 复制代码
发送方和接收方是否在使用同一份接口 descriptor?

它不负责:

text 复制代码
定位远端进程;
选择 add 方法;
验证调用者是否拥有业务权限。

Stub.onTransact() 读回参数并调用实现

服务端收到事务后,生成的分发结构是:

java 复制代码
@Override
public boolean onTransact(
        int code, Parcel data, Parcel reply, int flags)
        throws RemoteException {

    if (code >= FIRST_CALL_TRANSACTION
            && code <= LAST_CALL_TRANSACTION) {
        data.enforceInterface(DESCRIPTOR);
    }

    switch (code) {
        case TRANSACTION_add: {
            int a = data.readInt();
            int b = data.readInt();
            int result = this.add(a, b);
            reply.writeNoException();
            reply.writeInt(result);
            return true;
        }
    }

    return super.onTransact(code, data, reply, flags);
}

这里完成了四步反向翻译:

text 复制代码
校验接口 token
    ↓
用 transaction code 选择方法
    ↓
按协议顺序读取参数
    ↓
调用本地业务实现并写 reply

驱动只把 code 和 buffer 交到端点;Stub.onTransact() 才知道 code 对应 add()

协议不只包含参数和返回值

reply 同时承载异常状态

正常路径:

java 复制代码
// 服务端
reply.writeNoException();
reply.writeInt(result);

// 客户端
reply.readException();
return reply.readInt();

AIDL/Binder 支持的异常或状态类别可以编码进 reply;任意自定义 Java 异常不会自动变成稳定的跨进程协议。如果传输层失败,例如远端死亡,transact() 会以 RemoteException 或其子类暴露失败。

需要区分:

失败层次 例子 客户端观察
接口/业务失败 非法参数、服务自定义错误 reply 中的异常或状态
协议失败 descriptor 或 code 不匹配 SecurityExceptionfalse 或未知事务
传输失败 远端死亡、驱动事务失败 RemoteException / DeadObjectException

官方 AIDL backends 的错误处理 还强调:发生错误时,返回值和 out/inout 参数可能处于不确定状态,不能继续当作有效结果使用。

Parcel 还携带 Binder 对象语义

add(int, int) 来说,Parcel 看起来只是顺序写入几个值。但 Binder 协议还允许写入:

text 复制代码
字符串和 Parcelable
Binder 接口引用
文件描述符
异常状态
返回值

例如 callback 参数会生成:

java 复制代码
data.writeStrongInterface(callback);

服务端读取:

java 复制代码
IResultCallback callback =
        IResultCallback.Stub.asInterface(data.readStrongBinder());

Binder 引用和文件描述符不能仅靠原始字节复制保持语义。Parcel 还维护特殊对象位置,驱动会在跨进程时转换它们。第三、四篇再分别展开对象和驱动视角。

oneway 改变 flags 与 reply 约定

notifyValue() 的 Proxy 主干是:

java 复制代码
Parcel data = Parcel.obtain();
try {
    data.writeInterfaceToken(DESCRIPTOR);
    data.writeInt(requestId);
    data.writeInt(value);
    mRemote.transact(
            TRANSACTION_notifyValue,
            data,
            null,
            IBinder.FLAG_ONEWAY);
} finally {
    data.recycle();
}

与同步 add() 对比:

text 复制代码
没有 reply Parcel;
flags 带 FLAG_ONEWAY;
调用方不等待业务 reply。

这不代表新建线程,也不代表服务端已经执行完成。调用方仍要完成 Parcel 编码、进入驱动并面对 buffer 压力;"不等业务 reply"不能扩写成"绝对零等待、绝不失败"。线程模型留到第六篇。

本地与远端分支的对照证据

实验 A:服务和客户端同进程
text 复制代码
Stub.asInterface(localBinder)
    ↓ queryLocalInterface 命中
返回本地 Stub / 业务对象
    ↓
调用线程直接执行 add()

预期证据:

text 复制代码
clientPid == serverPid
clientTid == add() 内的 tid
calculator.getClass() 是本地实现类
实验 B:服务声明为远端进程
xml 复制代码
<service
    android:name=".CalculatorService"
    android:process=":remote" />

预期证据:

text 复制代码
clientPid != serverPid
clientTid != serverTid
calculator.getClass() 是 ICalculator$Stub$Proxy
calculator.asBinder().getClass() 是 BinderProxy

实验时应打印实际 class、PID、TID 和线程名。只看到"方法成功返回"不能证明经过 Binder Driver。

Build Tools / AIDL 36.0.0 的生成代码已用 SDK Platform 36.1 构建。API 36 基线日志记录了不同的 App/:remote PID、ICalculator$Stub$Proxyandroid.os.BinderProxy,证明该次 asInterface() 选择了远端 Proxy 路径。证据包没有同进程对照,因此本地分支仍是源码路径,不是该次运行的观测结果。

协议错位:token 或 transaction code 不匹配

下面列出的是可能表现 。最终 Java 层看到 SecurityExceptionfalse、未知事务或其他异常,取决于具体 onTransact()transact() 返回值、是否存在 reply、JNI 映射以及生成代码版本,不能从"协议不匹配"直接推出唯一异常类型。

假设手写客户端把 TRANSACTION_notifyValue 当成 add() 发送:

text 复制代码
客户端以为两个 int 分别是 a、b,code 却指向 notifyValue;
服务端把相同两个字段解释成 requestId、value,并执行 notifyValue 的副作用;
notifyValue 是 oneway,不会按 add 协议写回 int reply;
客户端若继续按 add 协议读取 reply,方法语义和 reply 协议都已经分裂。

或者客户端写错 descriptor:

text 复制代码
data.writeInterfaceToken("wrong.interface");

服务端 enforceInterface() 会拒绝这笔事务。

字段数量碰巧相同不代表协议兼容;transaction code + Parcel schema + oneway/reply 约定 才共同定义一笔调用。这说明 AIDL 的价值不只是"少写代码",而是让两端从同一接口定义生成对称协议,降低手写 code、顺序、类型和异常路径不一致的风险。

把协议角色重新放回调用链

text 复制代码
AIDL:
    定义接口,并生成双方一致的协议胶水。

业务 Proxy:
    把类型化方法调用编码成 descriptor、transaction code 和 data Parcel。

IBinder / BinderProxy:
    提供通用 transact 能力。

AIDL Stub:
    校验 descriptor,用 code 选择方法,解码参数并调用本地实现。

reply Parcel:
    把返回值或异常状态送回客户端。

到这里,AIDL 的边界很明确:它负责生成对称的协议胶水,不负责解释 mRemote 为什么能代表远端对象。沿着 mRemote 再往下,才会进入 BinderProxy、BpBinder、handle、binder_refbinder_node 的对象映射。

源码与官方文档

相关推荐
爱笑鱼2 小时前
Binder(一):一次方法调用,究竟怎样跨进程执行?
android
壮哥_icon3 小时前
【Android 系统开发】使用 BAT 脚本高效自动化管理 /system/priv-app/ 系统应用(安装与卸载)
android·运维·自动化
用户69371750013844 小时前
Claude Code终端日志Token占用实测:一个过滤器砍掉60%-90%
android·前端·后端
蝉蜕日记4 小时前
AccumuPDF高级版 v2.57 | 视图文转换PDF,合并分割压缩
android·智能手机·pdf·生活·软件需求
吐了啊取名字太难4 小时前
美颜系统AI修图本地跑并支持Mac、win、安卓、iOS不卡顿
android·人工智能·windows·数码相机·mac·ai编程
阿pin6 小时前
Android随笔-Retrofit
android·retrofit
想取一个与众不同的名字好难6 小时前
安卓自定义颜色选择器
android
我命由我123456 小时前
Android 开发问题:java.lang.NoSuchMethodError:No virtual method readAllBytes()
android·java·java-ee·kotlin·android studio·android-studio·android runtime
李艺为6 小时前
展锐相机DreamCamera2模式精简
android·java