27、Binder 的跨进程回调:如何通过 Binder 实现 Listener 或 Callback 模式
各位同学,今天我们来聊一个非常实战的话题——跨进程回调。
你想想看,在单进程里,我们写个接口回调,比如 OnClickListener,那是再自然不过的事了。但一旦跨了进程,事情就没那么简单了。为什么?因为 Binder 是单向的。客户端调用服务端,Binder 可以帮你把数据传过去。但服务端想主动通知客户端?对不起,你得先把客户端的“电话线”递过来。
说白了,跨进程回调的核心就是:客户端把自己的 Binder 对象传给服务端,服务端拿着这个 Binder 反过来调用客户端。
我在项目中遇到过不少同学,写跨进程回调时直接把 Java 接口传过去了,结果跑起来就报错。嗯,这里要特别强调:跨进程传递的必须是 Binder 对象,不是普通的 Java 对象。
27.1 跨进程回调的本质
我们先理清一个概念。在 Android 的 Binder 机制里,回调本质上就是一次反向的 Binder 调用。
- 客户端:持有一个 Binder 对象(回调接口的 Stub 端)
- 服务端:持有这个 Binder 对象的代理端(Proxy)
- 当服务端想通知客户端时,通过 Proxy 发起一次 Binder 调用
这个流程,说白了就是“你给我一根线,我通过这根线喊你”。
核心要点:跨进程回调 = 客户端把自己的 Binder 引用注册到服务端,服务端在需要时通过该引用回调客户端。
27.2 一个完整的跨进程回调示例
我们来看一个实际的例子。假设我们有一个下载服务,客户端想监听下载进度。
27.2.1 定义回调接口(AIDL)
// IDownloadCallback.aidl
package com.example.download;
interface IDownloadCallback {
void onProgress(int percent);
void onComplete(String filePath);
void onError(int errorCode, String message);
}
这个 AIDL 文件编译后,会生成对应的 Stub 和 Proxy。客户端需要实现这个 Stub,服务端通过 Proxy 调用。
27.2.2 服务端接口(AIDL)
// IDownloadService.aidl
package com.example.download;
import com.example.download.IDownloadCallback;
interface IDownloadService {
void startDownload(String url);
void registerCallback(IDownloadCallback callback);
void unregisterCallback(IDownloadCallback callback);
}
注意看,registerCallback 和 unregisterCallback 这两个方法,参数类型就是我们的回调接口。Binder 会自动帮我们把客户端的 Stub 对象跨进程传递到服务端。
27.2.3 客户端实现
// 客户端代码
private IDownloadCallback.Stub mCallback = new IDownloadCallback.Stub() {
@Override
public void onProgress(int percent) throws RemoteException {
// 注意:这里运行在 Binder 线程池,不是 UI 线程
// 需要 post 到主线程更新 UI
mHandler.post(() -> updateProgress(percent));
}
@Override
public void onComplete(String filePath) throws RemoteException {
mHandler.post(() -> showComplete(filePath));
}
@Override
public void onError(int errorCode, String message) throws RemoteException {
mHandler.post(() -> showError(errorCode, message));
}
};
// 绑定服务后注册回调
private ServiceConnection mConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
IDownloadService downloadService = IDownloadService.Stub.asInterface(service);
try {
downloadService.registerCallback(mCallback);
downloadService.startDownload("https://example.com/file.zip");
} catch (RemoteException e) {
e.printStackTrace();
}
}
};
避坑指南:我曾经在项目里犯过一个低级错误——直接在回调方法里更新 UI,结果导致崩溃。记住:Binder 回调运行在 Binder 线程池,不是 UI 线程。一定要通过 Handler 切换到主线程。
27.2.4 服务端实现
// 服务端代码
public class DownloadService extends Service {
private final RemoteCallbackList<IDownloadCallback> mCallbacks = new RemoteCallbackList<>();
private final IDownloadService.Stub mBinder = new IDownloadService.Stub() {
@Override
public void startDownload(String url) throws RemoteException {
// 启动下载线程
new DownloadTask(url, mCallbacks).start();
}
@Override
public void registerCallback(IDownloadCallback callback) throws RemoteException {
mCallbacks.register(callback);
}
@Override
public void unregisterCallback(IDownloadCallback callback) throws RemoteException {
mCallbacks.unregister(callback);
}
};
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
}
这里我用了 RemoteCallbackList,这是 Android 专门为跨进程回调设计的容器。它内部做了 Binder 死亡通知的处理,当客户端进程挂了,会自动移除对应的回调。
重要提醒:不要自己用 ArrayList 或 HashMap 来存 Binder 回调对象。因为 Binder 对象跨进程传递后,每次拿到的可能是不同的代理对象。用 RemoteCallbackList 才能正确管理。
27.3 跨进程回调的完整流程图
下面这张图,我画了跨进程回调的完整流程。你看一遍就能明白整个链路。
27.4 RemoteCallbackList 的妙用
刚才提到了 RemoteCallbackList,我再说说它的几个关键特性:
- 线程安全:内部做了同步处理,多线程环境下安全
- 自动清理:当客户端进程死亡,Binder 驱动会发送死亡通知,
RemoteCallbackList会自动移除对应的回调 - 遍历安全:提供了
beginBroadcast()和finishBroadcast()方法,保证遍历过程中不会出现并发修改异常
使用示例:
// 服务端遍历回调的正确姿势
int count = mCallbacks.beginBroadcast();
for (int i = 0; i < count; i++) {
IDownloadCallback callback = mCallbacks.getBroadcastItem(i);
try {
callback.onProgress(progress);
} catch (RemoteException e) {
// 客户端挂了,RemoteCallbackList 会自动处理
}
}
mCallbacks.finishBroadcast();
个人经验:我习惯在 beginBroadcast() 和 finishBroadcast() 之间尽量少做耗时操作,因为这段时间会持有内部锁。如果回调太多,可以考虑分批处理。
27.5 跨进程回调的注意事项
最后,我总结几个实战中容易踩的坑:
- 回调线程问题:回调方法运行在 Binder 线程池,不要直接更新 UI
- 回调注册时机:确保在服务连接成功后再注册回调,不要在
onServiceConnected之前调用 - 反注册回调:在
onServiceDisconnected或onDestroy中反注册,避免内存泄漏 - Binder 对象缓存:不要缓存服务端返回的 Binder 代理对象,每次使用前重新获取
- 回调超时:Binder 调用有默认超时(通常 1 分钟),如果回调处理太慢会触发 ANR
我曾经踩过的坑:有一次在回调里做了网络请求,结果客户端 ANR 了。后来才意识到,Binder 回调的超时机制和普通 Binder 调用是一样的。回调里千万别做耗时操作,实在要做就开个线程。
好了,跨进程回调的核心内容就这些。说白了就是:客户端把自己的 Binder 对象传给服务端,服务端拿着这个对象反过来调用客户端。记住用 RemoteCallbackList 管理回调,注意线程切换,基本就不会出大问题。
公众号:蓝海资料掘金营,微信deep3321