跨进程回调:Server 如何向 Client 发起反向调用?
好,我们继续聊 Binder 的另一个经典场景——跨进程回调。说白了,就是 Server 端怎么主动给 Client 端发消息。
常规的 Binder 调用,都是 Client 主动发起请求,Server 处理完返回结果。但有些场景下,Server 需要主动通知 Client,比如:
- 下载进度更新
- 传感器数据变化
- 服务状态变更
这时候就需要跨进程回调了。我刚开始接触这个机制时,总觉得有点绕——明明是 Client 调 Server,怎么反过来也能调?
回调的本质:Client 把自己的 Binder 对象传给 Server
跨进程回调的核心思想其实很简单:Client 在调用 Server 时,顺便把自己的一个 Binder 对象作为参数传过去。Server 把这个对象保存下来,等需要通知 Client 时,就拿着这个对象发起反向调用。
你想想看,Binder 驱动的核心能力就是跨进程传递 Binder 引用。Client 能拿到 Server 的 Binder 引用,反过来 Server 也能拿到 Client 的 Binder 引用。这不就双向打通了吗?
关键点:回调不是 Binder 的特殊机制,而是利用 Binder 已有的跨进程对象传递能力实现的模式。
标准实现流程
我习惯把回调的实现分成三步:
- 定义回调接口——在 AIDL 中声明一个 callback 接口
- Client 实现并注册——Client 创建回调对象,传给 Server
- Server 保存并调用——Server 持有回调引用,在需要时发起反向调用
来看一个具体的例子。假设我们有一个下载服务,需要实时通知进度:
// IDownloadCallback.aidl
interface IDownloadCallback {
void onProgress(int percent);
void onComplete(String filePath);
void onError(int errorCode, String message);
}
// IDownloadService.aidl
interface IDownloadService {
void startDownload(String url, IDownloadCallback callback);
void cancelDownload(String url);
}
这里 IDownloadCallback 就是回调接口。Client 调用 startDownload 时,把自己的回调对象传进去。
Server 端的实现细节
Server 端收到回调对象后,需要妥善管理。我见过不少新手直接把回调对象扔在一个成员变量里,结果多个 Client 同时调用时,回调就被覆盖了。
public class DownloadService extends IDownloadService.Stub {
// 用 Map 管理多个回调,key 是下载任务 ID
private final Map<String, IDownloadCallback> mCallbacks = new ConcurrentHashMap<>();
@Override
public void startDownload(String url, IDownloadCallback callback) {
String taskId = generateTaskId(url);
mCallbacks.put(taskId, callback);
// 启动下载线程
new DownloadTask(taskId, url, callback).start();
}
@Override
public void cancelDownload(String url) {
String taskId = generateTaskId(url);
mCallbacks.remove(taskId);
// 取消下载...
}
// 下载线程内部调用回调
private class DownloadTask extends Thread {
private String mTaskId;
private String mUrl;
private IDownloadCallback mCallback;
void onProgress(int percent) {
try {
mCallback.onProgress(percent);
} catch (DeadObjectException e) {
// Client 进程挂了,移除回调
mCallbacks.remove(mTaskId);
} catch (RemoteException e) {
// 其他异常处理
}
}
}
}
注意:回调调用必须放在 try-catch 中。Client 进程随时可能崩溃或退出,这时调用回调会抛出 DeadObjectException。我曾经在一个项目中忘了处理这个异常,结果 Server 端频繁崩溃,排查了好久才发现是回调调用没做保护。
回调的线程模型
这里有个容易踩坑的地方——回调在哪个线程执行?
Server 调用回调时,Binder 驱动会把调用请求发送到 Client 进程的 Binder 线程池。也就是说,回调是在 Client 的 Binder 线程中执行的,而不是在 Server 的线程中。
这意味着什么?如果你在回调里更新 UI,必须 post 到主线程。我见过有人直接在回调里操作 UI,结果闪退,还以为是 Binder 的问题。
// Client 端实现回调
IDownloadCallback.Stub mCallback = new IDownloadCallback.Stub() {
@Override
public void onProgress(int percent) {
// 注意:这里运行在 Binder 线程!
// 不能直接更新 UI
mHandler.post(() -> {
mProgressBar.setProgress(percent);
});
}
@Override
public void onComplete(String filePath) {
mHandler.post(() -> {
Toast.makeText(mContext, "下载完成", Toast.LENGTH_SHORT).show();
});
}
};
回调的生命周期管理
嗯,这里要重点说一下。回调对象是跨进程的,它的生命周期管理比普通对象复杂得多。
| 场景 | 处理方式 |
|---|---|
| Client 主动注销 | 调用 removeCallback 或类似接口 |
| Client 进程死亡 | Server 通过 linkToDeath 监听,自动清理 |
| Server 进程死亡 | Client 的 Binder 代理变成死亡状态,调用会抛异常 |
| 回调调用超时 | Binder 默认有超时机制,超时后抛出 TransactionTimeoutException |
我个人建议,Server 端一定要实现 linkToDeath 监听。这样当 Client 进程意外死亡时,Server 能及时清理回调引用,避免内存泄漏。
// Server 端注册死亡监听
IBinder.DeathRecipient deathRecipient = new IBinder.DeathRecipient() {
@Override
public void binderDied() {
// Client 进程挂了,清理回调
mCallbacks.values().removeIf(cb -> !cb.asBinder().isBinderAlive());
}
};
// 在注册回调时
callback.asBinder().linkToDeath(deathRecipient, 0);
回调的性能考量
跨进程回调毕竟涉及两次 Binder 调用(注册 + 回调),性能肯定比本地调用差。我在做性能优化时总结了几点:
- 合并回调:如果回调频率很高(比如每 10ms 一次),可以考虑批量回调,减少跨进程次数
- 使用 oneway:如果回调不需要返回值,声明为 oneway 可以提升性能
- 避免在回调中做耗时操作:回调在 Client 的 Binder 线程中执行,阻塞会影响其他 Binder 调用
// oneway 回调,不需要等待返回值
oneway interface IDownloadCallback {
void onProgress(int percent);
void onComplete(String filePath);
}
小技巧:如果回调数据量较大(比如传输图片),可以考虑用共享内存 + 文件描述符的方式,避免 Binder 传输大数据。Binder 的传输上限是 1MB,超过会抛 TransactionTooLargeException。
回调的典型应用场景
跨进程回调在 Android 系统中随处可见:
- ContentObserver:观察 ContentProvider 数据变化
- BroadcastReceiver:接收系统广播
- ServiceConnection:监听 Service 连接状态
- 自定义回调:比如我们上面实现的下载进度回调
我记得有一次做即时通讯应用,需要 Server 端实时推送消息给 Client。当时就是用回调机制实现的——Client 登录时注册回调,Server 收到新消息后通过回调推送给 Client。这个方案比轮询高效得多。
SVG 流程图:跨进程回调的完整调用链
从流程图可以看出,整个回调过程分为三个阶段:
- 注册阶段(蓝色箭头):Client 将回调 Stub 传给 Server,Server 保存为 Proxy
- 保存阶段(橙色箭头):Server 内部保存回调引用,等待触发
- 回调阶段(橙色箭头):Server 通过 Proxy 发起反向调用,Binder 驱动转发到 Client 的 Stub
总结
跨进程回调说白了就是 Client 把自己的 Binder 对象借给 Server 用。这个机制让 Binder 从单向通信变成了双向通信,非常灵活。
实际开发中,我建议你注意三点:
- 回调调用一定要 try-catch,处理 Client 死亡的情况
- 回调执行在 Binder 线程,更新 UI 要切到主线程
- Server 端要做好回调的生命周期管理,避免内存泄漏
掌握了这些,跨进程回调对你来说就不是什么难事了。