跨进程回调: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 已有的跨进程对象传递能力实现的模式。

标准实现流程

我习惯把回调的实现分成三步:

  1. 定义回调接口——在 AIDL 中声明一个 callback 接口
  2. Client 实现并注册——Client 创建回调对象,传给 Server
  3. 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 进程 Server 进程 Client App 回调 Stub Binder 驱动 Server Service 回调 Proxy Binder 驱动 ① 注册回调 (startDownload) ② 保存引用 ③ 发起回调 (onProgress) 转发 转发 注册流程 回调流程

从流程图可以看出,整个回调过程分为三个阶段:

  • 注册阶段(蓝色箭头):Client 将回调 Stub 传给 Server,Server 保存为 Proxy
  • 保存阶段(橙色箭头):Server 内部保存回调引用,等待触发
  • 回调阶段(橙色箭头):Server 通过 Proxy 发起反向调用,Binder 驱动转发到 Client 的 Stub

总结

跨进程回调说白了就是 Client 把自己的 Binder 对象借给 Server 用。这个机制让 Binder 从单向通信变成了双向通信,非常灵活。

实际开发中,我建议你注意三点:

  • 回调调用一定要 try-catch,处理 Client 死亡的情况
  • 回调执行在 Binder 线程,更新 UI 要切到主线程
  • Server 端要做好回调的生命周期管理,避免内存泄漏

掌握了这些,跨进程回调对你来说就不是什么难事了。


专注资料整理