前言: 在日常 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 --- 绑定服务并调用
⚠️ 注意事项: 客户端需要将
IMyManualService、MyManualServiceStub、MyManualServiceProxy这三个文件原样复制到客户端工程中,保证两端接口完全一致。
三、定义接口: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 的核心职责
- 定义事务码(
TRANSACTION_code),每个方法对应一个唯一整型 ID - 实现
asInterface(),判断同进程还是跨进程 - 重写
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 的核心职责
- 持有远程
IBinder引用(mRemote) - 将方法参数写入
Parcel(序列化) - 调用
transact()发起跨进程通信 - 从
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进行权限控制,防止任意应用访问。
七、客户端绑定与调用
客户端需要把服务端的三个文件(IMyManualService、MyManualServiceStub、MyManualServiceProxy)复制到客户端工程中,然后通过 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 底层原理 😏