简介面向Android中高级开发者的AIDL跨进程通信IPC实战资源包完整演示如何通过AIDL实现多个客户端同时调用同一个服务端代码并兼顾多客户端并发访问时的线程安全与资源管理。资源共92个文件核心包含java源码、aidl接口定义、xml布局/配置、class编译产物及可直接安装的apk示例配套项目结构清晰适合初学者对照学习AIDL定义、Stub生成、Service绑定和RemoteException处理等关键环节。压缩包仅235KB轻量易用便于快速导入工程验证多进程通信逻辑。已有1049人学习下载可作为理解Android IPC机制、编写跨应用服务调用或优化Binder性能的参考样例。1. 多个客户端共用一个 AIDL 服务先搞清楚调用模型再动手做过跨进程通信的人都知道AIDL 刚上手时最容易写出「一个客户端对应一个服务」的 demoActivity 绑定 ServiceService 里 return 一个 Binder完事。但一旦你遇到「多个 App 进程同时连接同一个服务各自注册回调服务端还要把数据推给所有人」这种需求——比如一个后台下载服务同时被三个界面进程监听进度或者一个音乐播放服务要同时给通知栏、悬浮窗、手表端推当前曲目——同一个 Binder 实例能撑住吗并发调用会不会互相堵死回调会不会串线这些问题不解决联调时一定翻车。这篇我把完整的服务端、客户端代码和四个最容易踩的坑拆开讲照着复现一遍能省下你至少一个下午的排查时间。2. AIDL 多客户端服务的理论选型Binder 线程池与回调注册机制2.1 Binder 机制下多个客户端天然共享一个服务实例AIDL 的底层是 Binder。同一个 Service 组件不管有多少个客户端通过bindService()连上来系统只会创建一个服务实例onBind()也只会调用一次。所有客户端拿到的都是同一个 Binder 代理对象。所以「多个客户端调用一个服务器」这个需求本质上是多进程通过同一个 Binder 向同一个服务端对象发消息服务端需要有能力区分「消息来自谁」并且能主动向「注册过的客户端」推送。这个认知很重要。它决定了你的服务端代码必须是有状态的不能是onBind()里返回一个无脑处理请求的 Stub 就完事。State 要记录谁注册过、谁注销过、谁已经死了。我第一版做多客户端 AIDL 就栽在这以为 Binder 会自动维护客户端列表结果服务端想广播时压根不知道发给谁。2.2 为什么不能用 ArrayList 管理客户端新手最容易想到的方案服务端定义一个ArrayListICallback客户端 bind 成功后调registerCallback(ICallback cb)把回调塞进列表广播时 for 循环遍历。这个方案有两个致命问题。第一跨进程对象不能长期持有 native 引用。Binder 代理对象在不同进程里是降级为句柄的你直接存 Java 对象也没问题但客户端进程一死服务端持有的 Binder 代理会失效你无法感知遍历时就会抛出DeadObjectException。第二并发遍历与增删冲突。回调注册和消息广播很可能发生在不同 Binder 线程里ArrayList边遍历边删除会抛ConcurrentModificationException即使你加锁广播时频繁持锁也会拖慢所有客户端的跨进程调用。正确的做法是用RemoteCallbackListE。它是 Android 官方专门为跨进程回调注册设计的数据结构内部自己加锁线程安全并且它底层通过IBinder的linkToDeath自动感知客户端死亡死掉的回调会被自动移除不用你手动清。后面服务端代码全建立在它之上。2.3 服务端接口设计注册、注销、广播设计接口时要想清楚三个动作registerCallback(ICallback cb)客户端把自己暴露给服务端。unregisterCallback(ICallback cb)客户端主动下线避免服务端保留死回调。业务方法比如setMessage(String msg)服务端收到后向所有已注册回调广播。其中回调接口ICallback里放什么方法取决于你要推什么数据。推送普通字符串、int、Parcelable 对象都可以。如果推送的是 list 或 map注意只有 AIDL 支持的类型才能通过。下面给出接口文件。2.4 参数说明表AIDL 支持的数据类型与限制多客户端通信时回调里传的数据类型直接决定你能不能跨进程。这里列一张表避免写完 aidl 文件编译报错类型分类支持情况说明基本类型boolean/byte/char/short/int/long/float/double默认支持不需要 importString/CharSequence支持跨进程传的是副本不是引用List支持元素必须是 AIDL 支持类型服务端收到的是单向的 ArrayListMap支持同样要求 key/value 是支持类型Parcelable支持需要显式 import且要写in、out或inout方向AIDL 接口支持回调本身就是这样传入的注意AIDL 里传的 List/Map 类型是固定 ArrayList/HashMap你声明ListString收到的实现类就是ArrayList不要试图在接口里收自定义 List 实现。方向标记默认是in回调接口建议也用in客户端传 Binder 代理进来就够了不需要回传。3. 服务端完整实现RemoteCallbackList 管理多客户端连接3.1 aidl 接口文件定义先建两个 aidl 文件。一个是服务端的业务接口一个是客户端回调接口。回调接口放服务端进程用oneway关键字声明原因后面避坑章节再说。IMessageService.aidlpackage com.example.messaging; import com.example.messaging.IMessageCallback; interface IMessageService { void registerCallback(in IMessageCallback callback); void unregisterCallback(in IMessageCallback callback); void sendMessage(String msg); }IMessageCallback.aidlpackage com.example.messaging; oneway interface IMessageCallback { void onMessageReceived(String msg); }逻辑说明registerCallback和unregisterCallback是客户端生命周期管理sendMessage是业务入口。注释里已经写明in方向回调 Binder 代理只需要单向传入。oneway放在回调接口上表示客户端接收推送时不需要等待服务端返回避免阻塞服务端 Binder 线程。参数说明oneway只对声明它的接口里的所有方法生效。它改变的是调用方行为——调用onMessageReceived时客户端不会等服务端真正执行完而是立刻拿到void返回。这样即使客户端处理慢也不会占用服务端线程池的等待窗口。3.2 服务端 Stub 实现MessageService.java核心代码public class MessageService extends Service { private final RemoteCallbackListIMessageCallback callbackList new RemoteCallbackList(); private final IMessageService.Stub binder new IMessageService.Stub() { Override public void registerCallback(IMessageCallback callback) throws RemoteException { if (callback null) return; callbackList.register(callback); android.util.Log.d(MessageService, registered, size callbackList.getRegisteredCallbackCount()); } Override public void unregisterCallback(IMessageCallback callback) throws RemoteException { if (callback null) return; // unregister 返回 true 表示确实移除成功 boolean removed callbackList.unregister(callback); android.util.Log.d(MessageService, unregistered, removed removed , size callbackList.getRegisteredCallbackCount()); } Override public void sendMessage(String msg) throws RemoteException { sendToAllClients(msg); } }; private void sendToAllClients(String msg) { int count callbackList.beginBroadcast(); for (int i 0; i count; i) { IMessageCallback callback callbackList.getBroadcastItem(i); try { callback.onMessageReceived(msg); } catch (RemoteException e) { // 客户端进程已死或 binder 失效RemoteCallbackList 会稍后自动清理 android.util.Log.w(MessageService, callback failed: callback); } } callbackList.finishBroadcast(); } Override public IBinder onBind(Intent intent) { return binder; } Override public void onDestroy() { callbackList.kill(); super.onDestroy(); } }逻辑说明beginBroadcast()和finishBroadcast()必须配对调用。beginBroadcast内部会加锁并返回当前有效回调数量保证遍历期间不会有新注册或注销插入避免并发修改。getBroadcastItem(i)取出第 i 个回调。遍历中 catch 住RemoteException避免一个客户端崩溃拖垮整个广播流程。参数说明RemoteCallbackList的register()底层会对传入的callback调用linkToDeath客户端进程死亡时自动从列表移除所以你不需要在unregisterCallback里手动调用unlinkToDeath。getRegisteredCallbackCount()只用于日志观察生产环境频繁调用有额外开销建议只在注册/注销时打日志。3.3 注册与注销的并发处理RemoteCallbackList已经内部线程安全注册和注销不需要你额外加synchronized。但是sendToAllClients里如果除了回调还访问其他共享变量——比如最近一条消息的缓存、客户端身份标识 Map——那就必须自己处理同步。比如你要区分每个客户端的身份不能只靠回调对象因为RemoteCallbackList.getBroadcastItem(i)拿到的只是 Binder 代理没有办法直接反查客户端包名。常见做法是注册回调时由客户端额外传一个clientId字符串服务端维护MapString, IMessageCallback但这样又回到线程安全问题。我一般会避免自己维护映射。因为 Binder 代理对象本身可以调asBinder()拿到IBinder你可以用IBinder作为身份 key配一个ConcurrentHashMapIBinder, String存客户端标识配合linkToDeath在死亡回调里移除。这是后话如果需求只是广播RemoteCallbackList一个结构就够了别自己造并发列表。3.4 向所有客户端广播消息广播逻辑只有 beginBroadcast / finishBroadcast 中间那段需要拧紧。还有一个细节不要在广播循环里调用任何可能阻塞的同步方法尤其不要在这个循环里再调回IMessageService的业务方法。原因Binder 客户端调用服务端方法时服务端会从 Binder 线程池取一个线程执行如果你的广播回调是oneway客户端那边执行是异步的但服务端发起调用的这个线程会在每个callback.onMessageReceived(msg)上停留——如果客户端处理慢服务端线程池会被占满。所以广播推送建议先收集消息快照再统一推。比如sendMessage进来时先把msg存到volatile String lastMessage然后开一个新的普通线程去执行sendToAllClients让 Binder 线程尽快释放。具体代码可以这样Override public void sendMessage(String msg) throws RemoteException { lastMessage msg; // 从 Binder 线程中脱离避免广播阻塞调用方 new Thread(() - sendToAllClients(msg)).start(); }逻辑说明new Thread每次创建线程不优雅但比占用 Binder 线程池好。更严谨的做法是注入一个单线程ExecutorService广播任务串行排队避免多个客户端同时sendMessage导致广播乱序。参数说明executor 的核心线程数可以设为 1因为业务逻辑是「大家按顺序收到同一条消息」串行最合理。如果用多线程并发广播客户端接收顺序就没保证了。3.5 服务端如何感知客户端掉线RemoteCallbackList会在底层自动处理掉线但你要不要做「通知业务层」就看需求了。比如某个客户端掉线后需要更新数据库里的在线状态你需要知道「哪个客户端掉了」。实现方式register之前对callback.asBinder()主动调一次linkToDeath然后保存 binder 与客户端 id 的映射。其实RemoteCallbackList内部已经 link 过了你再 link 会重复你需要拿到死亡回调时自己判定。更省事的方案是放弃RemoteCallbackList直接用MapIBinder, IMessageCallback配合linkToDeath管理这样死亡回调里你能立刻知道IBinder是谁。代价是并发控制要自己写。一句话结论如果只是「死了就不给它推」用RemoteCallbackList自动清理足够如果还要「死后做业务收尾」自己维护MapIBinder, 客户端信息更直观。4. 多客户端调用的避坑排查绑定、死线程与回调泄漏4.1 新客户端绑定不上Service 未启动或 Intent 不匹配现象第二个客户端不同进程调用bindService()时onServiceConnected一直不回调Logcat 里也没有onBind被调用的迹象。第一个客户端却正常。原因两种情况最常见。第一Service 没有调用startService()拉起而bindService()在 5.0 之后要求 Service 必须处于启动状态否则第一次绑定后服务可能被系统回收绑定结果不稳定。第二客户端绑定时用的 Intent 里包含包名或action与 Service 在AndroidManifest.xml里声明的 intent-filter 不一致尤其第二个客户端是不同进程时隐式 intent 无法跨进程匹配必须显式指定package。解决在onCreate或 MainActivity 里先startService(intent)再bindService(intent, conn, BIND_AUTO_CREATE)。Intent 统一写成显式的代码里通过ComponentName或setClassName指定要么 service 位于同一 App 内要么客户端 App 能拿到服务端 App 的包名与类全路径。4.2 回调不触发Stub 没持有被 GC 回收现象客户端registerCallback返回成功后服务端广播了客户端回调却一次都没执行。Log 里也没异常。原因经典 GC 坑。客户端把回调对象注册给服务端后自己没保存这个回调对象的强引用。服务端持有的其实只是 Binder 代理客户端进程里执行回调的 Stub 对象如果没有强引用会被 GC 回收。Binder 代理活着但本地 Stub 已经没了消息自然丢失。我当初排查了这个坑两个小时最后发现回调对象是个局部变量方法退出后就被回收了。解决把回调对象赋值给 Activity 或 Application 的字段并保证unregisterCallback之前不置空。比如private IMessageCallback callback new IMessageCallback.Stub() { Override public void onMessageReceived(String msg) throws RemoteException { runOnUiThread(() - textView.setText(msg)); } };逻辑说明Stub对象必须长期存活。runOnUiThread是因为回调方法在 Binder 线程池中执行不能直接刷新 UI。如果回调需要修改 View一定要切主线程。参数说明如果回调里要弹通知、刷新列表建议回调只做数据接收然后通过回调所在进程的 Handler 或 LiveData 转主线程不要在 Binder 线程里做重操作。4.3 并发广播导致 Binder 死线程oneway 与同步调用的取舍现象客户端多了以后sendMessage响应越来越慢甚至整个应用卡死Logcat 里大量TransactionTooLargeException或DeadObjectException。服务端线程池被占满。原因回调方法没有声明oneway。默认 AIDL 是同步调用服务端调callback.onMessageReceived(msg)会阻塞等待客户端执行完。客户端几十个每个都慢 100ms服务端 Binder 线程池默认 16 个线程很快被打满后续所有调用排队。解决回调接口统一声明oneway。这样服务端调用回调后不等待执行结果线程立刻释放。注意oneway也有限制方法不能有返回值不能有 out/inout 参数。如果你确实需要客户端回报处理结果别用oneway改成客户端主动再调服务端方法传入结果异步闭环。4.4 客户端进程被杀死后服务端仍保留回调RemoteCallbackList 自动清理现象用ArrayList管理回调时客户端进程被用户划掉服务端还保留回调代理重复注册后列表越来越大广播也越来越慢。原因普通ArrayList无法感知 Binder 死亡通知服务端不知道客户端已经没了直到广播时抛出DeadObjectException才知道——但这时列表里的死对象已经积累了一堆。每轮广播都要多次踩中异常性能自然恶化。解决替换为RemoteCallbackList。自动移除的前提是客户端进程确实死亡Binder 驱动会发出死亡通知。如果是客户端进程还活着只是回调对象被 GC 了见 4.2RemoteCallbackList不会清理因为它看到的是 Binder 代理还活着。这个区别要记清楚死进程能自动清死对象不能。4.5 服务端 onDestroy 时忘记 kill 回调列表现象服务重启后客户端registerCallback没有报错但广播无法到达任何客户端新客户端注册后getRegisteredCallbackCount()返回 0。原因RemoteCallbackList是对外映射 Binder 代理的核心结构。ServiceonDestroy时如果不调用kill()内部注册的回调可能残留且kill()会统一unlinkToDeath释放底层引用。不调用的话部分系统版本下 Service 重建后这个列表状态异常导致注册不生效。解决onDestroy里务必调callbackList.kill()并且把单例 Service 里的静态列表引用置空。代码Override public void onDestroy() { callbackList.kill(); super.onDestroy(); }逻辑说明kill()之后这个RemoteCallbackList不能再用于注册服务一旦销毁重建就重新 new 一个列表。如果你的服务是单例模式服务端可能长期存活这个坑不明显一旦系统回收重建不 kill 就会出玄学问题。5. 多客户端场景的验证与效率技巧压测、日志与运行期调试代码写完怎么证明「多个客户端真的同时接上了同一个服务」我常用的验证方式是做一个小工具界面服务端进程开一个控制页实时显示注册回调数量每个客户端进程模拟成不同身份的注册者。用两个 AVD 或一台真机加一个模拟器分别启动两个客户端实例验证三件事。第一件事两个客户端都注册后服务端日志里的registered, size是否递增到 2。这个最简单但要注意同一个 App 的两个实例如果跑在同一进程里不算多进程要验证多客户端就要让两个 App 打包签名后分别安装或者同一个 App 用android:process:remote开两个进程。第二件事杀掉其中一个客户端进程服务端下次广播前先打一条getRegisteredCallbackCount()的日志确认数量自动减一。这里有个时间窗口Binder 死亡通知是异步的进程被杀后 12 秒内列表可能还显示旧数量别急着断言失效。第三件事连续向服务端发送 500 条消息观察客户端是否全部收到且接收顺序与发送顺序一致。广播是串行执行时顺序基本一致如果你用了多线程并发广播顺序可能乱需要客户端在回调里打印序号核对。压测代码放在客户端onMessageReceived里统计计数每收到一条打一个点最后对比服务端发送总数。压测脚本不必写完整核心就是服务端记录发送序号客户端记录收到序号比对差值。比如private void sendToAllClients(String msg, int seq) { int count callbackList.beginBroadcast(); for (int i 0; i count; i) { IMessageCallback cb callbackList.getBroadcastItem(i); try { cb.onMessageReceived(seq seq : msg); } catch (RemoteException e) { // 忽略死回调 } } callbackList.finishBroadcast(); }逻辑说明序号由服务端生成从 1 累加到 500。客户端收到后打印日志如果最后收到的最后序号 500且中间没有缺失说明广播链路是通的。这里的seq拼接在msg里是偷懒做法更规范的做法是定义一个MessageDataParcelable 类字段包含 seq、content、timestamp跨进程传对象比拼字符串健壮。最后一个技巧调试时把TransactionTooLargeException的阈值提前暴露出来。Binder 单次事务数据量限制在 1MB 左右但多个客户端同时传输时每个事务都应控制在 100KB 以下最稳。如果你的广播消息超过 100KB比如图片 base64 字符串回调到达率会明显下降。解决方法是把大数据拆成小块或者传到文件路径让客户端自己读不要在 AIDL 里直接传大 payload。从那以后我每次做多客户端 AIDL 服务都会强制把「注册数量日志、死亡回调验证、并发序号比对」这三个环节走一遍发现ArrayList管理回调就直接重写发现回调忘存引用就先补字段发现同步回调没有oneway就先改接口。这套流程帮我避开了大部分线上翻车点。希望帮到你。本文还有配套的精品资源点击获取