Android多App共享AIDL服务:单实例与绑定避坑指南 简介面向 Android 开发者的 AIDL 跨进程通信完整工程案例解决不同 App 共享同一 Service 的需求。资源采用独立 library 项目封装 AIDL 接口与 Service 实现降低多应用通信耦合适合学习 Binder 机制、跨应用服务绑定与进程间数据交换的中高级开发者。压缩包共 2000 个文件包含 aidl 接口定义、Java 源码、class 与 jar 库文件、APK 安装包、Gradle 构建脚本以及大量 XML 资源文件等整体约 41.14MB从文件类型可看出工程已完整构建并附带调试产物便于导入 Android Studio 直接分析。已有 461 人学习。资源提供了从 AIDL 文件创建、Stub 实现、Service 注册到多 App 绑定调用的完整思路内含 sendMessage 与 receiveMessage 等示例接口并演示如何通过 BroadcastReceiver 联动启动服务借助这些代码可快速掌握跨应用广播联动、共享服务生命周期管理及授权访问控制等关键点是练习组件化通信和系统级 IPC 能力的实用素材。1. Android AIDL多App共享服务先理解单实例再谈绑定很多同学第一次接触 Android AIDL都是做一个“客户端调用服务端方法”的 Demo能跑通就觉得会了。但真正到工程里需求往往是三个甚至更多 App 要同时读写同一个服务的数据这时候你就会发现AIDL 最核心的门槛不是接口怎么写而是“服务端进程只有一个实例三个客户端各自持有的只是 Binder 句柄”。这个反直觉的结论决定了你后面所有的绑定、解绑、数据同步、异常处理代码怎么写。这篇笔记不绕弯子直接拿“3 个 App 共享 1 个服务”的完整工程来讲适合已经在用 AIDL 但没做过跨 App 共享的开发者也能让刚看完官方文档的新手少踩几个真实的坑。2. 服务端设计一个 Service 扛住三个客户端的访问2.1 服务端接口定义把共享能力固定成契约先明确一个概念AIDL 文件Android Interface Definition Language不是实现它是一份跨进程通信的契约。服务端和所有客户端必须持有包名完全一致的 .aidl 文件否则 Binder 驱动能建立连接但 Stub 接口类对不上直接抛异常。我给模拟项目X定义了一个共享用户状态的服务接口长这样// IUserStateService.aidl package com.demo.sharedservice; import com.demo.sharedservice.UserState; interface IUserStateService { // 读取用户状态 UserState getUserState(); // 更新用户状态 void updateUserState(in UserState state); // 获取在线设备数用于统计 int getOnlineDeviceCount(); }逻辑说明这个接口做了三件事——读、写、统计。getUserState()是同步返回一个 Parcelable 对象updateUserState()用in表示客户端传入的数据会被服务端读取getOnlineDeviceCount()返回一个 int 值。参数说明这里的in是 AIDL 的定向 tag还有out和inout。in表示数据从客户端流向服务端服务端修改不会回传out表示服务端产生的数据回传给客户端入参原始值会被忽略inout是双向。在跨 App 共享场景里我一般只建议用in因为inout会触发双向序列化每次调用多一次 writeToParcel性能损耗在频繁调用时放大得很明显。如果只是同步状态in足够。自定义的UserState是一个 Parcelable 类这里有个硬性要求它必须和 IUserStateService.aidl 放在同一个包名下且在服务端和每个客户端里都要有一份完全相同的源码文件。千万别想着“我在服务端写了这个类其他 App 远程拿一下就行”——做不到Binder 不会帮你传类定义它只传序列化后的字节。2.2 Service 实现与单实例保证一个进程只跑一份逻辑服务端的核心是 Service在 AndroidManifest 里注册配置android:exportedtrue让它能被其他应用访问。真正的实现代码看下面// UserStateService.java public class UserStateService extends Service { private static final String TAG UserStateService; // 用 ConcurrentHashMap 保存数据支持多线程并发 private final MapString, UserState mStateMap new ConcurrentHashMap(); private final IUserStateService.Stub mBinder new IUserStateService.Stub() { Override public UserState getUserState() { UserState state mStateMap.get(default); if (state null) { state new UserState(guest, 0); mStateMap.put(default, state); } return state; } Override public void updateUserState(UserState state) { // 这里必须加同步锁或使用并发容器后面避坑章细说 mStateMap.put(default, state); } Override public int getOnlineDeviceCount() { return mStateMap.size(); } }; Override public void onCreate() { super.onCreate(); // 服务创建时初始化只执行一次 } Override public IBinder onBind(Intent intent) { return mBinder; } }逻辑说明onBind返回的是IUserStateService.Stub的实现对象这个对象是 Binder 的本地实体所有客户端的调用最终都会走到这里。关键点是mBinder是整个服务进程里唯一的实例不管外面有多少 App 绑定Binder 驱动都是把调用路由到同一个对象的方法上。这里就解答了标题里最反直觉的问题“3 个 App 共享一个服务”不是说系统帮你起了三个服务实例而是三个 App 各自拿到一个 Binder 句柄这个句柄指向同一个内存里的同一个对象。数据天然共享因为mStateMap只有一个。常见做法是把服务做成android:process:remote单独开一个进程好处是服务端崩溃不影响客户端主进程坏处是每次调用都要跨进程序列化。如果三个 App 对实时性要求不高可以不开独立进程省一点 IPC 开销。2.3 AndroidManifest 与进程边界让三个 App 都能发现这个服务服务端写完了注册配置是第二个容易翻车的点。一个 App 默认只能访问exportedtrue的组件而且 Android 5.0 以后不管服务端还是客户端都强制要求显式声明 Intent。我的注册配置如下service android:name.UserStateService android:exportedtrue android:process:remoteservice intent-filter action android:namecom.demo.sharedservice.BIND_SERVICE / /intent-filter /service逻辑说明android:process:remoteservice表示这个服务跑在独立进程进程名是“包名:remoteservice”。三个 App 绑定它实际是跨进程访问这个进程里的服务。exportedtrue是前提不写的话其他 App 会收到SecurityException这是新手最容易漏的一行。参数说明intent-filter里的 action 只是给隐式 Intent 用的但在多 App 场景下我建议客户端全部用显式 ComponentName 绑定写死包名和类名避免系统找不到匹配对象。这里 action 留一个主要是为了调试时方便用adb shell activity start这类工具验证。服务进程只跑一份逻辑这个结论在dumpsys activity services里能看到。绑定后查看输出会看到只有一个 ServiceRecord而它下面挂了好几个连接记录。3. 客户端绑定与参数细节三个 App 同时连接时的真实行为3.1 显式绑定为什么不能用隐式 Intent客户端这边的绑定代码我见过太多人直接照抄官方文档的Intent(com.xxx.BIND)然后三个 App 全绑定失败。原因很简单隐式 Intent 在当前版本的 Android 上触发系统组件解析但如果你服务端没有设置intent-filter或者客户端没有包名限定解析结果就可能不是你要的那个服务。我的客户端绑定代码是这样写的// 在 Activity 里绑定远程服务 private IUserStateService mService; private ServiceConnection mConnection new ServiceConnection() { Override public void onServiceConnected(ComponentName name, IBinder service) { mService IUserStateService.Stub.asInterface(service); // 绑定成功后立刻拉一次数据初始化界面 UserState state mService.getUserState(); } Override public void onServiceDisconnected(ComponentName name) { // 服务进程异常退出时触发注意解绑时不会触发 mService null; } }; private void bindSharedService() { Intent intent new Intent(); intent.setComponent( new ComponentName(com.demo.sharedservice, com.demo.sharedservice.UserStateService)); boolean bound bindService(intent, mConnection, Context.BIND_AUTO_CREATE); if (!bound) { // 绑定失败检查包名、service类名、AIDL包名 } }逻辑说明setComponent直接写死服务端包名和类名绕开了所有隐式解析规则这是三个 App 能稳定连上同一个服务的关键。BIND_AUTO_CREATE的意思是如果服务没在运行系统先创建它再绑如果已经在运行直接连上已有实例。参数说明bindService的第三个参数 flags 一共有三个值可组合——BIND_AUTO_CREATE自动创建服务、BIND_DEBUG_UNBIND调试用、BIND_NOT_FOREGROUND表示不允许把服务拉到前台。通常只用BIND_AUTO_CREATE。这个参数的行为值得记一下多个进程用同一个 ServiceConnection 对象绑同一个服务系统只会回调一次onServiceConnected。3.2 回调机制onServiceConnected 与 onServiceDisconnected 的边界连接回调是另一个黑匣子。很多同学以为“解绑了服务就会回调 onServiceDisconnected”这是错的。真相是onServiceConnected在服务建立连接后回调你可以在这里拿 Binder 句柄转接口。onServiceDisconnected只有在服务进程被异常杀掉、或者系统强制回收时才回调。 你自己调用unbindService()时这个回调不会触发只是连接静默断开。数据同步就这样三个 App 同时绑定服务端onCreate只会执行一次。每个 App 的onServiceConnected都会回调但拿到的 Binder 对象指向同一个 native 层实体。如果你在三个 App 里同时调用updateUserState服务端的updateUserState会串行执行Binder 底层自带线程池处理并发调用但你的业务代码如果没有同步锁就会出并发问题。3.3 服务端单实例验证三个 App 是不是连的同一个对象有一招能直观看到单实例效果。先让三个 App 都绑定成功然后在任意一个 App 里执行adb shell dumpsys activity services com.demo.sharedservice输出里会有ServiceRecord和Connections列表。你会看到同一个ServiceRecord下面挂了三个ConnectionRecord对应三个进程的绑定关系。这就能证明服务端只有一个对象三个 App 共享它的内存。常见做法是在服务端updateUserState里打日志然后让三个 App 各自改一个字段最后任何一个 App 再getUserState都会读到被覆盖后的统一状态这就是共享效果。参数和边界条件也在这里暴露如果三个 App 中某个进程被杀onServiceDisconnected会触发但其他两个 App 的连接不受影响服务也不销毁只有最后一个连接全部解绑后服务才走onDestroy。这个生命周期规则决定了“共享服务”的开发思维——别依赖某个客户端保活服务服务端的可靠性要自己兜底。3.4 绑定失败排查返回值 false 的常见原因绑定失败的现场我列一个排查清单基本能覆盖 90% 的情况现象可能原因解决bindService 返回 false服务端包名或类名写错检查 ComponentName 两个参数返回 true 但 onServiceConnected 不回调AIDL 包名不一致或接口类版本不匹配确认两端 aidl 文件包名一致抛 SecurityException服务端 exportedfalse改为 true绑定成功但调用方法报空指针服务端进程被杀mService 为 null在 onServiceDisconnected 里重绑隐式 Intent 绑定失败Android 5.0 后限制隐式绑定 Service全部改用 setComponent这里要多说一句AIDL 接口文件修改后服务端和客户端都要同步替换。哪怕只是加了一个方法旧的客户端绑定成功后调用新方法会直接抛RemoteException而且 debug 版不一定给你明确提示。4. 多客户端绑定的避坑与排查五个真实翻车现场4.1 现象三个 App 里只有一个能收到回调真实场景某跨平台系统里三个子模块都要绑服务其中一个模块 A 绑定失败日志里没有异常bindService 返回 false。排查发现模块 A 的 manifest 里居然也声明了一个同名 Service 类但这个类不在服务端进程里。系统在解析 ComponentName 时优先匹配了本应用的同名组件而该组件又不满足导出条件于是绑定失败。原因ComponentName 只传了类名没有带完整包名或者本应用内存在同名类直接覆盖了解析结果。解决ComponentName 的第二个参数直接用全限定类名不要用相对类名同时检查应用内有没有重复的 Service 类定义。4.2 现象服务端进程被系统回收客户端所有方法调用全部失败这个坑带血泪经验。模拟项目X上线后线上反馈“设备状态不刷新”查日志发现服务端进程被 Low Memory Killer 回收了。客户端代码里mService还持有旧的 Binder 句柄调用任何方法都抛RemoteException而onServiceDisconnected是异步回调没来得及重绑就已经崩了。原因服务端在独立进程进程优先级低于客户端前台进程内存压力大时最先被回收。解决两个动作要做到位——第一客户端做重绑逻辑onServiceDisconnected触发后延迟 1 秒重绑避免频繁重建第二服务端声明android:persistenttrue只在系统级 App 里有效普通应用做不到所以更实际的办法是把服务做成前台服务保活概率大增// 服务启动时拉起前台通知 if (Build.VERSION.SDK_INT Build.VERSION_CODES.O) { startForeground(1001, createNotification()); }注意Android 12 以上startForeground会有 FGS 启动限制最好是客户端通过startForegroundService()先拉起再自动转前台。4.3 现象服务端方法里改一个字段另一个 App 读不到新值跨进程共享数据的典型问题没有并发控制。服务端updateUserState方法如果这样写Override public void updateUserState(UserState state) { mCurrentState state; // 不做同步 }三个 App 同时调用时Binder 线程池里多个线程同时进入方法赋值操作可能丢失更新。这不是 AIDL 的问题是服务端业务代码的线程安全问题。原因AIDL 底层是 Binder 驱动 线程池方法会并发执行服务端没有同步机制。解决统一用ConcurrentHashMap或给共享变量加锁。我在服务端设置了一个全局锁对象所有读写状态的方法都走同一个锁牺牲一点并发度换数据一致性。4.4 现象客户端在主线程调用 IPC 方法运行一段时间后 ANR跨进程调用和本地调用最大的差别是耗时不确定。远端进程可能在 GC、可能在走磁盘 IO、甚至可能被系统挂起主线程等一个 Binder 调用超过 5 秒就是 ANR。新手习惯了本地方法随意调用一上 AIDL 就翻车。原因Binder 调用是同步阻塞的执行线程会一直等远端返回。解决客户端所有 AIDL 方法调用必须放到子线程。我自己封装了一个ServiceProxy内部用线程池执行界面更新再切主线程private void updateStateInBackground() { executor.execute(() - { try { mService.updateUserState(new UserState(admin, 1)); } catch (RemoteException e) { // 处理远端异常 } }); }另外服务端有耗时操作时在 Binder 方法里开线程池别直接在 Stub 实现里 sleep。4.5 现象绑定正常数据也通但服务一直不销毁服务生命周期管理容易被忽略。一个 App 解绑了服务还活着这个正常但当最后一个 App 解绑后按理服务会onDestroy实际却没走。后来定位到问题是其中某个客户端调了bindService但没保存 ServiceConnection 对象系统持有引用无法释放。原因unbindService需要传入同一个 ServiceConnection 实例代码里 new 了第二个连接传进去系统匹配不到原有连接就不会真正解绑。解决ServiceConnection 实例在 Activity 里用成员变量持有解绑和绑定必须同一个对象。这个坑没有任何玄学就是引用不对齐。从那以后我每次写绑定代码都会把 ServiceConnection 的创建和解绑放进同一个生命周期块里避免引用丢失。5. 进阶用法Binder 连接池让共享服务不乱套5.1 Binder 连接池的代码骨架当共享的服务从一个变成多个比如除了 UserStateService 还要共享配置服务、告警服务每个客户端都写一遍绑定代码连接对象满天飞管理成本飙涨。我的做法是引入 Binder 连接池一个 Service 里注册多个 AIDL 接口客户端只绑定一次按需获取不同接口。// IConnectionPool.aidl package com.demo.sharedservice; interface IConnectionPool { IBinder getService(int serviceCode); }连接池服务端用一个 SparseArray 缓存所有 Binder 实现public class ConnectionPoolService extends Service { // serviceCode 到 Binder 的映射 private static final SparseArrayIBinder sBinderMap new SparseArray(); static { sBinderMap.put(0, new UserStateServiceStub()); sBinderMap.put(1, new ConfigServiceStub()); } Override public IBinder onBind(Intent intent) { return new IConnectionPool.Stub() { Override public IBinder getService(int serviceCode) { return sBinderMap.get(serviceCode); } }; } }客户端只维护一个ServiceConnection拿到IConnectionPool后按 code 取目标 Binderpublic class ServiceManager { private IConnectionPool mConnectionPool; public T T getService(int code, ClassT type) { IBinder binder mConnectionPool.getService(code); return type.cast(...); // 按需转成对应接口 } }逻辑说明连接池的核心价值是把“每个服务都要绑定一次”变成“只绑定一次入口”后续加服务不影响客户端绑定逻辑。这在大规模跨 App 通信场景里非常实用。5.2 参数与边界连接池也要防坑连接池的 serviceCode 必须全局统一我把 code 定义放在公共常量类里三个 App 各自编译也不会冲突。还有个细节getService返回的是 IBinder客户端拿到后要转成具体接口转错类型会抛异常所以getService方法里加了类型校验转不了直接抛带异常信息的 RuntimeException。另外连接池模式下连接管理更不能掉以轻心多个 ServiceConnection 引用生命周期不一致会导致内存泄漏。我的习惯是在 Application 里统一持有 ConnectionPoolService 的绑定所有子模块复用同一个连接避免每个 Activity 绑一次造成资源浪费。这套写法不一定是最优解但足够稳定。每次写完连接池我都会先跑一遍三 App 共享工程的完整测试绑定、读写、杀进程、重连全流程验证通过再收工。希望帮到你。本文还有配套的精品资源点击获取