
简介面向Android中高级开发者的AIDL跨进程通信实战资源围绕如何通过AIDL定义一个共享服务让三个独立App同时绑定并调用同一接口解决多应用数据交换与协同操作问题。资源配套完整示例工程包含IService.aidl接口定义、MyService服务端实现、AndroidManifest导出配置以及三个客户端App的调用代码并说明了用BroadcastReceiver统一拉起服务的思路便于理解Service与Binder机制。压缩包共2000个文件大小41.14MB以png截图、xml布局与配置、java/aidl源码、class编译产物为主另有jar、apk、dex等构建与运行文件覆盖从代码到安装包的完整链路。目前已有461人学习下载。通过这份资源读者能直接查看AIDL跨进程调用的完整工程结构对照源码学习Stub代理、onBind返回Binder、多应用共享等关键实现。丰富的png界面截图和xml配置可帮助还原项目环境适合作为IPC通信实战参考或二次开发基础。1. 三个 App 想共用一个后台服务AIDL 到底从哪一步开始某团队手里有三个独立 App都需要读取后台同一份状态数据。最初的做法是每个 App 各写一个 Service各连各的结果同一时间 A 改完的数据 B 看不到C 又要单独再开发一遍取数逻辑。问题不在于代码写不好而在于三个进程没有一条共享的数据通道。AIDL 在这种场景下是 Android 官方给出的标准答案你在一个进程里提供一个 Service把接口用 AIDL 定义好其余 App 通过 bindService 拿到同一个 Binder跨进程读写状态就像调用本地方法一样。这个方案能解决三个 App 重复实现服务、状态不同步、维护三套连接代码的痛点适合做多 App 数据中台、统一账户态、跨端任务进度同步这类需求。接下来从服务端开始一步步把三个客户端全部接入。2. 服务端先立起来AIDL 接口、exported 与 onBind 的返回值2.1 为什么是 AIDL 而不是 Messenger 或直接 BinderAIDL 只是 IPC 的一种上层封装。你完全可以手写 Binder、再用 Handler 自己收消息也可以直接用 Messenger 把消息发过去。手写 Binder 的缺点是接口协议要自己定义序列化和反序列化代码写起来又臭又长Messenger 走的又是 Handler 串行队列服务端每次只能按顺序处理消息三个客户端同时来请求时吞吐量很容易卡在单个 handler 上。AIDL 在线程模型上更直接每一个客户端调用会落在服务端 Binder 线程池的某个工作线程里多个客户端请求可以被并行处理它又帮你把 Stub 和 Proxy 生成好代码里只需要关心接口方法本身。当你要做的是“多个进程共享一个服务状态”而不是“发一条通知消息”AIDL 的同步调用加自定义接口能力是最合适的。我一般会优先选 AIDL除非业务简单到只有一两个方法、完全不需要返回值那才考虑 Messenger。AIDL 还有一个实际好处是回调天然支持。你可以定义监听器接口服务端状态一变就能主动推给所有客户端而不是让三个 App 各自轮询。后面 2.2 会写怎么定义带回调的接口这是 Messenger 很难优雅做到的。2.2 定义 ISharedStatusService.aidl包名、定向 tag 与回调接口AIDL 文件里的包名不是给别人看的它会直接决定编译后 Stub 类所在的包也会作为 Binder 的 descriptor 写在生成的代码里。三个 App 共享同一个服务前提是大家拿着同一份 AIDL 描述包名一字不差。否则客户端拿到的 Binder 和本地的 descriptor 对不上轻则 asInterface 返回 null重则直接抛异常。我见过最快翻车方式就是一份 AIDL 从服务端复制到客户端时改了 package客户端还编译过了运行起来才暴露。先定义主接口// 文件: ISharedStatusService.aidl // 包名是三家客户端和服务端之间的接头暗号必须一致 package com.demo.sharedservices; interface ISharedStatusService { // 同步获取当前共享状态方法会阻塞调用线程不要放主线程 String getStatus(); // 由任意客户端写入状态写入后会通知所有已注册的监听器 void updateStatus(String status); // 注册状态变化回推 void registerCallback(ISharedStatusCallback callback); void unregisterCallback(ISharedStatusCallback callback); }再定义回调接口// 文件: ISharedStatusCallback.aidl package com.demo.sharedservices; // oneway 让服务端调用回调时不需要等客户端处理完 oneway interface ISharedStatusCallback { void onStatusChanged(String status); }这里有两个容易写错的地方。第一AIDL 只支持有限的数据类型基本类型、String、CharSequence、List、Map以及你自己声明的 Parcelable。如果你要传自定义对象那个类必须实现 Parcelable并且要额外写一个对应的 .aidl 文件声明它。第二自定义 Parcelable 对象入参出参必须写定向 tagin 表示客户端传入服务端out 表示服务端填好返回给客户端inout 表示双向。漏写 out你会在服务端赋值半天却拿不到值。上面对话只用了 String所以不需要定向 tag但在传对象的方法里这个是必填项。回调接口上加 oneway 是我在第一版集成里踩出来的。不加 oneway 时服务端在 Binder 线程里同步等待客户端进程完成回调方法一旦客户端主线程忙、或者回调里做了耗时代码服务端的写线程会被拖住多个客户端一起回调写状态的方法越跑越慢。oneway 让调用变成异步投递不等待返回。注意 oneway 方法不能有返回值回调接口天然满足这个约束。2.3 服务端 Service 的最小实现onBind 返回同一个 Binder服务端核心是三层一个静态字段保存共享状态、一个列表保存已注册的客户端回调、一个继承了 Stub 的内部实现。静态字段必须放在 Service 所在的进程里因为跨进程状态下只有服务端进程的这份内存是大家公用的。三个客户端 App 各自进程里的 Application 和静态变量都是独立的改了自己那份根本不共享这一条一定先记住。// SharedStatusService.java public class SharedStatusService extends Service { // 共享状态只能放在服务端进程volatile 保证多线程可见 private static volatile String sStatus idle; // 因为 Binder 线程池会并发进入 updateStatus回调列表要用锁保护 private final ListISharedStatusCallback mCallbacks new ArrayList(); private final ISharedStatusService.Stub mBinder new ISharedStatusService.Stub() { Override public String getStatus() { // 这里运行在服务端 Binder 工作线程不要做耗时操作 return sStatus; } Override public void updateStatus(String status) { sStatus status; ListISharedStatusCallback dead new ArrayList(); synchronized (mCallbacks) { for (ISharedStatusCallback cb : mCallbacks) { try { cb.onStatusChanged(status); } catch (RemoteException e) { // 客户端进程已经死掉先记下来统一移除 dead.add(cb); } } if (!dead.isEmpty()) { mCallbacks.removeAll(dead); } } } Override public void registerCallback(ISharedStatusCallback callback) { if (callback null) { return; } synchronized (mCallbacks) { if (!mCallbacks.contains(callback)) { mCallbacks.add(callback); } } } Override public void unregisterCallback(ISharedStatusCallback callback) { synchronized (mCallbacks) { mCallbacks.remove(callback); } } }; Override public IBinder onBind(Intent intent) { // 所有客户端拿到的都是同一个 binder 对象 return mBinder; } }这段代码有几个参数值得展开。sStatus 用 volatile 是为了 getStatus 能读到其他线程写入的最新值String 引用本身的赋值是原子的volatile 足够但 updateStatus 里除了写值还要遍历回调列表这个遍历过程必须 synchronized 包住否则两个客户端同时 update回调列表可能在遍历时被改。RemoteException 不是普通异常它表示远端进程死了或者 binder 调用失败不能吃掉不处理应该顺手把连接断开防止列表里积一堆失效回调。registerCallback 用 contains 判断可以避免重复注册客户端如果忘了注销服务端会一直持有它的 binder导致该进程的 binder 对象无法释放属于典型泄漏点客户端 onDestroy 时记得调 unregisterCallback。Service 注册到 Manifest 时有一个关键项exported。从 Android 高版本开始非 exported 的 Service 根本无法被其他 App 绑定。所以跨进程服务必须是 true。比较稳妥的写法是同时指定一个固定 action方便客户端做定向绑定或者干脆不靠 intent-filter让客户端用 ComponentName 显式指定。我推荐用 ComponentName 显式绑定省去隐式 intent 在系统里的解析开销。service android:name.SharedStatusService android:exportedtrue android:permissioncom.demo.sharedservices.permission.BIND_STATUS intent-filter action android:namecom.demo.sharedservices.action.BIND_STATUS / /intent-filter /serviceandroid:permission是可选的保护层。如果这个服务并不想被任意 App 乱绑自定义一个 normal 级别的权限三个客户端在 Manifest 里各自声明 uses-permission就能把使用范围锁在自己人手里。权限别设成 signature否则三个 App 签名不一致时谁也绑定不上只要不是敏感操作normal 级别足够。3. 三个客户端绑定同一个服务显式 Intent、ServiceConnection 与共享调用3.1 bindService 的显式 Intent跨进程绑定必须这么起客户端绑定服务只需要三步构造显式 Intent、实现 ServiceConnection、调用 bindService。从高版本系统开始隐式 intent 跨应用解析服务的路子越来越难走最常见的问题是 bindService 返回 false后台日志里写 Service Intent not found。我一般在第一步就直接把组件名写死不让系统去猜。// ClientApp 里的绑定代码 private ISharedStatusService mService; private final ServiceConnection mConnection new ServiceConnection() { Override public void onServiceConnected(ComponentName name, IBinder service) { // 关键一步把 IBinder 转成 AIDL 接口 mService ISharedStatusService.Stub.asInterface(service); Log.d(ClientApp, service connected); } Override public void onServiceDisconnected(ComponentName name) { // 服务端进程被系统回收时回调不是解绑时回调 mService null; } }; private void bindSharedService() { Intent intent new Intent(); intent.setComponent(new ComponentName( com.demo.sharedservices, com.demo.sharedservices.SharedStatusService)); boolean ok bindService(intent, mConnection, Context.BIND_AUTO_CREATE); if (!ok) { Log.e(ClientApp, bindService return false); } }setComponent 的两个参数分别是服务所在 App 的包名和 Service 完整类名这两个字符串差一个字母都连不上。BIND_AUTO_CREATE 标志表示连接建立时如果服务还没创建就自动创建它反过来的 BIND_NOT_FOREGROUND 之类标志在这里不合适。ServiceConnection 的四个回调都跑在客户端主线程绑定过程是异步的bindService 返回 true 不代表已经连上真正拿到接口是在 onServiceConnected 里。onServiceDisconnected 只在服务进程崩溃或被系统杀死时触发主动 unbindService 不会进这个回调很多人在这里判断服务“被断开”会误判。断开连接要走 unbindService但它不通知回调需要自己维护连接状态。3.2 三个 App 怎么绑定同一个服务先选部署架构写绑定代码之前先决定服务到底跑在哪个进程里。同样是“三个 App 共享一个服务”部署方式有区别我列了一个对比表。部署方式服务端进程优点注意点独立服务 App单独进程服务生命周期不依赖某一端业务三个 App 对等多维护一个安装包服务端被杀后其他 App 要处理重连由主 App 提供主 App 进程不需要额外安装包主 App 自己也是客户端之一主 App 被强杀时所有端同时失联同一 UID 多进程共享 UID 的同一进程可以不需要 AIDL直接共享内存要求所有 App 同签名应用市场层面很难做到第二种最常用某 App 作为服务提供方另外两个端来绑。这种情况服务端代码和客户端代码在同一主工程里AIDL 文件天然共享不用担心接口漂移只不过服务提供方自己要小心即使用户把主 App 从最近任务里划掉服务进程要保得住否则剩下两个端全部连不上。第一种方案更适合做“公共能力底座”类产品但要多处理一个安装包的版本对齐问题。无论选哪种系统层面只会创建一份 ServiceRecord。三个 App 先后 bindService 到同一个组件第一个触发 onCreate 和 onBind后面两个只是新建立一条连接通道服务对象不会重复实例化。共享状态放在服务端静态字段里的前提就是服务只有一个实例、一个进程。如果服务端进程被杀后重启onCreate 会重新执行静态字段会回到初始值这一点在第五章会讲兜底方案。3.3 同步调用与回调getStatus 与 registerCallbackAIDL 接口里的普通方法是同步阻塞的。客户端调 mService.getStatus() 时调用会跨进程发送到服务端服务端执行完毕后结果再跨进程回来。这个往返时间从几百微秒到几十毫秒不等但绝对不能在主线程里直接调用否则遇到服务端忙或进程调度延迟界面就卡住了。我习惯在客户端维护一个单线程的 executor所有 AIDL 调用都丢进去。// 在子线程执行避免阻塞 UI ExecutorService sIpcPool Executors.newFixedThreadPool(4); sIpcPool.execute(() - { try { String status mService.getStatus(); runOnUiThread(() - textView.setText(status)); } catch (RemoteException e) { // 服务端进程断了等 ServiceConnection 重连 } });回调的注册代码看起来更简单但隐藏着一个线程问题。回调用 oneway 声明后服务端发过来的消息到达客户端时onStatusChanged 运行在客户端 Binder 线程池的某个工作线程里不是主线程。如果你要在回调里更新 UI必须再切一次主线程。我见过不少人直接在这里调用 TextView.setText崩溃日志指向 android.util.AndroidRuntimeException原因就在这。ISharedStatusCallback.Stub callback new ISharedStatusCallback.Stub() { Override public void onStatusChanged(String status) { runOnUiThread(() - textView.setText(status)); } }; mService.registerCallback(callback);注册后一定要配对注销否则服务端回调列表会越积越长。客户端退出页面时先 unregisterCallback 再 unbindService顺序不要反先解绑再注销会让回调停留在服务端直到进程死亡才被 RemoteException 清理。偶尔你会遇到 unregisterCallback 抛 DeadObjectException那说明服务端已经重启了客户端这边按重连流程处理即可不要在旧连接上重试。4. 共享服务避坑包可见性、服务被杀与三端数据不一致的 4 个实例4.1 坑一bindService 返回 false日志查不到目标服务现象是 Android 11 及以上设备上客户端 bindService 一直返回 falselogcat 里什么都没有或者只看到 Service not found。在旧版本设备上同一个包又是好的越界到新系统就翻车。原因是新系统引入了包可见性过滤客户端默认看不见其他包的 Service隐式 intent 基本都会被过滤掉即便用显式 componentName有些场景下系统仍会查不到。解决办法不是去改 targetSdk而是在客户端 Manifest 里声明要访问的包并坚持使用显式 ComponentName。queries package android:namecom.demo.sharedservices / /queries这个声明只对应用市场的包名可见性生效不影响 Service 自身。如果服务端定义了自定义 permission客户端还要补上 uses-permission否则 bind 时会因权限被拒绝日志里出现 Permission Denial。排查顺序建议是先看 bindService 返回值再看服务端进程是否活着最后看 logcat 里的 SecurityException。真要图省事直接把 setComponent 写死是最稳的查询逻辑越少被过滤的可能越小。4.2 坑二onServiceConnected 正常asInterface 却拿到 null现象是服务确实连上了onServiceConnected 里传入了一个非空 IBinder但 Stub.asInterface 返回 null下一步调用直接空指针极端情况下会在 asInterface 内部抛异常。这类问题九成是同一份 AIDL 在不同工程里被改出了差异服务端接口里有两个方法客户端那份只有一个方法或者是最经典的包名拼错。AIDL 编译后的 Stub 会把自己的接口描述符写进 Binder 事务头里asInterface 时会把事务里的描述符和本地 Stub 的 descriptor 做比对不一样就直接判负。解决方式没有任何玄学三端必须引用同一份 AIDL禁止手动复制改包名。我一般会把 AIDL 单独放在一个共享模块里编译成 jar 或 aar服务端和三个客户端统一依赖。改接口时所有人拿到的是同一个编译产物从源头杜绝派系分裂。如果你已经踩了检查顺序是先比包名再比方法签名最后看是否有人把接口参数类型从 String 换成了 CharSequence这种变化也会导致 descriptor 不同但肉眼很难看出来。4.3 坑三服务被系统回收reconnect 流程没写现象是服务端进程运行一段时间后被系统杀掉客户端 onServiceDisconnected 被触发之后所有 AIDL 调用开始抛 DeadObjectException。原因是只 bindService 不 startForegroundService 时服务端进程的 oom_adj 优先级不够高内存吃紧时系统优先挑这种后台进程下手。很多项目的解决方式是前台服务保活但只开前台不重连依然不完整客户端要重连因为连接通道在服务端被杀那一瞬间已经断了。onServiceDisconnected 回调里正确做法是记录断线状态延迟几百毫秒重新 bindService。不要无线重连超过三次就停下来等用户手动触发否则服务端一直起不来时客户端在疯狂空转。单纯的 bindService 保活效果有限最有效的组合是“startForegroundService bindService”服务端同时具备前台进程优先级和连接引用具体代码在 5.1。注意 startForegroundService 后必须在 5 秒内调用 startForeground否则系统会抛异常。4.4 坑四状态各写各的三个 App 永远不同步现象是客户端 A 调用 updateStatus 后自己的页面马上显示了新状态但客户端 B 在同一个时刻调用 getStatus拿到的是旧值。另一个更隐蔽的现象是服务端状态值是正确的但客户端不同页面拿到的值时好时坏。这里面最常见的原因是把共享状态写到了各自的进程存储里。客户端 A 把状态存到自己 App 的 SharedPreferences服务端接口永远读不到服务端读的是自己的静态字段客户端 B 读的自然还是旧值。三端共享的唯一通道是服务端进程的内存和存储任何一端私自加缓存都会造成数据分叉。正确做法是状态只放服务端客户端只读接口结果。如果一定要在客户端做缓存缓存要带上时间戳并且依赖回调刷新。还有一个线程层面的细节AIDL 方法会在服务端 Binder 线程并发执行两个客户端同时调用 updateStatus 时sStatus 的写入顺序由操作系统的锁调度决定业务层不能假设先到先得。要保证时序就在服务端方法里加一个递增的序号updateStatus 的入参带上 revision客户端比较版本号后决定是否采纳这比在客户端乱加锁靠谱。5. 让共享服务长期存活startbid 组合、前台服务与并发同步5.1 startbid 组合onStartCommand 与前台通知共享服务最怕的是进程被系统回收。纯 bindService 的服务引用关系只建立在客户端进程活跃期间一旦客户端都没有显式 startService系统在内存压力下会优先回收它。把服务改成“前台服务 绑定”双轨是常见做法。客户端先调 startForegroundService再调 bindService 拿连接服务端在 onStartCommand 里立刻执行 startForeground将进程提升到前台级别。Override public int onStartCommand(Intent intent, int flags, int startId) { String channelId shared_service_channel; NotificationManager nm getSystemService(NotificationManager.class); if (Build.VERSION.SDK_INT 26) { NotificationChannel channel new NotificationChannel( channelId, 共享服务, NotificationManager.IMPORTANCE_LOW); nm.createNotificationChannel(channel); } Notification notification new Notification.Builder(this, channelId) .setContentTitle(共享服务运行中) .setContentText(正在为多个 App 提供状态) .setSmallIcon(android.R.drawable.ic_menu_compass) .build(); startForeground(1001, notification); return START_STICKY; }需要注意几点。前台服务在较新系统上需要对应类型声明Manifest 里 service 节点要补 foregroundServiceType类型名要与业务匹配否则启动时直接崩溃。通知渠道的 IMPORTANCE_LOW 或更低的 MIN 可以避免在通知栏发出刺耳声音但也不能完全隐藏用户能感知到常驻通知是合规的代价。onStartCommand 返回 START_STICKY 表示进程被杀后系统会尝试重建但如果被杀后紧接着又启动启动时不一定有新 intent 进来所以状态重置后要依赖 5.3 的持久化恢复到最新值。客户端侧统一封装一个入口方法内部先 startForegroundService 再 bindService两个 Intent 必须指向同一个组件。5.2 多客户端并发写状态synchronized 与版本号AIDL 的服务端方法运行在 Binder 线程池不是单线程模型。三个客户端同时调用 updateStatus 时服务端会有多个线程同时进入实现类。2.3 的代码里我用了 synchronized 包住回调列表这保护了回调遍历但 sStatus 的写入没有和回调通知合成一个原子操作。更严谨的做法是把状态写入和通知放进同一个锁里让“改值 通知”对其他线程表现为一次完整的更新。private final Object mStateLock new Object(); public void updateStatus(String status) { synchronized (mStateLock) { sStatus status; ListISharedStatusCallback dead new ArrayList(); for (ISharedStatusCallback cb : mCallbacks) { try { cb.onStatusChanged(status); } catch (RemoteException e) { dead.add(cb); } } mCallbacks.removeAll(dead); } }回调调用本身是跨进程的放在锁里意味着所有客户端回调完成前其他线程的 updateStatus 会等待。这是有意的取舍保证了一个客户端看到一个新状态时它和其他客户端收到的状态顺序一致。代价是某个客户端进程卡住时服务端这条线程会被拖住。要更松耦合就把回调通知移出锁通过另一个单线程 executor 分发代价是各客户端收到通知的顺序就不一定了。具体业务选同步还是异步取决于“状态一致性”更关键还是“回调不阻塞写”更关键。入门先用锁后续出性能问题再拆。5.3 状态丢失的兜底重启后恢复快照static 字段在进程被杀后什么都不剩。前台服务加 START_STICKY 能保证进程重启但重启后 sStatus 回到 “idle”三个 App 再 bind 上来拿到的是空数据。解决方式是每次状态变更时顺便持久化到服务端进程的 SharedPreferencesonCreate 时读回。注意这个文件只能服务端读写三个客户端不要碰。private void persistStatus(String status) { getSharedPreferences(shared_state, MODE_PRIVATE) .edit().putString(status, status).apply(); } Override public void onCreate() { super.onCreate(); sStatus getSharedPreferences(shared_state, MODE_PRIVATE) .getString(status, idle); }apply 异步写盘在极端场景下进程立刻被杀可能丢失最后一次写入一般业务可以接受如果状态很重要改用 commit 同步写但会卡调用线程。还有一条兜底客户端在 onServiceConnected 之后不要立刻无条件 getStatus先等服务端自己把持久化读出来这个时序由 Service.onCreate 开始执行先于 onBind所以其实你不用担心onBind 触发时 onCreate 已经完成。真正要担心的是多进程同时写持久化文件只要坚持只在服务端进程内写入就不会有文件锁互坑的问题。6. 用 15 分钟验证三端真的在共享同一个 Binder代码写完不是结束验证才是。最直接的验证方式就是看 Binder 调用来自哪个进程。在服务端 getStatus 方法里打一行日志把调用方 pid 打出来。三个客户端分别触发一次读取logcat 里会出现三个不同 pid而服务端自己所在的 pid 始终不变。这一步能一次性证明连接确实跨了进程且连接的是同一个 Service 实例。Override public String getStatus() { Log.d(SharedStatus, getStatus from pid Binder.getCallingPid()); return sStatus; }adb logcat -s SharedStatus第二种验证是行为验证。客户端 A 调用 updateStatus 写入当前时间戳客户端 B 和 C 随后 getStatus如果三个端打印出的字符串完全一致说明共享生效。再让 B 写入一个新值观察 A 和 C 是否收到 onStatusChanged 回调这一步能验证回调链路没有断。第三种是更懒的方式用 dumpsys 看服务记录adb shell dumpsys activity services com.demo.sharedservices输出里会列出这个 ServiceRecord 维护的 connections正常绑定状态下你能看到三个客户端的进程名各自出现在连接列表里。如果只出现一两个说明有客户端绑定失败或提前解绑了回到第 4 章的坑位排查。这套流程是我每次做跨进程共享服务时的固定动作先验 Binder 身份再验数据一致性最后才是 dumpsys 兜底。轮流把每个客户端的 pid 和读取结果打到同一份日志里前后一对比问题在服务端还是客户端立刻能看出来。多端共享服务最怕的就是“自己测自己都是好的”三端同时联调时把日志打全能省下大量猜疑链时间。希望帮到你。本文还有配套的精品资源点击获取