简介:这份源码资源围绕 Android AIDL 跨进程通信展开,面向需要实现多客户端调用同一服务端的开发者,适合已掌握 Android 基础、希望深入 IPC 与 Binder 机制的中级学习者。包内共 92 个文件,以 class、java、xml、aidl 等源码与配置类型为主,辅以 png 界面截图、apk 安装包、dex 与 jar 依赖,压缩包约 235KB,完整呈现了服务端与多个客户端工程的组织方式。资源通过 T1、Service、T2 三个模块演示 AIDL 接口定义、Stub 实现、bindService 绑定与远程方法调用,并涉及多客户端并发调用时的线程安全与资源调度问题,可帮助读者理解 Binder 生成机制、RemoteException 处理及跨进程调用性能优化思路。目前已有 1050 人学习下载,适合作为 AIDL 多客户端通信的对照参考与调试排错素材。
1. 从一次跨进程回调翻车说起:这个 AIDL 多客户端 Demo 到底能干什么
前阵子帮一个做车机项目的朋友排查问题,场景很典型:一个 Service 跑在独立进程里管设备状态,同时有三四个客户端(仪表、中控、语音助手)都要连它。结果语音助手一断开,中控那边的回调也跟着哑了,日志里全是DeadObjectException。翻源码翻了半天,最后发现是服务端用了一个全局的IInterface引用去存回调,谁后注册就把前面的覆盖了。这类「多个客户端调一个服务器」的坑,几乎每个做 Android 跨进程通信的人都会踩一遍。
这份源码就是冲着这个场景来的:一个标准的 Android AIDL 多客户端调用单服务端的完整工程。它把「一个 Service 如何同时服务多个客户端、如何分别管理每个客户端的回调、客户端断开后如何清理」这几件事用可运行的代码讲清楚了。适合两类人:一是刚接触 AIDL、想找个能跑起来的最小闭环的新手;二是已经在做多进程架构、需要一份回调管理参考实现的熟手。下面我按「能干什么 → 怎么跑 → 怎么改」的顺序,把这份源码拆开讲。
2. 工程结构与通信模型:AIDL 多客户端到底怎么组织
2.1 目录结构与模块划分
拿到源码先别急着点运行,先把目录扫一遍。这类 AIDL 多客户端 Demo 的工程结构通常长这样,我按实际拆包的经验还原一下:
AidlMultiClient/ ├── app/ # 客户端宿主 App │ ├── src/main/ │ │ ├── java/com/example/client/ │ │ │ ├── MainActivity.java # 绑定服务、发起调用 │ │ │ └── ClientCallbackImpl.java # 客户端侧回调实现 │ │ ├── aidl/com/example/service/ │ │ │ ├── IRemoteService.aidl # 主接口:注册/反注册/业务方法 │ │ │ └── ICallback.aidl # 回调接口 │ │ └── AndroidManifest.xml ├── server/ # 服务端模块(可独立进程) │ ├── src/main/ │ │ ├── java/com/example/server/ │ │ │ ├── RemoteService.java # Service 实现 │ │ │ └── CallbackManager.java # 回调注册表(核心) │ │ ├── aidl/com/example/service/ # 与客户端保持同一份 aidl │ │ └── AndroidManifest.xml └── build.gradle两个关键点:第一,aidl目录下的包名和文件名在客户端、服务端必须完全一致,否则编译期生成的 Stub 对不上,运行时会直接抛Binder类型转换异常;第二,服务端单独拆成一个模块,是为了让它能跑在独立进程里,真正模拟跨进程场景。如果两个模块塞进同一个 App,android:process属性也能达到同样效果,但拆模块更接近真实工程。
2.2 两个 AIDL 接口的职责边界
这份源码里有两个.aidl文件,分工很明确。主接口负责「客户端 → 服务端」的调用,回调接口负责「服务端 → 客户端」的反向通知:
// IRemoteService.aidl package com.example.service; import com.example.service.ICallback; interface IRemoteService { // 客户端注册回调,传入唯一标识 void registerCallback(String clientId, ICallback cb); // 客户端反注册,避免服务端持有无效引用 void unregisterCallback(String clientId); // 一个普通业务方法,演示同步调用 int getDeviceStatus(); }// ICallback.aidl package com.example.service; interface ICallback { // 服务端主动通知客户端状态变化 void onStatusChanged(int status); }registerCallback里那个clientId是整份源码的关键设计。很多新手写 AIDL 回调时只传一个ICallback,服务端拿List存起来,结果客户端断开后列表里全是死引用,一通知就崩。这里用clientId做键,配合RemoteCallbackList或自定义 Map,才能做到「谁注册谁负责、谁断开谁清理」。参数cb是跨进程传过来的 Binder 代理,服务端拿到的不是客户端对象本身,而是一个可以反向调用的句柄,这一点后面讲回调管理时还会展开。
2.3 一次完整调用的链路
把链路串起来看:客户端bindService拿到IRemoteService.Stub.asInterface(binder),调用registerCallback,这个调用被 Binder 驱动序列化后传到服务端进程,服务端RemoteService的Stub实现收到请求,把clientId和cb存进回调表。之后服务端状态一变,遍历回调表,对每个cb调onStatusChanged,请求再反向跨进程回到对应客户端。
整条链路里,客户端和服务端各自跑在自己的进程,共享的只有 Binder 驱动和那份 AIDL 契约。理解这一点,后面所有「为什么断开要反注册」「为什么回调要判空」的问题就都有答案了。
3. 服务端实现:回调注册表与 RemoteCallbackList 的正确用法
3.1 RemoteCallbackList 为什么比 ArrayList 靠谱
先看服务端最核心的回调管理。新手最容易写成这样:
// 错误示范:用普通 List 存回调 private final List<ICallback> callbacks = new ArrayList<>(); @Override public void registerCallback(String clientId, ICallback cb) { callbacks.add(cb); // 客户端断开后这里就是死引用 }问题在于,ArrayList存的是 Binder 代理的强引用,客户端进程一挂,这个代理就失效了,但列表里还留着,下次遍历通知直接抛DeadObjectException。Android 专门为这个场景提供了RemoteCallbackList,它内部用IBinder的linkToDeath监听客户端死亡,自动把失效的回调从列表里摘掉:
// 正确做法:RemoteCallbackList 自动处理客户端死亡 private final RemoteCallbackList<ICallback> callbackList = new RemoteCallbackList<>(); @Override public void registerCallback(String clientId, ICallback cb) { if (cb != null) { callbackList.register(cb); // 内部绑定死亡通知 } } @Override public void unregisterCallback(String clientId, ICallback cb) { if (cb != null) { callbackList.unregister(cb); } }register和unregister是成对的,RemoteCallbackList内部维护了一个ArrayMap<IBinder, Callback>,key 是回调底层的IBinder,所以同一个客户端重复注册不会产生重复项。这里有个细节:unregister的入参必须是注册时那个cb对象,不能新建一个,否则IBinder对不上,摘不掉。
3.2 遍历通知时的 beginBroadcast / finish 配对
RemoteCallbackList遍历通知有一套固定写法,漏掉finish()会导致后续注册异常:
// 服务端状态变化时,通知所有在线客户端 private void notifyAllClients(int status) { int count = callbackList.beginBroadcast(); // 开始遍历,锁定列表 try { for (int i = 0; i < count; i++) { ICallback cb = callbackList.getBroadcastItem(i); try { cb.onStatusChanged(status); // 反向跨进程调用 } catch (RemoteException e) { // 单个客户端异常不影响其他客户端 Log.e(TAG, "notify client failed", e); } } } finally { callbackList.finishBroadcast(); // 必须配对调用 } }beginBroadcast会返回当前有效回调数量并冻结列表快照,getBroadcastItem(i)按索引取,finishBroadcast释放。三个方法必须严格配对,中间任何异常都要靠finally兜住。每个cb.onStatusChanged外面单独 try-catch 也是血泪经验——一个客户端崩了不能连累其他客户端,否则多客户端场景下就是连锁反应。
3.3 Service 的 onBind 与生命周期
服务端Service的onBind返回 Stub 实例,这是跨进程的入口:
public class RemoteService extends Service { private final IRemoteService.Stub binder = new IRemoteService.Stub() { @Override public void registerCallback(String clientId, ICallback cb) { callbackList.register(cb); } @Override public void unregisterCallback(String clientId) { // 简化版:实际应配合 clientId 精确反注册 } @Override public int getDeviceStatus() { return currentStatus; } }; @Override public IBinder onBind(Intent intent) { return binder; // 返回给客户端的就是这个 Binder } }onBind可能被多个客户端各调用一次,但返回的是同一个binder实例,所以服务端天然是「一个服务对多个客户端」。onBind的返回值不能为 null,否则客户端bindService会拿到 false。另外注意onUnbind和onDestroy里要做清理,虽然RemoteCallbackList会自动处理死亡回调,但主动kill掉列表更稳妥。
4. 客户端实现:绑定、注册回调与断开清理
4.1 bindService 与 ServiceConnection
客户端侧第一步是绑定服务。跨进程绑定和普通绑定写法一样,但拿到IBinder后要转成 AIDL 接口:
private IRemoteService remoteService; private final ServiceConnection conn = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { // 关键:把 IBinder 转成 AIDL 接口代理 remoteService = IRemoteService.Stub.asInterface(service); try { // 绑定成功后立即注册回调 remoteService.registerCallback(clientId, callbackImpl); } catch (RemoteException e) { Log.e(TAG, "register failed", e); } } @Override public void onServiceDisconnected(ComponentName name) { remoteService = null; // 服务端异常断开,置空 } };Stub.asInterface是跨进程的魔法入口,它把底层IBinder包装成接口代理,调用方法时自动走 Binder 序列化。onServiceDisconnected只在服务端进程崩溃时回调,正常unbindService不会触发,所以别把清理逻辑只写在这里。
4.2 回调实现与线程问题
客户端要实现ICallback.Stub,注意回调是在 Binder 线程池里执行的,不是主线程:
private final ICallback.Stub callbackImpl = new ICallback.Stub() { @Override public void onStatusChanged(int status) { // 这里运行在 Binder 线程,更新 UI 必须切主线程 mainHandler.post(() -> updateUi(status)); } };这是新手翻车高发区:直接在onStatusChanged里setText,运气好没崩,运气差就CalledFromWrongThreadException。Binder 回调默认跑在服务端发起的 Binder 线程,跟客户端主线程完全是两码事,所有 UI 操作都得 post 回主线程。
4.3 断开时的反注册顺序
客户端退出前必须反注册,顺序不能乱:
@Override protected void onDestroy() { super.onDestroy(); try { if (remoteService != null) { remoteService.unregisterCallback(clientId); // 先反注册 } } catch (RemoteException e) { Log.e(TAG, "unregister failed", e); } if (conn != null) { unbindService(conn); // 再解绑 } }先unregisterCallback再unbindService,顺序反了的话,服务端可能还持有回调引用,而客户端已经解绑,下次通知就找不到目标。虽然RemoteCallbackList能兜底,但主动清理永远比依赖兜底可靠。
5. 避坑与排查:多客户端场景下最容易翻车的几个点
5.1 回调被覆盖,只有一个客户端能收到通知
现象:多个客户端都注册了,但只有最后一个能收到onStatusChanged。原因:服务端用了单个成员变量存回调,比如private ICallback callback;,后注册的覆盖前面的。解决:改用RemoteCallbackList或Map<String, ICallback>,按clientId分别存。这是最典型的翻车,我见过不止一次。
5.2 DeadObjectException 刷屏
现象:客户端退出后,服务端日志疯狂报DeadObjectException。原因:服务端持有已失效的 Binder 引用还在遍历通知。解决:用RemoteCallbackList自动摘除死亡回调,或者手动linkToDeath监听。如果已经用了RemoteCallbackList还报,检查是不是在beginBroadcast和finishBroadcast之间做了耗时操作导致快照过期。
5.3 回调里更新 UI 崩溃
现象:onStatusChanged里直接操作控件,偶发CalledFromWrongThreadException。原因:回调跑在 Binder 线程。解决:用Handler或runOnUiThread切回主线程。别用Thread.sleep去「等」主线程,那是玄学调试。
5.4 AIDL 文件包名不一致导致编译或运行失败
现象:编译报cannot find symbol,或运行时ClassCastException。原因:客户端和服务端的.aidl包名、文件名不一致,生成的 Stub 类对不上。解决:把aidl目录整体复制,保证package声明和文件路径完全一致。改包名时两边一起改,别只改一边。
5.5 服务端进程被杀后客户端无感知
现象:服务端崩溃,客户端还在傻等回调。原因:没在onServiceDisconnected里做重连或状态重置。解决:在onServiceDisconnected里把remoteService置空,并触发重绑逻辑。注意这个回调只在异常断开时触发,正常解绑不会走。
6. 二次开发:把 Demo 改成能扛住真实业务的多客户端框架
跑通 Demo 只是起点,真到项目里,这份源码还有几处值得动手改。我一般会从三个方向下手。
第一,把clientId从字符串升级成带业务语义的标识。Demo 里clientId只是个占位,实际项目里它应该能区分客户端类型和实例,比如"dashboard_01"、"voice_02"。服务端可以据此做权限校验——不是所有客户端都有资格收全部通知。改造时在registerCallback里加一层校验:
@Override public void registerCallback(String clientId, ICallback cb) { if (!isAllowed(clientId)) { Log.w(TAG, "client " + clientId + " rejected"); return; } callbackList.register(cb); }第二,给回调加超时和重试。Binder 调用默认是同步阻塞的,客户端如果卡住,服务端onStatusChanged也会被拖住。稳妥做法是把通知丢到服务端自己的线程池里异步发,或者用oneway关键字声明回调方法。oneway让调用变成单向非阻塞,适合纯通知场景:
oneway interface ICallback { void onStatusChanged(int status); }加了oneway后,服务端发通知不会等客户端返回,一个慢客户端拖不垮整个服务。代价是拿不到返回值,也没法感知客户端是否真的处理了,所以只适合「发了就不管」的通知。
第三,把回调管理抽成独立类。Demo 里回调逻辑写在Service内部,客户端一多就臃肿。抽一个CallbackManager,对外暴露register、unregister、notifyAll三个方法,内部封装RemoteCallbackList和线程池。这样服务端Service只管业务,回调分发交给专门模块,测试也好写。
验证改造是否成功,我有个固定套路:起三个客户端,分别注册,然后依次杀掉其中一个,观察另外两个是否还能正常收到通知,服务端日志里有没有残留的死引用。这个「杀一个、看两个」的测试能覆盖大部分多客户端回调问题。从那以后我每次写 AIDL 回调,都强制先跑一遍这个用例,确认RemoteCallbackList的注册反注册配对没问题,再往下写业务。希望这份源码和上面的拆解,能帮你少走点弯路。
本文还有配套的精品资源,点击获取