不用 AIDL,手把手教你手写 Binder 代理类

前言: 在日常 Android 开发中,我们习惯了用 AIDL 来定义跨进程接口。但 AIDL 本质上只是一个代码生成工具,它自动生成的 Stub 和 Proxy 类完全可以手写。理解手写 Binder 的过程,能帮助你深入掌握 Android IPC 机制的核心原理。


一、Binder 通信整体架构

在开始手写代码之前,我们先搞清楚 Binder 通信的核心角色。一次完整的跨进程调用,涉及四个关键组件:

scss 复制代码
客户端 App
    ↓ transact()
Proxy(代理类)
    ↓ Binder Driver
Stub(存根类)
    ↓ onTransact()
服务端实现

💡 核心原理: Proxy 负责把方法调用「打包」成 Parcel 数据,通过 Binder 驱动发送到服务端;Stub 负责「解包」Parcel,调用真正的业务实现,再把结果「打包」返回给客户端。


二、项目结构准备

本文的示例分为服务端和客户端两个模块。手写 Binder 不需要 AIDL 文件,但需要手动维护接口一致性。

scss 复制代码
📁 server (服务端)
  📄 IMyManualService.java --- 接口定义
  📄 MyManualServiceStub.java --- 存根抽象类
  📄 MyManualServiceProxy.java --- 代理实现类
  📄 ManualBinderService.java --- 服务端 Service
​
📁 client (客户端)
  📄 MainActivity.kt --- 绑定服务并调用

⚠️ 注意事项: 客户端需要将 IMyManualServiceMyManualServiceStubMyManualServiceProxy 这三个文件原样复制到客户端工程中,保证两端接口完全一致。


三、定义接口:IMyManualService

首先定义 AIDL 中常见的 IInterface 接口。这个接口包含两个核心要素:

  • DESCRIPTOR:Binder 服务的唯一标识符,用于客户端查询本地接口
  • 业务方法:定义服务端暴露给客户端调用的方法

IMyManualService.java

java 复制代码
package com.qhsoft.foodsafety.module;
​
import android.os.IInterface;
import android.os.RemoteException;
​
public interface IMyManualService extends IInterface {
    // 唯一标识符,用于区分不同的 Binder 服务
    // 这个字符串必须全局唯一,建议用包名+接口名的全限定名
    String DESCRIPTOR = "com.qhsoft.foodsafety.module.IMyManualService";
​
    // 定义远程方法:两数相加
    int add(int a, int b) throws RemoteException;
​
    // 定义远程方法:打招呼
    String sayHello(String name) throws RemoteException;
}

💡 为什么继承 IInterface? IInterface 是 Android 为 Binder 接口定义的基础接口,它只有一个方法 asBinder(),用于获取底层的 IBinder 对象。所有 AIDL 生成的接口都继承自它。


四、手写 Stub:MyManualServiceStub

Stub 是 Binder 通信的服务端入口 。它继承 Binder 并实现自定义接口,负责接收客户端发来的请求,并分发给具体的业务实现。

Stub 的核心职责

  1. 定义事务码(TRANSACTION_code),每个方法对应一个唯一整型 ID
  2. 实现 asInterface(),判断同进程还是跨进程
  3. 重写 onTransact(),解析客户端数据并调用本地实现

MyManualServiceStub.java

java 复制代码
package com.qhsoft.foodsafety.module;
​
import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import android.os.Parcel;
import android.os.RemoteException;
​
public abstract class MyManualServiceStub extends Binder implements IMyManualService {
​
    // 事务码:客户端和服务端必须完全一致
    // 这里的 1、2 就是方法的"身份证"
    static final int TRANSACTION_add = 1;
    static final int TRANSACTION_sayHello = 2;
​
    public MyManualServiceStub() {
        // 将当前 Binder 对象和描述符绑定
        // 这样客户端 queryLocalInterface 时才能找到它
        this.attachInterface(this, DESCRIPTOR);
    }
​
    @Override
    public IBinder asBinder() {
        return this;
    }
​
    /**
     * 将 IBinder 转换为接口对象
     * 这是 Binder 通信的"路由分发器":
     * - 同进程:直接返回本地 Stub 对象(性能最优,不走 IPC)
     * - 跨进程:创建 Proxy 代理对象(通过 Binder 驱动通信)
     */
    public static IMyManualService asInterface(IBinder obj) {
        if (obj == null) return null;
​
        // 先尝试获取本地接口
        // 如果 obj 是本地对象(同一进程),直接返回
        IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
        if (iin != null && iin instanceof IMyManualService) {
            return (IMyManualService) iin;
        }
​
        // 跨进程:返回代理对象
        return new MyManualServiceProxy(obj);
    }
​
    @Override
    protected boolean onTransact(int code, Parcel data, Parcel reply, int flags)
            throws RemoteException {
        switch (code) {
            // 标准接口查询事务
            case INTERFACE_TRANSACTION:
                reply.writeString(DESCRIPTOR);
                return true;
​
            case TRANSACTION_add:
                // Step 1: 验证接口标识,防止恶意调用
                data.enforceInterface(DESCRIPTOR);
​
                // Step 2: 按顺序读取参数(顺序必须与 Proxy 写入一致!)
                int arg0 = data.readInt();
                int arg1 = data.readInt();
​
                // Step 3: 执行实际业务逻辑
                // this.add() 会调用到子类(匿名内部类)的实现
                int result = this.add(arg0, arg1);
​
                // Step 4: 写入返回值
                reply.writeNoException();  // 先标记无异常
                reply.writeInt(result);     // 再写入结果
                return true;
​
            case TRANSACTION_sayHello:
                data.enforceInterface(DESCRIPTOR);
                String str = data.readString();
                String resultStr = this.sayHello(str);
                reply.writeNoException();
                reply.writeString(resultStr);
                return true;
        }
​
        // 不认识的事务码,交给父类处理
        return super.onTransact(code, data, reply, flags);
    }
}

关键要点: onTransact() 中的参数读取顺序和 Proxy 中的写入顺序必须完全一致,否则会出现数据错位,导致莫名其妙的崩溃或数据错误。


五、手写 Proxy:MyManualServiceProxy

Proxy 运行在客户端进程,它是服务端 Stub 的「远程代理人」。当客户端调用接口方法时,实际上是由 Proxy 将参数打包(序列化),通过 Binder 驱动发送给服务端。

Proxy 的核心职责

  1. 持有远程 IBinder 引用(mRemote
  2. 将方法参数写入 Parcel(序列化)
  3. 调用 transact() 发起跨进程通信
  4. Parcel 读取返回值(反序列化)

MyManualServiceProxy.java

java 复制代码
package com.qhsoft.foodsafety.module;
​
import android.os.IBinder;
import android.os.Parcel;
import android.os.RemoteException;
​
public class MyManualServiceProxy implements IMyManualService {
    // 远程 Binder 对象的引用
    // 所有跨进程调用都通过它转发
    private IBinder mRemote;
​
    public MyManualServiceProxy(IBinder remote) {
        mRemote = remote;
    }
​
    @Override
    public IBinder asBinder() {
        return mRemote;
    }
​
    @Override
    public int add(int a, int b) throws RemoteException {
        // _data:  存放输入参数(从客户端到服务端)
        // _reply: 存放返回结果(从服务端到客户端)
        Parcel _data = Parcel.obtain();
        Parcel _reply = Parcel.obtain();
        int _result;
​
        try {
            // 1. 写入接口标识符
            // 服务端会用它做 enforceInterface 校验
            _data.writeInterfaceToken(DESCRIPTOR);
​
            // 2. 写入方法参数(顺序很重要!)
            _data.writeInt(a);
            _data.writeInt(b);
​
            // 3. 发起远程调用,阻塞等待结果
            // 参数说明:
            //   TRANSACTION_add --- 事务码,告诉服务端要调用哪个方法
            //   _data            --- 输入参数
            //   _reply           --- 输出结果
            //   0                --- flags,0 表示同步调用
            mRemote.transact(MyManualServiceStub.TRANSACTION_add, _data, _reply, 0);
​
            // 4. 读取异常标记(如果服务端抛了异常,这里会重新抛出)
            _reply.readException();
​
            // 5. 读取返回值
            _result = _reply.readInt();
        } finally {
            // 6. 必须回收 Parcel,防止内存泄漏
            _reply.recycle();
            _data.recycle();
        }
​
        return _result;
    }
​
    @Override
    public String sayHello(String name) throws RemoteException {
        Parcel _data = Parcel.obtain();
        Parcel _reply = Parcel.obtain();
        String _result;
​
        try {
            _data.writeInterfaceToken(DESCRIPTOR);
            _data.writeString(name);
​
            mRemote.transact(
                MyManualServiceStub.TRANSACTION_sayHello,
                _data, _reply, 0
            );
​
            _reply.readException();
            _result = _reply.readString();
        } finally {
            _reply.recycle();
            _data.recycle();
        }
​
        return _result;
    }
}

⚠️ 容易踩的坑: Parcel.obtain() 获取的对象必须在 finally 块中调用 recycle() 回收。如果忘记回收,会造成内存泄漏,在高频调用场景下尤为危险。


六、服务端 Service 实现

有了 Stub 和 Proxy,接下来需要在服务端创建一个 Service,在 onBind() 中返回 Stub 的实例。

ManualBinderService.java

java 复制代码
package com.qhsoft.foodsafety.module;
​
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
​
public class ManualBinderService extends Service {
    private static final String TAG = "ManualBinderService";
​
    // 创建一个 Stub 的匿名实现类
    // 这里写真正的业务逻辑
    private final MyManualServiceStub mBinder = new MyManualServiceStub() {
​
        @Override
        public int add(int a, int b) throws RemoteException {
            Log.d(TAG, "服务端收到请求: " + a + " + " + b);
            return a + b;
        }
​
        @Override
        public String sayHello(String name) throws RemoteException {
            Log.d(TAG, "服务端收到请求: greeting " + name);
            return "Hello, " + name + "! (Manual Binder)";
        }
    };
​
    @Override
    public IBinder onBind(Intent intent) {
        // 返回 Binder 对象给客户端
        // 客户端收到的就是 Stub 本身(同进程)或 Binder 驱动的代理
        return mBinder;
    }
}

在 AndroidManifest.xml 中注册

为了让客户端能够找到并绑定这个服务,需要在清单文件中声明,并指定一个唯一的 action

xml 复制代码
<!-- 服务端 (Server) 的 AndroidManifest.xml -->
<service
    android:name="com.qhsoft.foodsafety.module.ManualBinderService"
    android:enabled="true"
    android:exported="true">  <!-- Android 11+ 需注意可见性,设为 true 允许外部访问 -->
​
    <intent-filter>
        <!-- 定义一个唯一的 Action 标识符 -->
        <!-- 客户端就靠这个 action 来找到服务 -->
        <action android:name="com.qhsoft.foodsafety.module.MANUAL_BINDER_SERVICE" />
    </intent-filter>
</service>

⚠️ 安全提示: android:exported="true" 会让其他应用也能绑定你的服务。生产环境中建议配合 android:permission 进行权限控制,防止任意应用访问。


七、客户端绑定与调用

客户端需要把服务端的三个文件(IMyManualServiceMyManualServiceStubMyManualServiceProxy)复制到客户端工程中,然后通过 bindService() 绑定远程服务。

MainActivity.kt

kotlin 复制代码
private var manualService: IMyManualService? = null
private var bound = false
​
private val mConnection: ServiceConnection = object : ServiceConnection {
​
    override fun onServiceConnected(name: ComponentName, service: IBinder) {
        // 跨进程连接成功:
        // asInterface 内部判断:
        //   - 如果 service 是本地 Binder(同一进程),返回 Stub 本身
        //   - 如果 service 是远程 Binder(跨进程),创建 Proxy 包装它
        manualService = MyManualServiceStub.asInterface(service)
        bound = true
​
        try {
            // 像调用本地方法一样调用远程方法
            // 实际底层走的是 Binder 驱动 + IPC
            Log.d(TAG, "sayHello => " + manualService?.sayHello("sorting"))
            Log.d(TAG, "add(10, 5) => " + manualService?.add(10, 5))
        } catch (e: RemoteException) {
            // 跨进程通信可能失败,必须捕获 RemoteException
            e.printStackTrace()
        }
    }
​
    override fun onServiceDisconnected(name: ComponentName) {
        Log.w("Client", "Service 意外断开连接")
        bound = false
        manualService = null
    }
}
​
// 构建 Intent 并绑定服务
val intent = Intent().apply {
    action = "com.qhsoft.foodsafety.module.MANUAL_BINDER_SERVICE"
    `package` = "com.qhsoft.foodsafety"  // 服务端应用包名
}
bindService(intent, mConnection, Context.BIND_AUTO_CREATE)

💡 Kotlin 小技巧: 在 Kotlin 中,package 是关键字,所以需要用反引号包裹:package。这样可以精确指定服务端应用的包名,避免 Intent 被其他应用拦截。


八、流程总结

让我们以 add(10, 5) 为例,回顾一次完整调用的全过程:

步骤 位置 动作
1 客户端 调用 manualService?.add(10, 5),实际进入 MyManualServiceProxy.add()
2 Proxy _data.writeInt(10)_data.writeInt(5) 打包参数
3 Proxy mRemote.transact(TRANSACTION_add, ...) 发起 IPC
4 Binder Driver _data 拷贝到服务端进程
5 Stub onTransact() 被触发,data.readInt() 读出 10、5
6 Stub 调用 this.add(10, 5),执行真正业务逻辑,得到 15
7 Stub reply.writeInt(15),返回结果
8 Binder Driver reply 拷贝回客户端进程
9 Proxy _reply.readInt() 得到 15,返回给调用方

九、写在最后

手写 Binder 的价值不在于替代 AIDL,而在于理解原理。当你遇到以下场景时,手写 Binder 的知识会派上用场:

  • 需要优化 IPC 性能,想减少一次不必要的拷贝
  • AIDL 生成的代码出了 bug,需要排查
  • 实现自定义的 Binder 对象传递逻辑
  • 面试中被问到 Binder 底层原理 😏
相关推荐
mmsx2 小时前
osmdroid 地图实战 03|地图的"分层世界":离线方案、图层模型与 Overlay 体系
android·前端
张文是假的啊3 小时前
IOC依赖注入问题:@Primary 和 @Qualifier的使用
android
换元不配限3 小时前
ConstraintLayout核心用法详解(三):引导线、屏障、组和占位
android·placeholder·barrier·约束布局·guideline
雨白4 小时前
Kotlin 泛型进阶:攻克型变与实化,直击痛点
kotlin
心平气和量大福大4 小时前
android studio(AS)工具-调试-统计总行数
android·ide·android studio
你的坚定6 小时前
SurfaceFlinger合成一帧的完整流程
android
爱笑鱼7 小时前
Android 系统启动机制(七):进入 SystemServer.main() 以后,system_server 还是一个空进程吗?
android·linux
好安静8 小时前
SurfaceFlinger 合成一帧的完整流程
android
alexhilton8 小时前
架构测试为何需要双视图
android·kotlin·android jetpack