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);
}

注意看,registerCallbackunregisterCallback 这两个方法,参数类型就是我们的回调接口。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 跨进程回调的完整流程图

下面这张图,我画了跨进程回调的完整流程。你看一遍就能明白整个链路。

跨进程回调流程图 客户端进程 服务端进程 客户端代码 调用 registerCallback(mCallback) IDownloadCallback.Stub Binder 实体对象(onTransact) Binder 驱动 将 Stub 引用传递到服务端 RemoteCallbackList 存储回调代理对象 IDownloadCallback.Proxy Binder 代理对象(transact) 服务端业务逻辑 下载完成时调用 callback.onComplete() ① 注册回调 ② 回调 onComplete() 注:箭头①和②分别代表注册和回调两个方向的数据流

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 跨进程回调的注意事项

最后,我总结几个实战中容易踩的坑:

  1. 回调线程问题:回调方法运行在 Binder 线程池,不要直接更新 UI
  2. 回调注册时机:确保在服务连接成功后再注册回调,不要在 onServiceConnected 之前调用
  3. 反注册回调:在 onServiceDisconnectedonDestroy 中反注册,避免内存泄漏
  4. Binder 对象缓存:不要缓存服务端返回的 Binder 代理对象,每次使用前重新获取
  5. 回调超时:Binder 调用有默认超时(通常 1 分钟),如果回调处理太慢会触发 ANR

我曾经踩过的坑:有一次在回调里做了网络请求,结果客户端 ANR 了。后来才意识到,Binder 回调的超时机制和普通 Binder 调用是一样的。回调里千万别做耗时操作,实在要做就开个线程。

好了,跨进程回调的核心内容就这些。说白了就是:客户端把自己的 Binder 对象传给服务端,服务端拿着这个对象反过来调用客户端。记住用 RemoteCallbackList 管理回调,注意线程切换,基本就不会出大问题。


公众号:蓝海资料掘金营,微信deep3321