服务端与客户端实现:理解 Binder 基本流程
到了这一章,咱们就要真刀真枪地写代码了。Binder 通信不是玄学,它本质上就是 服务端暴露接口 → 客户端远程调用 这么一套流程。我个人习惯先把 AIDL 文件写好,再分别实现两端,这样结构最清晰。
1. 先定接口:AIDL 文件
你想想看,两个进程要通信,总得有个统一的“协议”吧?AIDL 就是干这个的。我用一个简单的计算器接口做例子:
// ICalculator.aidl
package com.example.binderdemo;
interface ICalculator {
int add(int a, int b);
}
注意,这里的 int 是基本类型,直接支持。如果要传自定义对象,得实现 Parcelable,那是后话了。AIDL 编译后会生成对应的 Java 文件,里面包含 Stub 和 Proxy,咱们待会儿就会用到。
2. 服务端:继承 Stub,实现接口
服务端的核心是 Service。你需要在 onBind() 方法里返回一个 Binder 对象,这个对象就是客户端远程调用的“门把手”。
// CalculatorService.java
public class CalculatorService extends Service {
private final ICalculator.Stub mBinder = new ICalculator.Stub() {
@Override
public int add(int a, int b) throws RemoteException {
// 这里就是真正的业务逻辑
return a + b;
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
<service android:name=".CalculatorService" android:exported="true" />,否则外部进程访问不了。这里我强调一点:mBinder 是一个匿名内部类,继承了 AIDL 生成的 ICalculator.Stub。AIDL 编译器会自动帮你处理好线程安全、跨进程数据序列化等等。说白了,你只需要关心业务逻辑怎么写就行。
3. 客户端:bindService,拿到代理
客户端要调用远程方法,第一步是绑定 Service。绑定成功后,会得到一个 IBinder,你需要用 ICalculator.Stub.asInterface() 把它“翻译”成你认识的接口。
// ClientActivity.java 片段
private ICalculator mCalculator;
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
// 关键一步:将 IBinder 转换为接口
mCalculator = ICalculator.Stub.asInterface(service);
}
@Override
public void onServiceDisconnected(ComponentName name) {
mCalculator = null;
}
};
// 发起绑定
Intent intent = new Intent();
intent.setComponent(new ComponentName("com.example.binderdemo",
"com.example.binderdemo.CalculatorService"));
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
// 调用远程方法(需要在子线程,因为 Binder 调用可能阻塞)
new Thread(() -> {
try {
int result = mCalculator.add(3, 5);
Log.d("Client", "result = " + result);
} catch (RemoteException e) {
e.printStackTrace();
}
}).start();
注意,onServiceConnected 是在主线程回调的。但远程调用本身应该放在子线程里做,避免阻塞 UI。我在项目中见过有人直接在回调里调用远程方法导致 ANR,后来改成异步才解决。
4. 完整流程梳理
为了让你有个全局概念,我画了一张简易流程图。你可以看到三个关键角色:服务端进程、Binder 驱动(内核)、客户端进程。箭头表示数据流动方向。
5. 关键点总结
| 环节 | 服务端 | 客户端 |
|---|---|---|
| 接口定义 | 实现 AIDL 接口方法 | 无 |
| Binder 对象 | 继承 Stub 生成 | Stub.asInterface() 转换 |
| 生命周期 | Service,需声明 | bind/unbind,注意解绑 |
| 线程模型 | Binder 线程池处理 | 不要在 UI 线程调用 |
6. 一些经验之谈
- 关于 Service 注册: Android 系统级服务会注册到 ServiceManager(现在叫 servicemanager 实例),但普通应用的服务通过 Intent 绑定,不需要你主动去注册。你只要写好
onBind()就行了。 - 关于异常处理: 远程调用可能抛出
RemoteException,比如服务端崩溃或者 Binder 连接断开。我习惯在每个远程调用外面包一层 try-catch,并且打印日志方便定位。 - 小心内存泄漏: 客户端 bindService 后一定要在合适时机
unbindService(),否则 Service 会一直运行。我在项目中见过因为忘了 unBind 导致 Service 常驻,电量狂掉。
小提示: 如果你想调试 Binder 通信,可以在服务端和客户端分别打印线程 ID,观察是否在不同线程中执行。Binder 驱动会使用服务端进程的 Binder 线程池来执行远程方法,所以你的服务端代码必须考虑线程安全。
好了,写到这里你应该能跑通一个最简单的 Binder 示例了。把代码敲一遍,感受一下 IPC 的“透明”感——客户端调用方法就像本地调用一样,但背后其实跨了进程。理解了基本流程,后续咱们聊异常 Mock 和回放才有基础。