资讯动态

Android AIDL多客户端通信:连接模型、线程池与定向推送详解

发布时间:2026/10/15 13:23:49 来源:尧图企业网站定制
简介这是一份面向Android中高级开发者的AIDL多客户端通信实战源码包聚焦多个客户端同时调用同一服务端的典型场景帮助理解跨进程IPC、Binder机制与线程安全处理。包内包含T1、Service、T2三个可运行工程提供AIDL接口定义、服务端Stub实现、客户端bindService调用及多客户端并发调用的完整示例并附带已编译APK与dex文件便于直接安装验证或对照源码学习。资源共92个文件以Java源码、AIDL接口、XML布局、class编译产物及PNG资源为主压缩包仅235KB结构清晰。作者梳理了从AIDL定义、Binder生成到服务端注册、客户端调用的全链路代码并对多客户端并发下的资源调度提出了优化思路。已有1049人学习下载适合正在开发跨进程服务、消息总线或需要掌握AIDL高并发调用技巧的Android工程师参考。1. AIDL 多客户端连一个服务端为什么说这是 IPC 的完整形态当你的 Android 项目里出现「多个客户端调用一个服务器」这样的 AIDL 需求时你面对的不再是单进程里的假跨进程通信。真正的 IPC 场景里服务端进程只有一个但绑定它的客户端进程可能有三个、五个甚至更多。常见做法是把服务端实现为一个常驻 Service各客户端进程分别bindService共用同一个 AIDL 接口收发数据。它解决的核心痛点是多个独立进程需要共享一份数据状态、接收同一类事件推送但彼此不允许直接访问对方内存。这类需求常见于跨应用配置同步、公共消息分发、多端状态上报等场景。难点不在接口怎么设计而在回调管理和线程模型。多客户端 AIDL 翻车十有八九翻在这两个地方。这一篇会从连接模型讲起把接口定义、服务端实现、客户端绑定、避坑排查和定向推送一次说清。适合已经写过单客户端 AIDL、准备或正在做多客户端改造的开发者。2. 多客户端连接模型与 Binder 线程池先搞清楚谁在跑、跑在哪很多开发者第一次写多客户端 AIDL会把服务端想象成一台 Web 服务器每个客户端连接进来服务端就开一个线程伺候。AIDL 完全不是这个模型理解错了后面全是玄学。这一章先把连接模型和线程模型讲透这两个概念直接决定了你后面的代码怎么写。2.1 bindService 的语义服务端进程只有一个连接可以有很多服务端是一个 Service 组件在进程里只有一个实例onBind返回的是同一个IBinder对象。多个客户端各自bindService时系统不会为每个客户端复制一份服务端而是把这个IBinder的代理对象分别交给每个客户端。客户端拿到的代理指向同一个服务端 Binder所有调用最终都收敛到服务端那一个实例上。这意味着服务端的成员变量天然就是共享的。比如你在 Service 里维护了一个ArrayListString任何客户端通过 AIDL 方法往里面塞数据其他客户端通过另一条调用读到的都是同一个列表的内容。不需要额外做单例设计因为它本来就是单例。反过来你需要操心的是多客户端并发访问这个共享状态时的线程安全。服务端进程怎么放也需要提前决策。同一个应用内做多进程绑定Service 可以跑在主进程如果客户端是独立应用最好给 Service 单独配一个进程避免客户端把服务端进程拖垮。manifest 里的配置长这样service android:name.RemoteDataService android:exportedtrue android:process:remote /android:process:remote表示服务端跑在一个以冒号开头的私有进程里冒号开头意味着只有本应用能往这个进程里放组件。android:exportedtrue允许其他应用跨进程绑定。注意跨应用绑定时服务端应用的 Application 和客户端应用的 Application 在各自进程里各自初始化静态变量完全不共享——服务端进程里的静态数据和客户端进程里的同名变量没有任何关系这个割裂感刚接触时特别容易让人困惑。还有一个细节bindService可以被同一个客户端调用多次。同一个客户端进程里用两个不同的ServiceConnection绑同一个 Service服务端不会重建但你会收到两次onServiceConnected回调。如果你每次回调里都注册回调对象服务端那边就会多出重复的订阅。2.2 Binder 线程池多个客户端同时调用时的执行模型客户端调用 AIDL 方法时请求通过 Binder 驱动传到服务端进程服务端由一个 Binder 线程池来执行这些方法。这个线程池是系统为每个进程统一维护的默认上限一般是 16 个线程具体数值随系统版本和厂商定制略有浮动。这 16 个线程不是你创建的也不归你管但你的 AIDL 方法体就运行在这些线程上。这带来三个直接结论。第一多个客户端的同时请求不会在服务端排队逐个执行而是由线程池里的不同线程并发执行——你的 AIDL 方法体必须能承受多线程并发进入。第二方法体里如果有耗时操作比如写文件、查数据库、Thread.sleep()它会占住一个 Binder 线程几个客户端同时发起耗时调用线程池很快就满了后面的请求只能排队等等超过 Binder 驱动超时阈值就报调用失败。第三服务端 AIDL 方法体运行在 Binder 线程不是服务端主线程所以不能直接在方法体里操作主线程上的 View 或 Looper。还有一个多客户端场景里很容易被忽略的现象重入调用。客户端 A 调服务端方法服务端方法里又调客户端 A 的回调回调实现里再调服务端的方法——这条链路上后两次调用会复用同一个 Binder 线程形成嵌套调用。Binder 机制允许这种重入但线程栈会变深一旦逻辑复杂就很难追踪。更麻烦的是如果两个客户端同时触发这条链路两条 Binder 线程会同时钻进同一个客户端的回调实现里客户端回调方法的线程安全性就成了新的课题。所以服务端 AIDL 方法的实现原则只有一条方法体能多快就多快耗时的逻辑一律丢到自己的工作线程里方法只做入队和返回。这条原则在单客户端时还能侥幸糊弄过去多客户端时线程池耗尽几乎是必然的。2.3 oneway 与同步调用的取舍AIDL 接口里的方法默认是同步的客户端调service.getData(key)会阻塞等待服务端执行完并返回。多客户端场景下同步本身不是问题Binder 线程池让请求并发执行但要区分哪些方法值得用oneway。oneway关键字把方法声明为异步客户端调用后立即返回请求在 Binder 层排队服务端按接收顺序执行。它适合两类方法一是注册回调、注销回调这类控制方法客户端不需要等结果二是客户端单向上报事件、服务端不回包的方法。方法场景默认同步oneway获取数据 getData需要返回值必须同步不允许oneway 不能有返回值注册/注销回调可以同步但没必要等建议加 oneway调用立即返回单向事件上报客户端会阻塞等结果建议加 oneway避免客户端等待使用oneway有两个硬限制方法不能有返回值也不能有out或inout参数。这是 AIDL 语法规则编译期直接报错。另外oneway请求在 Binder 层是有序的同一个客户端发起的多个oneway调用会按顺序到达服务端不同客户端的请求之间没有顺序保证——如果你的业务依赖调用顺序需要自己带序号或者时间戳不要把顺序寄托在oneway上。3. 从接口到服务端多客户端 AIDL 代码的搭建顺序理解完模型直接动手。这一章按「接口 → 服务端 → 客户端」的顺序把代码完整搭出来。模拟项目X是一个公共数据仓库服务多个客户端进程可以往仓库写键值数据任何客户端写入后服务端要把变更广播给所有已注册的客户端。3.1 定义 AIDL 接口回调注册方法必须单独设计多客户端场景下AIDL 接口最少包含三组方法数据读写、回调注册、回调注销。回调必须以独立的 AIDL 接口传给服务端不能用一个字符串标识符代替——因为服务端需要持有客户端的 Binder 代理才能主动调用客户端而 Binder 代理只能从客户端传入的接口对象上取。先定义回调接口// IRemoteCallback.aidl package com.example.projectx; interface IRemoteCallback { void onDataChanged(String key, String value); }再定义主接口// IRemoteDataService.aidl package com.example.projectx; import com.example.projectx.IRemoteCallback; interface IRemoteDataService { String getData(String key); void setData(String key, String value); void registerCallback(IRemoteCallback callback); void unregisterCallback(IRemoteCallback callback); }这里的getData用同步返回适合客户端在子线程里主动拉一次数据。setData是写入操作写入成功后由服务端广播给所有客户端。如果想让getData也走异步推送可以改成无返回值、由服务端通过回调把结果推回来但要注意推送对象是调用者自己还是所有客户端——多客户端场景下异步推送很容易误伤所以我更倾向于让读取保持同步、写入走广播。参数说明key和value是String基本类型AIDL 对基本类型、String、List、Map、Parcelable 有内置支持不需要额外 import。IRemoteCallback是自定义 AIDL 接口类型必须import而且主接口和回调接口的包名要一致否则编译期会报类型不匹配。写完这两份 .aidl 文件后Android Studio 会在 build 目录下自动生成对应的Stub和Proxy类不要手动改生成代码。3.2 服务端实现RemoteCallbackList 管理多个客户端回调服务端最关键的数据结构选择不要用ArrayListIRemoteCallback手动管理回调。增删要加锁遍历要防并发修改客户端死了要手动清理——自己实现这三个问题代码量不大但每个都是坑。Android 提供的RemoteCallbackList就是专门为多客户端回调管理设计的增删、遍历、死亡清理都封装好了。// RemoteDataService.java public class RemoteDataService extends Service { private final RemoteCallbackListIRemoteCallback mCallbacks new RemoteCallbackList(); private final ConcurrentHashMapString, String mDataStore new ConcurrentHashMap(); private final IRemoteDataService.Stub mBinder new IRemoteDataService.Stub() { Override public String getData(String key) { return mDataStore.get(key); } Override public void setData(String key, String value) { mDataStore.put(key, value); broadcastChange(key, value); } Override public void registerCallback(IRemoteCallback callback) { mCallbacks.register(callback); } Override public void unregisterCallback(IRemoteCallback callback) { mCallbacks.unregister(callback); } }; private void broadcastChange(String key, String value) { int count mCallbacks.beginBroadcast(); for (int i 0; i count; i) { IRemoteCallback cb mCallbacks.getBroadcastItem(i); try { cb.onDataChanged(key, value); } catch (RemoteException e) { // 客户端已断开RemoteCallbackList 会在下一轮清理 } } mCallbacks.finishBroadcast(); } Override public IBinder onBind(Intent intent) { return mBinder; } }逻辑说明beginBroadcast()和finishBroadcast()必须成对调用。beginBroadcast返回当前存活回调的数量内部会把已死客户端进程被杀、Binder 代理失效的条目摘除getBroadcastItem(i)取出第 i 个存活回调cb.onDataChanged()抛RemoteException表示这个客户端刚好断开捕获后不需要额外清理。register和unregister内部做了去重和死亡监听客户端进程死了回调会被自动标脏并在下一轮广播时移除。参数说明mDataStore选择ConcurrentHashMap而不是HashMap——Binder 线程池会并发执行多个客户端的setData普通HashMap在并发写时会丢数据甚至死循环。RemoteCallbackList自身线程安全外面不需要再包锁。Stub是编译器根据 .aidl 生成的抽象类服务端继承它实现接口方法onBind返回mBinder所有客户端拿到的都是这个 Binder 的代理。3.3 客户端绑定与注册bindService Stub.asInterface 的标准写法客户端要做三件事绑定服务、拿到接口代理、注册回调。生命周期管理是这个环节的重头——绑定、注册、注销、解绑四个动作必须成对出现漏一个就是内存泄漏。// ClientActivity.java public class ClientActivity extends Activity { private TextView mStatusText; private IRemoteDataService mService; private boolean mBound; private final IRemoteCallback mCallback new IRemoteCallback.Stub() { Override public void onDataChanged(String key, String value) { runOnUiThread(() - mStatusText.setText(key value)); } }; private final ServiceConnection mConnection new ServiceConnection() { Override public void onServiceConnected(ComponentName name, IBinder binder) { mService IRemoteDataService.Stub.asInterface(binder); mBound true; try { mService.registerCallback(mCallback); } catch (RemoteException e) { mBound false; mService null; } } Override public void onServiceDisconnected(ComponentName name) { // 服务端进程异常退出时回调此时 binder 已失效 mService null; mBound false; } }; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent new Intent(); intent.setClassName(com.example.projectx, com.example.projectx.RemoteDataService); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } Override protected void onDestroy() { if (mBound) { try { mService.unregisterCallback(mCallback); } catch (RemoteException e) { // 服务端已死忽略即可 } unbindService(mConnection); mBound false; } super.onDestroy(); } }逻辑说明Stub.asInterface(binder)把服务端返回的 Binder 代理转换成接口对象。之后所有 AIDL 调用走这个代理参数会被序列化后经 Binder 驱动传到服务端进程执行执行结果再序列化返回。registerCallback(mCallback)把客户端的回调代理注册进服务端的RemoteCallbackList此后服务端可以主动调用客户端的onDataChanged。参数说明setClassName第一个参数是服务端应用的包名第二个是 Service 完整类名。跨应用绑定必须用显式 IntentAndroid 5.0API 21之后隐式 Intent 绑定服务会直接抛异常。BIND_AUTO_CREATE表示服务端如果没有运行绑定时自动创建不加这个 flag服务不存在时绑定会失败。onDataChanged回调执行在 Binder 线程不是客户端的 UI 线程所以代码里用runOnUiThread切回主线程再碰 View。onServiceDisconnected在服务端进程崩溃或被杀时回调此时 binder 代理已失效不能调用任何 AIDL 方法Android 不会自动重连需要自己记录状态在合适的时机重新bindService。3.4 服务端主动推送广播回调里的安全遍历写法setData调用了broadcastChange这个方法是整个多客户端服务端推送的核心。它的正确写法只有一个不要在项目里手写synchronized(list)加 for 循环的版本。private void broadcastChange(String key, String value) { int count mCallbacks.beginBroadcast(); for (int i 0; i count; i) { IRemoteCallback cb mCallbacks.getBroadcastItem(i); try { cb.onDataChanged(key, value); } catch (RemoteException e) { // 单个客户端失败不影响其他客户端 } } mCallbacks.finishBroadcast(); }逻辑说明beginBroadcast到finishBroadcast之间RemoteCallbackList持有内部锁阻塞其他线程的register/unregister操作。所以这段广播区域必须保持轻薄——不要在循环体里做数据库查询、网络请求或者任何可能耗时的操作否则任何一个客户端的慢回调都会卡住其他客户端的注册操作。参数说明finishBroadcast()一定要保证执行。如果循环里提前return或者抛了未捕获异常跳过它RemoteCallbackList的内部锁永远不会释放之后所有客户端的register和unregister都会永久阻塞。保险写法是把finishBroadcast()放进finally块里。这是多客户端 AIDL 里最容易埋雷的地方后面避坑章会再展开。4. 多客户端 AIDL 避坑指南现象、原因与解决这一章挑四个最常见的坑每条按「现象 → 原因 → 解决」来写。多客户端 AIDL 的问题有个共性单客户端时永远测不出来客户端一多就集中爆发。如果你已经遇到了类似现象直接对号入座。4.1 客户端反复进出导致回调堆积服务端越跑越慢现象同一个客户端每次进入页面绑一次服务、注册一次回调退出时忘记注销。来回操作十几次后服务端每次广播的耗时从几毫秒涨到几百毫秒客户端收到的回调越来越多一次数据变更甚至触发十几遍同样的推送。原因RemoteCallbackList并不按客户端去重。每次registerCallback传入的都是客户端新建的回调 Stub 实例同一个进程即使注册的是同一个接口只要实例不同RemoteCallbackList就全部保留。进入页面三次却没注销服务端就存了三个回调广播自然推三遍。解决客户端onDestroy里必须对称注销且传入的必须是注册时的同一个实例。另一个常见做法是在registerCallback里先注销再注册用同一实例覆盖式注册Override public void registerCallback(IRemoteCallback callback) { mCallbacks.unregister(callback); // 先移除旧引用 mCallbacks.register(callback); // 再注册新引用 }注意覆盖式注册只对同一实例有效。如果客户端每次 new 一个 Stub 传入unregister移除的是旧实例新实例照样堆积。服务端侧能做的只有兜底客户端侧的生命周期管理才是根因。4.2 客户端进程被杀后服务端回调抛 DeadObjectException现象客户端 App 被用户在最近任务里划掉进程死亡。服务端下次广播时cb.onDataChanged()抛出DeadObjectException日志里堆栈刷屏广播线程被异常打断。原因Binder 代理指向的客户端 Binder 对象已经随进程死亡销毁服务端持有的回调变成了死引用。RemoteCallbackList的死亡清理不是实时的而是在下一次beginBroadcast时才统一摘除死条目。解决广播遍历里捕获RemoteException是必须的捕获后不要手动做额外清理finishBroadcast内部会标记失效引用。如果业务需要立刻感知客户端死亡来释放服务端资源可以在registerCallback时对回调的 binder 手动挂死亡监听Override public void registerCallback(IRemoteCallback callback) { mCallbacks.register(callback); IBinder binder callback.asBinder(); binder.linkToDeath(mDeathRecipient, 0); }linkToDeath的DeathRecipient.binderDied()回调运行在 Binder 线程不能操作 UI也不能在这里执行长任务而且linkToDeath不是白用的客户端正常注销后要记得unlinkToDeath否则死亡监听会泄漏。4.3 多个客户端同时写共享数据后写覆盖先写现象客户端 A 和客户端 B 几乎同时setData(count, 1)和setData(count, 2)服务端最终存的是 2 还是 1 完全不确定。更隐蔽的是读改写场景A 读出 value 为 1加 1 后写回 2B 同时读出 value 也是 1加 1 后写回 2——丢了一次更新。原因setData方法体里的「读-改-写」三个步骤不是原子的。Binder 线程池并发执行同一个方法时A 和 B 可以交错执行各自基于同一份旧值计算后写完的覆盖先写完的。解决不要在客户端做「读出来 → 改 → 写回去」而是让服务端提供原子操作。服务端对共享数据的读改写加锁锁的粒度要覆盖所有访问路径Override public void incrementData(String key) throws RemoteException { String newValue; synchronized (mDataStore) { String cur mDataStore.get(key); int val cur null ? 0 : Integer.parseInt(cur); newValue String.valueOf(val 1); mDataStore.put(key, newValue); } broadcastChange(key, newValue); }注意加锁对象要和所有访问mDataStore的路径一致。如果getData也读mDataStoregetData也要加同一把锁否则加锁只保护了写路径读路径还是能看到中间状态。锁块里不要做广播这种耗时操作——广播遍历期间其他线程注册回调会被阻塞锁加广播等于把阻塞时间翻倍。4.4 Binder 线程池耗尽客户端调用全部卡住现象客户端调用service.getData(key)卡住不返回主线程直接 ANR日志里出现 Binder 调用超时。服务端日志显示某个 AIDL 方法被反复执行每次都耗时几百毫秒。原因服务端 AIDL 方法里有耗时操作占住了 Binder 线程。默认十来个线程的线程池被多客户端的并发请求占满后后续请求全部排队排队超过 Binder 驱动超时阈值就报调用失败客户端表现为卡住或者抛TransactionFailedException。解决AIDL 方法体只做内存操作和入队耗时逻辑丢到服务端自己的工作线程。典型错误是直接在setData里写文件、查数据库、做网络同步private final ExecutorService mWorkers Executors.newFixedThreadPool(4); Override public void setData(String key, String value) { mDataStore.put(key, value); // 快操作直接做 mWorkers.execute(() - { // 耗时逻辑落盘、同步、上报 broadcastChange(key, value); // 操作完成后再广播 }); }newFixedThreadPool(4)的线程数按服务端实际负载调整一般 2 到 4 够用。注意广播的位置如果把broadcastChange放在入队前执行客户端还没看到最新的持久化结果就先收到了回调时序就乱了。先完成耗时操作再广播。5. 进阶识别客户端身份与定向推送广播推送只解决「通知所有客户端」这一种需求。真实业务很少这么简单有的客户端只关心自己订阅的数据有的客户端要求只接收指定 key 的变更。这一章把广播模型升级成可识别身份的定向模型。5.1 用 Binder.getCallingUid() 区分每个客户端服务端 AIDL 方法执行时可以在方法体内调用Binder.getCallingUid()拿到当前调用者的 UID。这个 UID 由 Binder 驱动透传是调用者进程的真实身份客户端传参伪造不了。拿到 UID 后用PackageManager解析成包名就可以按客户端维度做登记private final ConcurrentHashMapString, IRemoteCallback mClientRegistry new ConcurrentHashMap(); Override public void registerCallback(IRemoteCallback callback) { int callingUid Binder.getCallingUid(); String packageName getPackageManager().getNameForUid(callingUid); mClientRegistry.put(packageName, callback); mCallbacks.register(callback); }注意getCallingUid()必须在 Binder 调用栈内执行。在方法体里直接调用没问题但一旦切了线程——比如把后续逻辑丢进mWorkers线程——调用栈就变成服务端自己的线程了拿到的就不再是客户端的 UID。所以身份识别必须在进入工作线程之前完成把结果作为参数传进去。Binder.getCallingPid()也能拿调用者进程 ID但 PID 会被系统回收复用几个小时后另一个进程可能就拿到同一个 PID不适合做长期身份标识只在日志排查时用一下就够了。5.2 按客户端分组管理回调从广播改成定向需求变成「不同客户端订阅不同数据」时全量广播就不合适了。改造思路是注册时让客户端带一个分组名服务端按组维护多个RemoteCallbackList// IRemoteDataService.aidl interface IRemoteDataService { void setData(String key, String value); void registerCallback(IRemoteCallback callback, String groupName); void unregisterCallback(IRemoteCallback callback, String groupName); }服务端用ConcurrentHashMap按组存放回调列表private final ConcurrentHashMapString, RemoteCallbackListIRemoteCallback mGroupedCallbacks new ConcurrentHashMap(); Override public void registerCallback(IRemoteCallback callback, String groupName) { RemoteCallbackListIRemoteCallback list mGroupedCallbacks.get(groupName); if (list null) { RemoteCallbackListIRemoteCallback newList new RemoteCallbackList(); RemoteCallbackListIRemoteCallback old mGroupedCallbacks.putIfAbsent(groupName, newList); list old ! null ? old : newList; } list.register(callback); }putIfAbsent在这里是必须的两个 Binder 线程同时注册同一个新分组时如果先containsKey判断再put后写的会覆盖先写的列表导致先注册的回调丢失。推送时先按组取出列表再走标准的 beginBroadcast 遍历。5.3 服务端主动断开指定客户端的完整写法有时候服务端要主动踢掉某个客户端版本过旧、身份校验失败、或者占用资源超限。踢人的本质是从所有回调列表里移除该客户端的回调对象。前提是你还持有这个客户端的回调引用所以需要一份「身份 → 回调对象」的映射。结合 5.1 的注册逻辑踢人操作长这样public void kickClient(String packageName) { IRemoteCallback cb mClientRegistry.remove(packageName); if (cb ! null) { mCallbacks.unregister(cb); } }注意unregister的匹配规则它按 Binder 底层对象比较不是按 Java 的equals。两个内容完全相同的回调 Stub 实例如果底层 binder 不同unregister就移除不了目标。所以注册和注销必须使用同一个回调对象不能一边注册一边 new 新的。另外踢人只是从回调列表移除客户端的绑定连接还在客户端下次调用setData时服务端方法照样执行只是不会再收到推送。要彻底断开连接需要客户端自己感知后解绑服务端这边能被踢的只有回调订阅权。6. 多客户端场景的验证技巧从模拟多进程到定位线程池问题写完了不等于能跑。多客户端 AIDL 的验证是最容易被跳过的环节——单客户端测试永远测不出并发和回调管理的问题。我一般用三条路做验证。第一真机多进程模拟多客户端。不需要起多个模拟器在同一个 App 里把几个 Activity 分别放进不同进程manifest 里给 Activity 配android:process各自绑定同一个服务并注册回调就能在同一台设备上模拟出多个客户端。启动三个不同进程的页面在任意一个页面上调setData观察另外两个进程的页面是否都收到了onDataChanged。这一步能验证广播链路但测不出跨应用的真实身份识别。第二用adb shell dumpsys activity services检查绑定关系adb shell dumpsys activity services com.example.projectx输出里能找到服务端的 ServiceRecord下面列出了每个客户端进程的 Connection 条目。如果发现同一个客户端包名下面挂着多条 Connection说明绑定没有成对解绑——这是回调堆积的前兆。结合 4.1 的场景反复进出页面后再执行这条命令能看到连接数只增不减。第三验证 Binder 线程池是否被占满。确定服务端进程的 PID 后直接查线程adb shell top -H -p 服务端pid | grep binder正常的服务端进程里能看到binder_1到binder_16这些线程绝大多数时间是 S 状态休眠等待。压测时起 10 个客户端同时调setData再跑这条命令如果大部分 binder 线程变成 R 状态运行中且长时间不回落说明 AIDL 方法体里有耗时操作在占线程。这条命令比看日志直观得多是我排查多客户端卡顿的第一手段。最后一个习惯每次改完 AIDL 接口先清理再重编译。AIDL 生成的 Java 文件偶尔会因增量编译没有刷新服务端和客户端拿着不匹配的接口定义互调运行时报TransactionFailedException或者ClassCastException看起来像玄学其实是编译产物过期。先清后编一次能省下至少一小时排查时间。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑