48. 跨进程回调:RemoteCallbackList 的实现原理与使用场景

跨进程回调,说白了就是服务端主动给客户端发消息。你想想看,如果客户端只是单向调用服务端,那用 Binder 直接调就行了。但现实场景往往是——客户端发起一个请求,服务端处理完了,或者处理过程中有状态变化,需要通知客户端。这时候就需要回调机制了。

我最早接触这个,是在做一个后台下载服务。客户端发起下载请求,服务端下载进度变了,得实时通知客户端更新进度条。嗯,这时候 RemoteCallbackList 就派上用场了。

为什么需要 RemoteCallbackList?

你可能会想:不就是跨进程传个回调对象吗?直接用 Binder 传一个 Listener 接口不就行了?

理论上可以,但实际坑很多。我遇到过几个典型问题:

  • 客户端挂了怎么办? 客户端进程被杀,服务端还持有它的 Binder 代理,调用时直接抛 DeadObjectException。
  • 回调注册了没注销? 客户端重复注册,服务端列表里一堆重复对象,回调时重复通知。
  • 线程安全问题 多个客户端同时注册/注销,服务端同时遍历回调列表,不加锁直接崩。

RemoteCallbackList 就是 Android 官方为了解决这些问题而设计的工具类。它内部封装了 Binder 死亡通知、线程安全、自动清理等机制。

核心原理剖析

RemoteCallbackList 的源码其实不长,但设计得很精巧。我拆开来讲:

1. 数据结构:ArrayMap + IBinder 死亡通知

它内部维护了一个 ArrayMap<IBinder, Callback>。key 是客户端的 Binder 代理对象,value 是回调接口的包装类。为什么用 IBinder 做 key?因为每个客户端的 Binder 代理是唯一的,而且可以通过它注册死亡通知。

// 伪代码示意
ArrayMap<IBinder, Callback> mCallbacks = new ArrayMap<>();

public boolean register(E callback) {
    IBinder binder = callback.asBinder();
    // 注册死亡通知
    binder.linkToDeath(mDeathRecipient, 0);
    mCallbacks.put(binder, callback);
    return true;
}

当客户端进程死亡时,Binder 驱动会通知服务端,RemoteCallbackList 内部收到死亡通知后,自动移除对应的回调。这就避免了调用已死进程的 Binder 代理。

2. 线程安全:隐式锁

RemoteCallbackList 内部使用了一个 CopyOnWriteArrayList 的思想?不,它用的是 synchronized 块。所有注册、注销、遍历操作都加了锁。但这里有个巧妙的设计——遍历时,它会把回调列表拷贝一份出来,然后释放锁,再遍历拷贝的列表。

public void broadcast(SomeData data) {
    int N = beginBroadcast();
    try {
        for (int i = 0; i < N; i++) {
            E callback = getBroadcastItem(i);
            try {
                callback.onEvent(data);
            } catch (DeadObjectException e) {
                // 客户端已死,忽略
            }
        }
    } finally {
        finishBroadcast();
    }
}

注意 beginBroadcast()finishBroadcast() 这对方法。它们内部会加锁拷贝列表,然后释放锁。这样你在遍历回调时,其他线程可以继续注册/注销,不会阻塞。但遍历期间不能修改列表,所以必须成对调用。

我曾经踩过的坑:beginBroadcast()finishBroadcast() 之间调用了 register()unregister(),直接抛 IllegalStateException。记住,这对方法之间是「只读模式」。

3. 自动清理:死亡通知 + 弱引用

除了死亡通知自动移除,RemoteCallbackList 还支持 unregister() 时解除死亡通知。另外,它内部对回调对象持有的是强引用吗?不,它持有的是 Callback 包装类,这个包装类内部持有回调的强引用。但如果你调用 unregister(),它会立即移除。

我个人习惯在客户端 onDestroy()onUnbind() 时主动注销回调,而不是依赖死亡通知。因为死亡通知有延迟,而且如果服务端和客户端在同一进程(调试时),死亡通知不会触发。

使用场景

我总结了几种典型场景:

场景 说明 示例
进度通知 服务端执行耗时操作,实时通知客户端进度 下载服务、文件拷贝、数据同步
状态变化 服务端内部状态改变,通知所有客户端 连接状态、设备插拔、配置变更
事件广播 服务端产生事件,广播给多个客户端 消息推送、传感器数据、日志收集
数据订阅 客户端订阅某类数据,服务端有数据时推送 股票行情、实时位置、监控数据

完整使用示例

我写一个典型的 AIDL 回调示例:

// IProgressCallback.aidl
interface IProgressCallback {
    void onProgress(int percent);
    void onComplete();
    void onError(String message);
}

// IDownloadService.aidl
interface IDownloadService {
    void startDownload(String url);
    void registerCallback(IProgressCallback callback);
    void unregisterCallback(IProgressCallback callback);
}

// 服务端实现
public class DownloadService extends Service {
    private RemoteCallbackList<IProgressCallback> mCallbacks = 
            new RemoteCallbackList<>();

    private final IDownloadService.Stub mBinder = new IDownloadService.Stub() {
        @Override
        public void startDownload(String url) {
            // 启动下载线程
            new DownloadTask(url, mCallbacks).start();
        }

        @Override
        public void registerCallback(IProgressCallback callback) {
            mCallbacks.register(callback);
        }

        @Override
        public void unregisterCallback(IProgressCallback callback) {
            mCallbacks.unregister(callback);
        }
    };

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}

// 下载任务中回调
class DownloadTask extends Thread {
    private RemoteCallbackList<IProgressCallback> mCallbacks;

    void notifyProgress(int percent) {
        int N = mCallbacks.beginBroadcast();
        try {
            for (int i = 0; i < N; i++) {
                IProgressCallback cb = mCallbacks.getBroadcastItem(i);
                try {
                    cb.onProgress(percent);
                } catch (DeadObjectException e) {
                    // 客户端已死,忽略
                }
            }
        } finally {
            mCallbacks.finishBroadcast();
        }
    }
}

SVG 结构图:RemoteCallbackList 核心流程

RemoteCallbackList 核心流程 客户端 A register(callback) 客户端 B register(callback) 客户端 C(已死) 进程被杀死 RemoteCallbackList ArrayMap<IBinder, Callback> + 死亡通知监听 + 线程安全锁 + 自动清理 beginBroadcast() 拷贝列表 finishBroadcast() 释放锁 注册 注册 死亡通知 服务端 broadcast() 遍历回调 获取回调列表 客户端死亡 → 自动移除回调 | 遍历时拷贝列表 → 不阻塞注册/注销

避坑指南

我总结几个实际项目中容易踩的坑:

  • 不要忘记 finishBroadcast():beginBroadcast() 和 finishBroadcast() 必须成对出现,否则内部锁不会释放,后续所有注册/注销都会死锁。
  • 回调方法不要耗时:遍历回调是在服务端的 Binder 线程池中执行的,如果某个客户端的回调方法执行时间过长,会阻塞其他客户端的回调。我建议回调方法里只做轻量操作,比如发个消息到主线程。
  • 客户端主动注销:虽然 RemoteCallbackList 会自动清理死去的客户端,但主动注销更及时。我在 onServiceDisconnected() 和 onDestroy() 中都会调用 unregister()。
  • 跨进程回调的异常处理:每个回调调用都要 try-catch DeadObjectException,因为客户端可能在你遍历的过程中挂掉。
我的一个小技巧: 如果回调需要返回结果,可以用 beginBroadcast() 遍历时收集结果,但要注意每个客户端的返回时间。我一般会设置超时机制,避免一个慢客户端拖垮整个服务。

RemoteCallbackList 是 Android 跨进程通信中非常实用的工具。它把 Binder 死亡通知、线程安全、自动清理这些脏活累活都封装好了。你只要记住 beginBroadcast/finishBroadcast 的配对使用,以及回调方法不要耗时,基本就能用好它。

嗯,如果你在项目中遇到多个客户端需要同时监听服务端状态变化的场景,RemoteCallbackList 绝对是首选方案。


专注资料整理