十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AIDL多客户端共享Service:从Binder机制到全套工程实践

AIDL多客户端共享Service:从Binder机制到全套工程实践 简介这是一份面向 Android 开发者的 AIDL 多客户端通信示例工程定位清晰解决多个客户端应用同时连接并调用同一服务端接口时的服务绑定、代理获取与并发管理问题适合正在学习 Android 跨进程通信或需要在业务中实现多客户端绑定同一服务的开发者。压缩包共 92 个文件大小约 235KB包内既有可直接阅读和修改的 Java 源码、AIDL 接口定义、XML 布局与配置文件也有编译生成的 class 字节码、构建产物与 APK 安装包既能导入工程查看结构也能安装成品快速观察跨进程通信效果。工程按 T1、Service、T2 三个模块划分分别对应两个客户端与一个服务端完整演示了服务端注册 Binder、客户端通过绑定服务与连接回调获取 Stub 代理、调用跨进程方法并捕获 RemoteException 的完整链路。同时给出多客户端并发访问时服务端线程安全与资源调度的处理方法也包含数据交换量控制、接口调用频率优化等实践提示可帮助读者有效规避跨进程通信中常见的性能与稳定性陷阱。该资源已有 1049 人学习下载轻量紧凑但覆盖环节完整是理解 AIDL 多客户端协作的不错参考。 去年接了一个播放器改造项目需求本身不复杂通知栏要控制播放App内的设置页要改播放模式后来手表端也要接进来暂停/下一首。三个模块各在一个进程里却都得驱动同一个播放服务。这就是典型的多客户端调用同一个服务器场景在AIDL里涉及到的内容远不止写个接口跑通这么简单。这篇把完整方案和坑都摊开讲。1. 底层机制一个Service凭什么同时服务多个客户端先说结论系统在实现上做了最关键的复用多个客户端绑定到同一个Service时拿到的其实是同一个Binder代理对象。也就是说服务端不需要自己写任何多例逻辑。这条复用的链路是这样的第一个客户端执行bindService()时如果目标Service还没创建系统会先走onCreate()然后走onBind()把服务端返回的IBinder存起来。后续第二个、第三个客户端再来bindService()时Service已经存在系统不会再调用onBind()而是直接把第一次缓存的那个IBinder包装成客户端的代理对象返回。设计几个关键点onBind()在Service存活期间通常只执行一次所以不要在onBind()里写每次绑定都要重置状态的逻辑。所有客户端拿到的代理表面上像各自持有的对象底层指向同一份服务端数据。如果所有客户端都解绑了系统会销毁Service下次再绑定时会重新走一遍创建流程这时onBind()会再次执行。这个Service销毁后再重建的场景很容易被当成Bug排查半天。Binder通信还有一个特性服务端方法默认运行在Binder线程池中多个客户端同时调用服务端就是并发的。这和后端接口差不多只是框架层已经帮你把跨进程的序列化和线程切换都封装掉了。2. 服务端搭建定义一个支持多客户端的基础骨架不管客户端有几个服务端代码的结构是固定的一个AIDL接口一个ServiceonBind()返回Stub实现。2.1 AIDL接口定义以播放器场景为例定义一个带回调注册能力的接口// IPlayerService.aidl package com.example.player.service; import com.example.player.service.IPlayerCallback; interface IPlayerService { void play(); void pause(); void seekTo(int position); void registerCallback(IPlayerCallback callback); void unregisterCallback(IPlayerCallback callback); }AIDL支持的类型不多基本类型、String、CharSequence、List、Map、Parcelable和AIDL接口。IPlayerCallback也是AIDL接口用于服务端主动往客户端推消息。双向通信是必须的否则客户端只能拉没法收。// IPlayerCallback.aidl package com.example.player.service; interface IPlayerCallback { void onPlayStateChanged(boolean isPlaying); void onPositionChanged(int position); }2.2 Service实现public class PlayerService extends Service { private final IPlayerService.Stub mBinder new IPlayerService.Stub() { Override public void play() throws RemoteException { // 播放逻辑 } Override public void pause() throws RemoteException { // 暂停逻辑 } Override public void seekTo(int position) throws RemoteException { // 跳转逻辑 } Override public void registerCallback(IPlayerCallback callback) throws RemoteException { // 稍后详细展开 } Override public void unregisterCallback(IPlayerCallback callback) throws RemoteException { // 稍后详细展开 } }; Override public IBinder onBind(Intent intent) { return mBinder; } }有一点很容易忽略onBind()里直接返回mBinder成员变量即可不要每次都new一个新的Stub。new出来的实例功能上没问题因为Binder底层是根据handle定位目标的但复用同一个实例语义更清晰也方便维护服务端共享状态。2.3 服务端配置客户端不在同一个App时Service需要声明导出。Android 12API 31之后没有显式设置android:exported的应用会直接无法安装。service android:name.service.PlayerService android:exportedtrue android:process:player /多客户端跨进程调用最好把Service放到独立进程避免占用UI进程内存。:player冒号前缀表示私有进程仅当前应用可以使用。如果要给别的应用调用就用不带冒号的全进程名比如com.example.player.service。android:exportedtrue意味着任何应用都能调建议加权限保护permission android:namecom.example.player.permission.REMOTE_SERVICE android:protectionLevelsignature / service android:name.service.PlayerService android:exportedtrue android:permissioncom.example.player.permission.REMOTE_SERVICE android:process:player /protectionLevelsignature表示只有用同一签名密钥签名的应用才有权绑定。调用方在自己的Manifest中uses-permission android:namecom.example.player.permission.REMOTE_SERVICE /不加权限的话任意应用都能bind进来搞事情在真实项目里就是事故。3. 客户端绑定多个模块各自持有代理状态却完全一致客户端绑定Service的代码是固定的多客户端场景下每个客户端都做同样的事绑定拿代理、解绑释放引用。public class PlayerClient { private IPlayerService mService; private final ServiceConnection mConnection new ServiceConnection() { Override public void onServiceConnected(ComponentName name, IBinder service) { mService IPlayerService.Stub.asInterface(service); } Override public void onServiceDisconnected(ComponentName name) { // 服务端异常死亡时回调正常解绑不会走这里 mService null; } }; public void bind(Context context) { Intent intent new Intent(); intent.setComponent(new ComponentName( com.example.player, com.example.player.service.PlayerService)); context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } public void unbind(Context context) { if (mService ! null) { context.unbindService(mConnection); mService null; } } }多客户端场景下的几个关键点绑定Intent必须显式。跨应用场景隐式Intent往往不生效。最简单的方式就是用setComponent()指定包名和类名。传入的ComponentName前者是Service所在应用的包名后者是Service全类名。多个客户端绑定同一个ServiceonServiceDisconnected在各客户端的表现是独立的。比如通知栏模块绑定的代理死了设置页的代理不一定受影响因为每个客户端持有的是各自的Binder代理对象。但如果服务端进程整个被杀那所有客户端都会收到onServiceDisconnected。绑定是异步的。bindService()返回后立即调用mService.play()会拿空指针。可以通过回调或LiveData把已连接状态广播给调用方。4. RemoteCallbackList多客户端回调的正确管理姿势几乎每个需要通知所有客户端的场景都会在这块栽跟头。先看错误示范private final ListIPlayerCallback mCallbacks new CopyOnWriteArrayList(); // 注册 Override public void registerCallback(IPlayerCallback callback) { mCallbacks.add(callback); }问题在于客户端进程被杀时服务端这边还留着对这个失效代理的引用。调用时会抛DeadObjectException但没法及时感知。自己维护List要么在广播时逐个 try-catch要么定期清理怎么处理都别扭。Android官方早就准备好了解法RemoteCallbackListT。private final RemoteCallbackListIPlayerCallback mCallbacks new RemoteCallbackList(); Override public void registerCallback(IPlayerCallback callback) { mCallbacks.register(callback); } Override public void unregisterCallback(IPlayerCallback callback) { mCallbacks.unregister(callback); } // 广播给所有客户端 private void broadcastPlayState(boolean isPlaying) { int count mCallbacks.beginBroadcast(); for (int i 0; i count; i) { try { mCallbacks.getBroadcastItem(i).onPlayStateChanged(isPlaying); } catch (RemoteException e) { // 客户端已死RemoteCallbackList下次会自动清理 } } mCallbacks.finishBroadcast(); }几个必须注意的细节beginBroadcast()和finishBroadcast()必须成对调用哪怕中间抛了异常也要在finally里调用finishBroadcast()否则后续注册/反注册会被锁死。广播期间不要做耗时操作回调是同步调用的会阻塞Binder线程。回调里不要调用可能触发注册/注销的代码beginBroadcast()到finishBroadcast()之间的锁会导致死锁或异常。RemoteCallbackList内部通过linkToDeath监听客户端死亡失效条目会被自动清理这就是它优于普通List的核心原因。5. 多客户端并发访问服务端的线程安全红线Binder线程池模型决定了一个客户端一个线程通通并行跑在服务端代码里。单客户端时可能碰到的并发问题多客户端时一定会碰到。举个例子服务端维护一个播放队列private final ListTrack mPlayQueue new ArrayList();客户端A往队列里加歌客户端B同时取队列首播如果没有任何同步机制ConcurrentModificationException甚至数据错乱随时可能出现。几种常用的处理方式方式一synchronized或ReentrantLock保护共享数据结构适合短操作直接给数据加锁最简单private final Object mLock new Object(); Override public void addTrack(Track track) { synchronized (mLock) { mPlayQueue.add(track); } } Override public Track getNextTrack() { synchronized (mLock) { return mPlayQueue.isEmpty() ? null : mPlayQueue.remove(0); } }方式二SingleThreadExecutor串行化Binder调用适合逻辑本身较重的场景。所有AIDL实现方法都提交到同一个单线程线程池执行保证Service内部不会有并发执行的问题private final ExecutorService mSerialExecutor Executors.newSingleThreadExecutor(); private final IPlayerService.Stub mBinder new IPlayerService.Stub() { Override public void play() { mSerialExecutor.execute(() - doPlay()); } };这样做的代价是客户端调用play()后服务端执行是异步的方法返回先于实际播放完成。如果客户端需要等结果就得通过回调确认。Binder线程始终建议快进快出。AIDL方法里不要直接执行耗时任务文件IO、网络请求这种动辄几十毫秒的操作直接放Binder线程里跑很快就会打满线程池导致整个服务卡死。正确做法是接活儿然后丢给专属线程池处理。6. 服务端如何区分这是谁身份识别与个性化数据多客户端共享同一个Service时很多业务需要区分当前调用者是谁。比如每个App有自己的播放列表或者不同模块要拿不同的初始化数据。AIDL天然提供了这个能力Binder.getCallingPid()和Binder.getCallingUid()。Override public void play() throws RemoteException { int callingPid Binder.getCallingPid(); int callingUid Binder.getCallingUid(); Log.d(PlayerService, caller pid callingPid , uid callingUid); // 根据uid做差异化逻辑 }Binder.getCallingUid()返回的是Linux层面的用户ID同一App的所有组件不管在哪个进程uid相同。想更精细地区分可以把客户端身份标识作为参数传给AIDL方法interface IPlayerService { void registerClient(String clientId, IPlayerCallback callback); void unregisterClient(String clientId); }用clientId作为key在服务端维护一张ConcurrentHashMapString, ClientRecord记录每个客户端的回调和偏好设置。服务端的生命周期管理要注意客户端注册后没有反注册会产生僵尸记录。比较稳妥的方案是注册时监听死亡回调public void registerClient(String clientId, IPlayerCallback callback) { clientMap.put(clientId, new ClientRecord(callback)); try { callback.asBinder().linkToDeath(() - clientMap.remove(clientId), 0); } catch (RemoteException e) { clientMap.remove(clientId); } }客户端崩溃时linkToDeath触发清理避免Map越积越大。这是多客户端场景里非常实用的兜底策略。7. 踩坑实录多客户端联调时最常见的六个问题七个坑太多写六个吧——最后这个别的文章很少提。坑一bindService()返回false连不上原因排查顺序Service的android:exported是否为trueAndroid 12后不写直接安装失败。是否声明了android:permission客户端是否已声明对应uses-permission。绑定Intent是否隐式。跨应用必须setComponent。直接显式用包名 类名是成功率最高的写法。坑二客户端被杀后服务端还残留回调引用这个上面已经说了RemoteCallbackList解决。如果是自己维护Map务必加linkToDeath。坑三onServiceDisconnected没有触发直接重绑失败onServiceDisconnected只在连接意外断开时回调。客户端主动unbindService不会触发。重连逻辑不能只依赖这个回调要有明确的主动重绑机制。坑四所有客户端解绑后Service没马上销毁再绑定时上次的脏数据还在onBind()返回的对象还在但客户端全解绑后Service可能存活一段时间比如还有startService在跑。再次绑定时拿到的是旧状态。建议客户端绑定成功后主动拉取一次完整状态而不是等服务端推。坑五Binder回调里的主线程更新问题onPlayStateChanged这个回调跑在Binder线程。在回调里直接更新UI会崩。客户端收到回调后必须runOnUiThread或切到主线程Handler。服务端设计回调本就该精简只发数据不做UI。坑六AIDL接口变更后没重编运行时各种诡异异常AIDL接口加了一个方法却只改了服务端没重新编译客户端运行时很容易出现NullPointerException或TransactionTooLargeException。方法是按顺序和类型匹配的两端版本不一致参数解析就会错位。服务端和所有客户端的AIDL文件必须保持同步改完接口全部模块重新构建。8. 工程化建议多客户端场景下从开始就要想清楚的事最后分享几个项目落地层面的建议一、封装客户端代理库。把bind/unbind、状态缓存、回调线程切换这些都封成一个库模块。客户端只有传个Context进来拿一个实例调三个方法的体验。多端接入的成本才能压下来。二、服务端状态机要单一。所有客户端都控制同一个播放器服务端的pause()和seekTo()必须维护同一份状态。多客户端的本质是入口多但内核只有一个状态分散在服务端是不可接受的。三、为心跳维护留点余地。长时间不通信的连接Binder不会自动断开但服务端注册的回调可能已经失效。建议客户端在onResume等时机重新注册回调幂等设计。四、Service进程崩溃的恢复。Android系统会在绑定场景下自动重启Service但Binder句柄需要客户端重新绑定。可以在onServiceDisconnected里延迟重试比如3秒后重绑。五、善用oneway关键字。AIDL方法声明为oneway后客户端调用不阻塞立即返回适用于通知推送这类不需要等待结果的场景。多客户端场景下能显著减少Binder占用。注意oneway方法不能有返回值也不能抛异常。我们当时的播放器三端接入后核心Service代码基本没怎么动三个客户端各写各的。架构理解清楚了AIDL多客户端真没有想象中那么玄。最重要的是先想明白那两条底层机制客户端分享同一个Service实例Binder方法天然并发执行。想明白这两点设计方案时就不会跑偏。本文还有配套的精品资源点击获取
返回列表