简介:面向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 或直接 Binder
AIDL 只是 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 对象入参出参必须写定向 tag:in 表示客户端传入服务端,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 List<ISharedStatusCallback> 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; List<ISharedStatusCallback> 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:exported="true" android:permission="com.demo.sharedservices.permission.BIND_STATUS"> <intent-filter> <action android:name="com.demo.sharedservices.action.BIND_STATUS" /> </intent-filter> </service>android: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 与 registerCallback
AIDL 接口里的普通方法是同步阻塞的。客户端调 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 一直返回 false,logcat 里什么都没有,或者只看到 Service not found。在旧版本设备上同一个包又是好的,越界到新系统就翻车。原因是新系统引入了包可见性过滤,客户端默认看不见其他包的 Service,隐式 intent 基本都会被过滤掉;即便用显式 componentName,有些场景下系统仍会查不到。解决办法不是去改 targetSdk,而是在客户端 Manifest 里声明要访问的包,并坚持使用显式 ComponentName。
<queries> <package android:name="com.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. 让共享服务长期存活:start+bid 组合、前台服务与并发同步
5.1 start+bid 组合: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; List<ISharedStatusCallback> 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 上来拿到的是空数据。解决方式是每次状态变更时顺便持久化到服务端进程的 SharedPreferences,onCreate 时读回。注意这个文件只能服务端读写,三个客户端不要碰。
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 和读取结果打到同一份日志里,前后一对比,问题在服务端还是客户端立刻能看出来。多端共享服务最怕的就是“自己测自己都是好的”,三端同时联调时把日志打全,能省下大量猜疑链时间。希望帮到你。
本文还有配套的精品资源,点击获取