简介面向Android开发者的AIDL跨进程通信实战案例以模拟第三方应用调用支付宝支付为主线系统演示如何通过AIDL定义支付接口、在独立进程中注册服务、客户端绑定并调用方法适合想理解IPC机制或准备接入支付SDK的中高级Android工程师学习。压缩包约18.71MB共2000个文件除核心的aidl接口、Java源码、AndroidManifest.xml配置外还包含class、dex、jar等编译产物与json、properties、gradle工程文件便于直接导入Android Studio对照阅读。案例中IAliPayService.aidl的创建、Stub实现、bindService连接及unbindService释放均有完整代码结构可拆解学习跨进程调用的完整链路。已有614人学习下载配套AlipayAidlDemo工程对梳理AIDL入门与支付场景模拟很有参考价值。1. 从 IPC 到支付模拟为什么支付能力要放在独立进程先把一个反直觉的结论放在前面跨进程通信IPC在 Android 里的真实开销远比你以为的乐观。Binder 机制把一次跨进程调用压缩到微秒级别所以支付、安全校验这类敏感能力才敢放心地放进独立进程运行——主进程崩溃时它不受影响主进程内存吃紧时系统还能优先压缩它。这份资源就是围绕 AIDLAndroid Interface Definition Language实现的一套模拟支付服务应用主进程负责界面和业务支付服务注册在另一个进程里通过 AIDL 定义的接口完成下单、查单、结果回调的完整链路体验上非常接近对接真实支付 SDK 的跨进程调用与异步回调。适合已经在用 bindService、但搞不清 AIDL 接口为什么要拆成两段、回调为什么总跑在奇怪线程的开发者也适合想预研支付模块、但暂时不敢接真实 SDK 的团队。2. 先把支付协议定下来AIDL 接口设计的三个关键取舍2.1 为什么是 AIDL而不是 Messenger 或广播Android 的跨进程通信选项不少广播适合单向通知ContentProvider 适合结构化数据共享Messenger 基于 Handler 串行收发消息AIDL 则直接暴露一个类型安全的远程接口。放在支付场景里广播首先排除——支付结果涉及金额和订单号广播是全局可见的不适合承担需要定向回执的业务ContentProvider 和方法调用这个心智不匹配你没法自然地表达createOrder 返回一个订单号剩下 Messenger 和 AIDL 是真正的竞争者。如果只是做一次「请求-应答」Messenger 完全够用Message.what 定义几个常量就能跑。但支付链路的调用形态有下单、查单、状态回调三种尤其回调节点会贯穿整个支付周期Messenger 需要把 callback 包装成另一组消息类型业务代码里到处是散落的 Message.what 分支时间长了没人愿意维护。AIDL 会为接口生成完整的 Stub/Proxy 骨架跨进程的序列化和 Binder 交互都被封装起来业务侧拿到的就是一个类型安全的接口编译期有问题当场暴露。这是 AIDL 在这个场景里胜出的关键——接口定义本身就是协议文档实现侧和调用侧使用完全相同的签名。这个过程也正好解释了 AIDL 文件为什么要单独维护它就是跨进程通信的契约。契约一旦改动服务端和客户端必须同步修改否则运行时报错。常见做法是把 AIDL 接口从业务代码里拆出来甚至单独建一个 contract 模块两端引用同一个模块的依赖避免拷贝两份接口文件导致的不一致——这是我在项目里吃过亏后强制定下的习惯。2.2 下单、查单、回调三个方法怎么拆接口粒度直接影响后续扩展。我把模拟支付拆成两个 AIDL 文件IPayService 是客户端调服务端的下单与查单通道IPayCallback 是服务端把支付结果推回客户端的通道。下面这份 IPayService.aidl 是项目里最先定稿的版本// IPayService.aidl package com.example.paysdk; import com.example.paysdk.IPayCallback; interface IPayService { /** * 创建订单传入订单描述如 amount0.01goodstest返回订单号 */ int createOrder(String orderInfo); /** * 查询订单结果返回是否支付成功 */ boolean queryPayResult(int orderId); /** * 注册支付结果回调 */ void addCallback(IPayCallback callback); /** * 注销支付结果回调 */ void removeCallback(IPayCallback callback); }注释部分我留了关键信息。createOrder 的入参是拼接好的订单字符串模拟真实支付 SDK 中下单时上送的商品与金额摘要返回值是订单号服务端在同一进程内用自增 ID 生成避免重复下单产生同一个订单号的边界竞争。queryPayResult 是简单的状态查询返回 boolean 只表达是否支付成功如果需要更细的失败原因应该把返回值设计成状态码而不是布尔值。回调侧单独定义在另一个文件里// IPayCallback.aidl package com.example.paysdk; interface IPayCallback { /** * 支付结果回调resultCode 0 处理中1 成功-1 失败 */ void onPayResult(int orderId, int resultCode, String message); }把回调拆成独立 AIDL 接口有一条硬规则AIDL 不允许把普通 Java 接口当参数传递必须使用 AIDL 生成的 Binder 接口类型。如果把 onPayResult 塞进 IPayService 作为普通方法客户端就没法注册一个跨进程回调对象。这条规则新手很容易忽略编译期不会报错运行到绑定阶段才暴露出来属于典型的编译过、跑不快的暗坑。备注里标了 resultCode 的三档取值这是模拟支付服务内部的状态编码客户端和服务端共用一份常量建议在服务端用静态常量集中定义别在回调和判断处写裸数字。2.3 in/out/inout方向标签背后的传输开销AIDL 对每个非基本类型参数都强制要求方向。in 表示客户端流向服务端out 表示服务端流向客户端inout 双向都走。方向不只是语义更关系序列化次数out 参数跨进程传输时客户端传入的数据在服务端拿到的是空壳inout 参数则要在两个方向各做一次 Parcel 序列化和反序列化开销直接翻倍。方向数据流向序列化次数适用场景in客户端 - 服务端1 次下单参数、查询条件out服务端 - 客户端1 次服务端回填的对象inout双向2 次尽量不要用支付场景里 orderInfo 是纯入参标 inqueryPayResult 的 int 返回值是基本类型不需要方向回调对象作为入参传递但它的方向严格说是另一条链路按 in 处理没错。我一般会建议团队里定一条规矩默认能用 in 就用 in出现 out 必须写注释说明为什么inout 直接不允许出现在新接口里。方向标签也是新手容易翻车的地方。把回调对象标成 inout 之后Binder 驱动会尝试在两个方向做 Parcel 传递而回调对象内部持有的是 Binder 代理双向序列化容易触发异常。这个坑我实际遇到过一次接口评审时把 addCallback 的入参标成了 inout运行到 addCallback 时直接抛异常排查了半天才发现是方向标签画蛇添足。改回 in 之后一切正常。另外AIDL 不是万能的。它支持基本类型、String、CharSequence、List、Map、Parcelable 与另一个 AIDL 接口但不支持直接传一个普通 Java 对象。如果需要传订单实体就得让订单类实现 Parcelable 并额外建一个同名 .aidl 文件声明它第 6 章会演示范。3. 服务端侧的两个关键点Remote Service 注册与状态回调实现3.1 Remote Service 注册进程与导出权限服务端的核心是把支付逻辑装进独立进程。搞独立的做法只有一个在 AndroidManifest.xml 里给 Service 配置 android:process。这个属性把 Service 分派到一个以冒号开头的私有进程里应用主进程和支付进程就此隔离主进程的内存抖动和卡顿不会波及支付链路。service android:name.PayService android:exportedfalse android:process:payprocess /exportedfalse 的含义要厘清它限制的是其他应用能不能来绑这个服务同应用内部不同进程间的绑定不受影响。这一点我见过不少开发者误会以为跨进程就一定要 exportedtrue结果把支付服务暴露给外部支付接口裸奔在公网上。如果这个模块将来要作为 SDK 提供给别的 App 集成再改成 exportedtrue并同时配置 permission 做调用方校验。同一份资源里另一个容易被忽略的细节是进程名。android:process 以冒号开头系统会拼上包名作为实际的进程名直接写一个不带冒号的名字会变成全局可见的进程名其他应用知道进程名就可以拿到你的进程共享文件存在安全隐患。模拟支付用冒号开头就够真实产品里如果确实要跨应用共享进程再单独评估风险。Service 本体继承普通 Service唯一区别是 onBind 必须返回一个 IBinder 实例。这个实例不是随便 new 一个 Binder而是由 AIDL 生成的 IPayService.Stub 的子类实例。Stub 内部已经实现完整的 Binder 事务处理我们只需要填业务逻辑。3.2 Stub 实现状态容器与线程安全AIDL 接口的每个方法都不是跑在 Service 的主线程而是由 Binder 驱动分派到 Binder 线程池。这意味着 createOrder、queryPayResult、addCallback 可能同时被多个线程调用共享数据结构必须做并发控制否则订单状态错乱、回调列表并发修改异常都算轻的。public class PayService extends Service { private static final int STATUS_PROCESSING 0; private static final int STATUS_SUCCESS 1; private static final int STATUS_FAIL -1; // 订单号 - 状态ConcurrentHashMap 保证跨线程可见性 private final ConcurrentHashMapInteger, Integer orderStatus new ConcurrentHashMap(); // 订单号 - 订单描述用于查单日志 private final ConcurrentHashMapInteger, String orderInfoMap new ConcurrentHashMap(); // 回调列表遍历多写少CopyOnWriteArrayList 读不加锁 private final CopyOnWriteArrayListIPayCallback callbacks new CopyOnWriteArrayList(); // 订单号生成器保证并发下不重复 private final AtomicInteger orderIdGenerator new AtomicInteger(1); // 模拟支付线程池避免每次下单都 new Thread private final ExecutorService payExecutor Executors.newFixedThreadPool(4); private final IPayService.Stub binder new IPayService.Stub() { Override public int createOrder(String orderInfo) throws RemoteException { int orderId orderIdGenerator.getAndIncrement(); orderStatus.put(orderId, STATUS_PROCESSING); orderInfoMap.put(orderId, orderInfo); mockPayProcess(orderId); return orderId; } Override public boolean queryPayResult(int orderId) throws RemoteException { Integer status orderStatus.get(orderId); return status ! null status STATUS_SUCCESS; } Override public void addCallback(IPayCallback callback) throws RemoteException { if (callback ! null !callbacks.contains(callback)) { callbacks.add(callback); } } Override public void removeCallback(IPayCallback callback) throws RemoteException { callbacks.remove(callback); } }; Override public IBinder onBind(Intent intent) { return binder; } }第一个字段用 ConcurrentHashMap 存订单状态因为 mockPayProcess 会在另一个线程里修改状态主流程线程同时也在读用 HashMap 会丢状态甚至死循环。callbacks 用 CopyOnWriteArrayList 的原因正好相反回调遍历频繁写入也就是注册注销读多写少的默认选择。AtomicInteger 解决并发下单的订单号冲突Binder 线程池同一时刻可能进来多个 createOrder 请求。线程池固定 4 个线程而不是每次 new Thread是防止并发下单高峰期把进程线程数打到几百系统会判定为内存异常。3.3 模拟支付处理与回调推送机制下单之后不能立即给成功结果得把处理中和最终结果两个状态区分开。我在 PayService 里单独抽了 mockPayProcess 方法从线程池取线程sleep 5 秒模拟支付网关的耗时然后把订单状态改成成功再通知注册过的所有回调。private void mockPayProcess(int orderId) { payExecutor.execute(() - { try { Thread.sleep(5000); orderStatus.put(orderId, STATUS_SUCCESS); notifyPayResult(orderId, STATUS_SUCCESS, 支付成功); } catch (InterruptedException e) { orderStatus.put(orderId, STATUS_FAIL); notifyPayResult(orderId, STATUS_FAIL, 支付失败); } }); } private void notifyPayResult(int orderId, int resultCode, String message) { for (IPayCallback cb : callbacks) { try { cb.onPayResult(orderId, resultCode, message); } catch (RemoteException e) { // 客户端进程可能已死移除失联回调防止列表膨胀 callbacks.remove(cb); } } } Override public void onDestroy() { super.onDestroy(); payExecutor.shutdown(); }notifyPayResult 是回调推送的核心入口。它遍历所有已注册回调逐个调用 onPayResult每次调用都是一次真实的跨进程传输。RemoteException 是这里必须处理的客户端崩溃或解绑之后服务端这边的回调代理已经失效继续调用会让异常在每个循环里炸一遍catch 住之后顺手从列表里移除等于给回调列表一个自净机制。onDestroy 里 shutdown 线程池是收尾动作虽然模拟支付进程结束得很快但养成习惯对真实 SDK 落地有好处。服务端完整逻辑就是这个闭环createOrder 种下处理中状态线程池异步推进状态机最终通过回调把结果推回客户端。查单接口 queryPayResult 也是读同一个状态表客户端可以在没有注册回调的情况下主动轮询两条路互不干扰。4. 客户端侧的三个动作绑定服务、注册回调、刷新 UI4.1 绑定服务显式 Intent 与 ServiceConnection客户端第一步是把远程 Service 拉到自己的进程里。bindService 方法需要三个参数Intent、ServiceConnection、绑定标志。Intent 必须显式指定目标服务跨进程场景下最稳妥的做法是用 ComponentName 而不是 Class 对象因为混淆之后类名会变字符串形式至少可控。private IPayService payService; private boolean bound false; private void bindPayService() { Intent intent new Intent(); ComponentName cn new ComponentName( getPackageName(), com.example.paysdk.PayService); intent.setComponent(cn); bindService(intent, connection, Context.BIND_AUTO_CREATE); } private final ServiceConnection connection new ServiceConnection() { Override public void onServiceConnected(ComponentName name, IBinder binder) { payService IPayService.Stub.asInterface(binder); bound true; try { payService.addCallback(payCallback); } catch (RemoteException e) { Log.e(PayClient, 注册回调失败, e); } } Override public void onServiceDisconnected(ComponentName name) { bound false; payService null; } };BIND_AUTO_CREATE 标志表示绑定的同时如果服务没启动就自动创建它。onServiceConnected 拿到的 IBinder 是 Binder 驱动的代理对象通过 IPayService.Stub.asInterface 包一层就转换成了类型安全的接口。onServiceDisconnected 并非 unbindService 的镜像回调它只会在服务端进程异常崩溃或系统强杀时触发正常解绑不会走这里很多开发者在这里断点等待结果永远不来以为是自己的 bug其实是语义理解错了。绑定时机放在 Activity 的 onStart解绑放 onStop而不是 onCreate/onDestroy。支付页面旋转屏幕时 onDestroy 不触发但 onStop 会走如果不在 onStop 解绑服务端那边回调列表会越积越长最终拖垮回调推送的遍历效率。4.2 从下单到收结果一次完整调用链路绑定成功只是建立通道真正的支付流程是注册回调 → 点击下单 → 服务端返回订单号 → 等待异步回调。点击按钮的下单代码在客户端很简单因为业务逻辑都在服务端public void onClickPayButton(View view) { if (!bound || payService null) { Toast.makeText(this, 支付服务未连接, Toast.LENGTH_SHORT).show(); return; } try { String orderInfo amount0.01goodsandroid_aidl_demo; int orderId payService.createOrder(orderInfo); orderStatusText.setText(下单成功订单号 orderId 支付处理中); } catch (RemoteException e) { Log.e(PayClient, 下单失败, e); } }这段逻辑有两个容易被忽略的点。一是 RemoteException 的捕获跨进程调用和本地调用不一样服务端进程可能刚好重启Binder 链路断了调用就会抛异常不 catch 会让整个 App 挂在崩溃线上。二是 createOrder 是同步调用但因为它跑在 Binder 线程池并不会阻塞 UI 线程。如果服务端方法本身耗时较长客户端这侧的调用线程会一直等所以 AIDL 方法设计时要在同步返回和异步回调之间做取舍第 6 章展开 oneway 方案。4.3 回调翻线程Binder 线程池不是 UI 线程onPayResult 回调的运行线程是 Binder 线程池里某一个线程不是主线程。直接在里面调 textView.setText会立刻抛 CalledFromWrongThreadException。新手第一次遇到这个异常都会愣住回调明明是在客户端代码里写的为什么不在主线程private final IPayCallback payCallback new IPayCallback.Stub() { Override public void onPayResult(int orderId, int resultCode, String message) { // 当前线程是 Binder 线程池不是主线程 PayClientActivity.this.runOnUiThread(() - { if (resultCode 1) { payStatusText.setText(订单 orderId 支付成功); } else if (resultCode 0) { payStatusText.setText(订单 orderId 处理中); } else { payStatusText.setText(订单 orderId 支付失败); } }); } };runOnUiThread 会把 UI 操作 post 到主线程消息队列由主线程 Looper 串行执行。如果回调频率高或者需要在动画帧里频繁更新建议换成 Handler 持有主线程 Looper方便 removeCallbacks 做生命周期管理。回调的注册与注销必须成对出现。我在 onServiceConnected 里 addCallback在 onStop 里 removeCallback 再解绑Override protected void onStop() { super.onStop(); if (bound) { try { payService.removeCallback(payCallback); } catch (RemoteException e) { Log.e(PayClient, 注销回调失败, e); } unbindService(connection); bound false; } }这里 removal 放在 unbind 之前是为了保证服务端在收到解绑之前先把回调列表清干净。如果先解绑客户端代理已经失效removeCallback 这趟跨进程调用可能直接抛异常回调就留在服务端成了僵尸。5. 避坑手册跨进程支付里最容易翻车的五个瞬间5.1 bindService 静默失败onServiceConnected 永远不来现象调用 bindService方法返回 true但 onServiceConnected 始终不被回调界面一直停在支付服务未连接。原因Service 没在 AndroidManifest.xml 注册或者注册的进程名与实际启动的不一致Intent 用的是隐式匹配且目标 Service 又设置了 exportedfalse。bindService 是异步过程匹配失败不会抛异常只会无声无息地失败。解决先确认清单里有service android:name.PayService并且包名路径正确Intent 用 ComponentName 显式指定就像 4.1 节写的那样还要检查 android:process 属性是否写错导致启动到了另一个进程。排查时在 Service 的 onCreate 里打日志能确认服务有没有真正创建。5.2 疯狂注册回调重复 addCallback 造成的累积现象Activity 每次重建后支付结果回调都比上一次多触发一倍服务端日志里能看到同一个回调对象反复出现在列表里。原因Activity 重建时 onStart 又执行了一次 bindService 和 addCallback而旧实例的 removeCallback 要么没调用要么因为时序没执行服务端又没做去重。解决服务端 addCallback 里用 contains 去重客户端把注册/注销这对操作放在生命周期对称位置并确保注销优先于解绑。更稳妥的做法是回调代理不走强引用用 WeakReference 包一层服务端持有的是弱引用Activity 销毁后自然被回收这是真实 SDK 的常用姿势。5.3 DeadObjectException服务端进程被杀之后的调用现象支付服务在后台被系统回收过一会儿用户再点击立即支付客户端直接抛 DeadObjectException界面闪退。原因Binder 链路是建立在系统内核驱动的上下文上的服务端进程死亡后代理对象还在客户端内存里但底层通道已经断了任何调用都会抛这个异常。这不是偶发是跨进程设计的必然——服务端进程必须做生命周期兜底。解决绑定成功后给 binder 注册 linkToDeath 监听IBinder.DeathRecipient deathRecipient () - { Log.w(PayClient, 支付服务进程已死亡尝试重连); bound false; payService null; bindPayService(); }; binder.linkToDeath(deathRecipient, 0);重连不是立刻执行一个新的 bindService而是延迟 1 到 2 秒再发起避免服务端进程还没重启完就撞上同样的错误。这个回调发生在 Binder 线程池里重连动作记得 post 到主线程再执行。5.4 回调里改 UI 崩溃CalledFromWrongThreadException现象onPayResult 里直接调 textView.setText偶发崩溃崩溃堆栈指向 setText 内部而且只在支付结果回来时出现。原因回调在 Binder 线程池执行UI 视图只能在主线程操作跨线程触碰 UI 会在 ViewRootImpl 检查时抛异常。偶发是因为 Binder 线程池调度时机不同有时候碰巧主线程空闲处理得快。解决所有 UI 更新统一在 runOnUiThread 或 Handler 里做。习惯做法是不要在 AIDL 回调里写任何与视图相关的代码回调只负责把数据更新到一个 ViewModel 或状态类UI 层观察变化再刷新。这样哪怕回调频次再高主线程压力和崩溃风险都不会叠加。5.5 数据量超限TransactionTooLargeException现象支付结果回调里附加了订单详情的 List几十条数据时正常上百条时服务端推回调就抛 TransactionTooLargeException。原因Binder 单向事务的数据量上限大约 1MB超过之后系统直接拒绝传输。这不是 AIDL 代码写错而是把太多数据塞进了跨进程通道。解决跨进程只传必要字段。订单详情明细要求回调里缩短成状态码和简短描述需要详情时客户端用 queryPayResult 或再拉一次数据。如果一定要传大对象把对象落到文件只传文件路径让客户端自己去读。这条经验是从一个真实项目里带过来的当时在回调里塞了一个带签名图的支付凭证Bitmap 序列化直接炸了换成路径之后问题消失。6. 进阶一步oneway、Parcelable 与真实 SDK 替换6.1 oneway 关键字与 Parcelable 订单对象把 createOrder 改成 oneway调用方发完请求立即返回不等服务端计算结果。这在语义上更像真实的下单流程客户端只负责把订单递出去。改造时 AIDL 文件改动很小// IPayService.aidl interface IPayService { oneway void createOrderAsync(String orderInfo); int createOrder(String orderInfo); }oneway 方法不能有返回值也不能有 out 参数所以异步版 createOrderAsync 返回 void订单号只能在回调里回传。如果业务需要先拿订单号再展示二维码模拟扫码支付场景就得在服务端先创建订单把订单号由回调推回来时序对调一下。自定义订单对象传参也顺手体验一下。定义 OrderInfo 类实现 Parcelable再新建一个 OrderInfo.aidl 声明它IPayService 方法签名改成int createOrder(OrderInfo order)AIDL 文件里用 import 引入订单类。注意订单类必须声明 parcelable 关键字和包名否则编译器认不出来。跨进程多字段参数用对象传比拼接字符串 orderInfo 更便于维护字符串方案为了简单可以保留真实 SDK 集成阶段建议直接上 Parcelable 或 Bundle。6.2 替换真实支付 SDK 的接口映射模拟支付最大的用处是让业务侧先跑通流程等真实 SDK 到位时换掉的应该是 IPayService 的实现类而不是整个客户端架构。把 IPayService 当成支付抽象层模拟实现和真实实现各自维护动作模拟实现真实 SDK 对应下单createOrder 返回订单号SDK 的 pay 方法发起支付结果通知IPayCallback.onPayResultSDK 的支付回调监听查询queryPayResult订单查询接口客户端代码里只依赖 IPayService 和 IPayCallback 这两个抽象模拟实现通过 bindService 绑定到独立进程真实 SDK 可能是同一个进程内直接初始化。业务代码不需要感知底层是跨进程还是本地调用因为接口签名完全一致。这一步做好之后支付模块的替换成本被压缩到重新实现 Stub这一层。我从这个项目里学到的习惯是跨进程回调每次上线前先问一遍回调在哪个线程、生命周期跟着谁走确认之后才写第一行实现代码。从那以后我再也没有被 CalledFromWrongThreadException 和回调泄漏绊倒第二次。希望帮到你。本文还有配套的精品资源点击获取