ARTICLE DETAIL

资讯详情

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

Android跨进程通信:Binder、AIDL与Messenger原理及实战指南

Android跨进程通信:Binder、AIDL与Messenger原理及实战指南 开门见山说一句做Android跨进程通信绕不开Service、Binder、Messenger和AIDL这四样东西。我刚入行那年被Binder机制折磨得够呛后来在项目里真正用AIDL写完一个音乐播放器的后台控制又把Messenger用在IM模块的进程间消息投递上才慢慢摸清楚它们之间的关系。这篇是系列第二篇默认你已经知道Service怎么启动和绑定我们直接撕开跨进程通信这层窗户纸。这篇文章适合谁刚接触跨进程通信、容易把Binder和AIDL搞混的同学以及已经在项目里用但总遇到崩溃或卡顿的老哥。我不打算照本宣科而是从“系统为什么要这么设计”、“代码怎么写才不会翻车”两个角度来讲把原理和工程实践拧在一起。1. 三种通信方式的定位为什么Android偏偏选了这几样1.1 从“进程隔离”说起一切跨进程通信的前提Android的每个应用默认跑在自己的进程里进程之间内存隔离你在一个进程中new出来的对象另一个进程里根本没有地址可以访问。这是Linux内核提供的基本安全边界也是多任务系统能稳定运行的根本。可一个App往往不止一个进程比如大型播放器把播放逻辑放在独立进程避免UI卡顿IM软件把推送服务单独扔一个进程保证主界面被杀后消息还能收。这时问题就来了进程之间怎么把数据交给对方传统Linux有管道、共享内存、Socket这些手段但Android没直接照搬。原因很朴素性能要够快不能每次通信都来一次完整的网络协议栈传输的数据要支持对象语义不能只传字节流再手动解析更重要的是安全系统服务要让每个App都能调用但绝对不能允许App随便伪造身份乱调别人的数据。所以Binder被推上台面整个Android系统的跨进程通信底座就是它。四大组件之间的调用、系统服务比如ActivityManager、WindowManager的访问、我们写的AIDL接口底层全走Binder。1.2 Binder、Messenger、AIDL三者是“底层与封装”的关系很多新手会把Binder和AIDL放在对立面去比较这本身就搞错了方向。它们不是竞争关系而是不同层次的东西Binder是内核提供的一套进程间通信机制相当于高速公路和交通规则AIDL是定义接口的语法工具编译后转换成Binder调用的具体代码相当于你编写目的地和行驶路线Messenger则是基于AIDL做的一层现成封装内部已经定义好了传输Message的接口相当于一辆已经帮你配好的车你只管坐上给司机指令就行。说得再直白点你用AIDL最终生成的代码里就是Binder驱动在做底层传输你用Messenger它内部的IMessenger接口本身就是一个AIDL定义。所以真正要理解的只有两件事——Binder怎么搬运数据以及你怎么定义这份“搬运协议”。1.3 选型逻辑什么场景用哪个不是越复杂越好在实际开发中我的选型原则是这样的需求特征推荐方案理由临时获取一次服务端数据频率低自定义Binder就是绑定时直接拿到Binder对象调方法简单直接不用额外生成文件与远程服务持续通信方法固定且字段较多AIDL编译期检查参数类型工程化好需要远程服务主动回调客户端AIDL Listener双向通信的标准做法只是简单收发消息方法很少比如通知栏控制音乐Messenger内部帮你封装好了不需要自己写同步锁逻辑高频、大批量数据比如视频帧或日志流共享内存 Binder传递文件描述符避开Binder事务大小限制记住一条经验能用Messenger解决的别急着上AIDLAIDL写起来爽维护Review成本也高只有清晰的认识到底层机制才能在两者之间做出合理取舍。2. Binder机制的核心原理一次内存拷贝背后的设计智慧2.1 用户态、内核态与内存映射讲Binder原理绕不开内核态和用户态。操作系统为了保护硬件资源把CPU权限分成多个级别Android这边主要关心两个用户态普通App代码运行的地方和内核态系统内核代码运行的地方。普通App不能直接访问硬件、不能直接操作其它进程的内存必须通过系统调用进入内核态让内核帮你干这些事。传统IPC比如管道、Socket的流程是发送方先把数据从自己的用户态空间拷贝到内核态空间内核再把它拷贝到接收方的用户态空间。一次数据传递至少要经历两次拷贝效率低不说数据在中间还容易被篡改。Binder的高明之处在于引入了内存映射。发送方进程把用户态数据拷贝到内核态缓冲区同时接收方进程也通过mmap把这个内核缓冲区映射到自己的用户态空间。这么一来数据只需要一次拷贝就同时能被接收方直接访问。这就是Binder在性能上的核心优势——一次拷贝而不是传统IPC的两次拷贝。可能你会问为什么Android不直接用共享内存共享内存确实能省去拷贝但管理共享内存的代码太容易出安全问题而且Android的系统服务需要对调用方做严格的权限校验Binder机制天然地在每次事务中携带了UID/PID方便系统检查是谁在调用。安全性和性能两头都占了这才是Android把Binder当成系统基座的原因。2.2 ServiceManagerBinder世界的“电话总机”Binder通信有三个角色Client调用方、Server服务提供方、ServiceManager服务注册和查询中心。Server把自己的Binder实体注册到ServiceManagerClient通过ServiceManager查询到Server的代理引用然后直接通信。用一个生活化的类比就是ServiceManager是电话总机Server是酒店客房Client是住客。你入住时客房电话已经跟总机接通了注册住客想找客房服务调用服务只需要打给总机“帮我转接一下XX房间”查询接通后就直接对话不用总机在中途插嘴。在Android系统里ServiceManager本身也是一个Binder服务它的标识符是固定的0号。App进程启动后如果要调用系统服务首先要问ServiceManager要一个API接口也就是常见的getSystemService()。我们自己写的AIDL服务则不需要注册到系统的ServiceManager只需要在Service的onBind()里把自己暴露出来客户端通过bindService拿到引用即可因为Android框架层的ActivityManager帮我们做了服务的发现和绑定。2.3 Binder对象的两个形态实体与代理理解Binder还有一个关键点同一个Binder对象在Server进程里是实体Binder在Client进程里是代理BinderProxy。为什么因为实体本身在那个进程里是真实存在的对象而其它进程无法访问这个内存只能在本地创建一个“替代品”这个替代品知道如何把调用请求封装成数据包发往内核让Binder驱动找到真正的实体来处理。AIDL生成的代码里有一个asInterface()方法它会判断当前对象是实体还是代理如果本地就是一个Binder实体直接强转类型使用如果是一个BinderProxy则包装成代理对象Stub.Proxy。这正是AIDL能同时支持本地调用和远程调用的原因。你在写代码时完全不用关心对方进程在不在同一块内存里Binder把这层差异透明化了。但这层透明也会带来隐患比如调用一个远程Binder方法可能因为对方进程死亡而抛出DeadObjectException后面我会专门讲这个问题。3. AIDL实战从零编写远程服务接口3.1 AIDL文件里能写什么、不能写什么AIDL全称Android Interface Definition Language是一种接口定义语言语法很接近Java但支持的字段类型有限制。写之前必须清楚边界支持的类型Java八种基本数据类型int、long、boolean、float、double、byte、char、shortString、CharSequenceList有序列表元素类型必须受支持Map键值对键和值类型必须受支持Parcelable对象实现了Parcelable接口的自定义数据类其它AIDL接口。不支持的类型自定义的非Parcelable对象泛型static字段循环引用。在实际业务中最通用的做法是定义一个Parcelable实体类用它来承载结构化数据。写Parcelable类时记得要同时写两个方法writeToParcel()负责把字段写入ParcelCREATOR的createFromParcel()负责解析。Android Studio里可以通过插件自动生成但理解读写对称非常关键——写入顺序和读出顺序必须一致否则解析出来全是错位数据。3.2 一步步写出第一个AIDL接口我现在就完整带大家走一遍。假设我们要做一个远程音乐控制服务客户端传歌名让服务端播放服务端返回播放状态。第一步在module的src/main/aidl目录下创建包结构再创建接口文件IMusicPlayer.aidlpackage com.example.music; import com.example.music.Song; interface IMusicPlayer { // 传入歌曲信息返回播放是否成功 boolean play(in Song song); // 暂停播放 void pause(); // 获取当前播放进度毫秒 long getCurrentPosition(); // 服务端主动回调播放状态变化 void registerListener(IMusicCallback callback); void unregisterListener(IMusicCallback callback); }同时创建Song.aidl声明这个Parcelable类型package com.example.music; parcelable Song;再创建IMusicCallback.aidl作为回调接口package com.example.music; interface IMusicCallback { void onPlayStateChanged(boolean isPlaying, long position); }注意几个细节in表示客户端把数据传往服务端这个方向最常用out表示服务端把数据写回客户端inout双向。方向的差异会影响Binder事务的生成代码选择不当会多一次Parcel写入开销回调接口的定义和主接口要分开文件并且回调接口不能返回值void因为跨进程调用回调时如果等它返回客户端会被阻塞在binder线程里AIDL里不能用中文注释里的特殊字符避免编码不一致导致编译失败。第二步在Service实现类里实现接口public class MusicService extends Service { private final IMusicPlayer.Stub mBinder new IMusicPlayer.Stub() { Override public boolean play(Song song) throws RemoteException { // 这里运行在Binder线程池不要再开线程处理耗时任务 return playerEngine.start(song); } Override public void pause() { playerEngine.pause(); } Override public long getCurrentPosition() { return playerEngine.getPosition(); } Override public void registerListener(IMusicCallback callback) { callbackList.add(callback); } Override public void unregisterListener(IMusicCallback callback) { callbackList.remove(callback); } }; Override public IBinder onBind(Intent intent) { return mBinder; } }看一眼AS生成的IMusicPlayer.Stub代码你会发现里面有个asBinder()方法、一个onTransact()方法还有asInterface()方法。onTransact()运行在服务端的Binder线程池里客户端每次调方法底层都会发送一个transact()请求最终执行到这里根据方法编号code来分发执行哪个分支。这就是AIDL的本质接口方法被映射成一个个整型编号跨进程时只传编号和Parcel化的参数执行结果再以Parcel形式返回。第三步在客户端绑定并调用private IMusicPlayer mMusicPlayer; private ServiceConnection connection new ServiceConnection() { Override public void onServiceConnected(ComponentName name, IBinder service) { mMusicPlayer IMusicPlayer.Stub.asInterface(service); } Override public void onServiceDisconnected(ComponentName name) { mMusicPlayer null; } }; Intent intent new Intent(this, MusicService.class); intent.setPackage(getPackageName()); bindService(intent, connection, Context.BIND_AUTO_CREATE);3.3 绑定流程的坑显式Intent和自动创建凡是跨进程绑定强烈建议用显式Intent。Android 5.0之后隐式Intent无法启动服务国内各种定制ROM对隐式绑定限制更狠经常出现bindService静默失败。显式Intent必须带包名或ComponentName如果你的Service是一个独立module里的库服务可以在目标包里定义一个辅助方法返回Intentpublic static Intent createIntent(Context context) { Intent intent new Intent(context, MusicService.class); intent.setAction(com.example.music.REMOTE_SERVICE); intent.setPackage(com.example.music); return intent; }从源码层面看BIND_AUTO_CREATE标志位让系统在绑定前自动创建Service不需要先调用startService。大多数场景下远程音乐控制这类服务我们都希望它随绑随建、随解随毁这是最省资源的。3.4 线程模型与ANR跨进程调用为什么会卡死跨进程调用是同步阻塞的。客户端调用play()时当前线程会等待服务端执行完返回如果服务端处理慢客户端所在线程就一直挂着。所以客户端这边不要在UI线程直接调用耗时超过100ms的Binder方法否则ANR弹窗教你做人服务端这边onTransact()运行在Binder线程池默认多个客户端请求会并行处理如果方法里有耗时逻辑一定要转线程池执行否则占用一个Binder线程太久其它调用排队堆积。还有一个隐蔽的坑服务端一次性收到多个客户端的并发注册、数据上报时你如果用了某个集合来保存Listener要加锁或者用CopyOnWriteArrayList否则ConcurrentModificationException会从Binder线程里冒出来直接导致该进程崩溃。4. MessengerHandy版AIDL为什么至今没被淘汰4.1 Messenger内部结构一份现成的AIDL封装很多时候我们的远程服务其实就是收发几条消息不需要精心设计几十个方法。这时候上AIDL要维护接口文件、Parcelable实体、回调方法确实有点重。Messenger就是为这种场景准备的。Messenger的核心组成一个Handler处理远端发来的Message内部持有一个IMessenger的AIDL接口对象通过getBinder()返回给客户端。客户端拿到这个Binder后把它包装成自己的Messenger调用send(Message)向服务端发消息。如果客户端想让服务端回复只需要在Message里设置replyTo字段值为客户端自己的Messenger服务端就能用这个Messenger逆向发消息回来。这本质上是一个用Handler和Message做消息模型的、基于AIDL的封装。它的优点非常明显不需要自己写AIDL文件内部已经帮你写好了线程模型简单数据最终都会回到Handler所在的线程天然支持队列化处理消息一条条处理不容易并发打崩服务。4.2 服务端代码实现示例服务端public class MessengerService extends Service { private static final int MSG_PLAY 1; private static final int MSG_PAUSE 2; private final Messenger mMessenger new Messenger(new ServiceHandler()); private static class ServiceHandler extends Handler { Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_PLAY: String songName msg.getData().getString(song); // 执行播放逻辑 callbackToClient(msg.replyTo, 100, playing); break; case MSG_PAUSE: callbackToClient(msg.replyTo, 101, paused); break; } } } private static void callbackToClient(Messenger replyMessenger, int what, String info) { if (replyMessenger null) return; Message reply Message.obtain(); Bundle data new Bundle(); data.putString(info, info); reply.setData(data); reply.what what; try { replyMessenger.send(reply); } catch (RemoteException e) { e.printStackTrace(); } } Override public IBinder onBind(Intent intent) { return mMessenger.getBinder(); } }客户端绑定后通过Messenger(mService)得到服务端Messenger然后发送消息Message msg Message.obtain(); msg.what MSG_PLAY; Bundle data new Bundle(); data.putString(song, 告白气球); msg.setData(data); msg.replyTo mClientMessenger; mServiceMessenger.send(msg);客户端也要建一个Handler接收服务端回传的消息。Message.replyTo是一个跨进程可传递的对象底层自动转换成对应进程的Messenger代理不需要手动序列化。4.3 Messsage对象用的Bundle也有数据量限制调用send()时Message里的data是一个Bundle底层会走Binder事务。这个事务缓冲区默认大约是1MB超过这个大小会直接抛TransactionTooLargeException。图片、大字节数组就不要指望通过Messenger传了它适合传轻量级结构化消息。我见过有人用Messenger传Bitmap结果在小米和华为上页面崩溃排查半天才定位到是数据量超限。解决方案有两个方向传文件路径或图片Uri接收方自己去存储读文件如果非要传大内存对象绕到AIDL 共享内存的路线先共享文件描述符再直接读内存区域。4.4 为什么说Messenger比直接写AIDL更适合快速迭代团队协作时会发现AIDL接口文件一旦改了方法签名服务端和客户端必须同步更新否则onTransact()的方法编号对不上就会出现“调过去了但方法不存在”的诡异崩溃。而Messenger只传what常量消息协议通过文档约定双方只要按约定收发即可接口演进成本低很多。尤其在跨团队合作时别的团队不希望你直接依赖他们的AIDL代码用Messenger这种“消息协议”就少了一层代码耦合。代价是什么呢类型安全变差参数全都塞在Bundle里后端改了key不会在编译期暴露问题只能靠测试兜底。所以同一份代码里两个方案可以并存按模块边界划分就好。5. 进阶经验线程调度、死亡监听与错误排查5.1 Binder线程池是怎么工作的为什么调用会堵前面反复提到“运行在Binder线程池”这个线程池值得好好理解。每个进程里有一个Binder线程池默认最大线程数由BINDER_IPC_MAX_THREADS决定不是实际代码里默认值是16现在高版本系统会动态调整。当客户端发起一次Binder调用内核驱动会从服务端进程的Binder线程池里挑一个空闲线程来执行onTransact()。如果服务端方法里Sleep了三秒这个线程就占用了三秒。客户端调用多了线程池用完新的调用就会在内核里排队等待。所以如果服务端方法全是耗时操作你会发现所有跨进程调用变得越来越慢直到卡到ANR。排错建议服务端Binder方法里能做异步就异步别偷懒如果大量调用必须实时返回数据考虑给Service单独开一个进程android:process标签免得服务端Binder线程卡顿影响UI主线程用Binder.setThreadStrictModePolicy()在开发和测试阶段打开StrictMode能快速发现磁盘IO、网络调用发生在Binder线程里这类问题。5.2 客户端如何感知服务进程死亡跨进程通信最大的不确定性是服务端进程可能会被系统杀掉。系统进程有低内存回收机制后台Service在内存紧张时会被回收。客户端如果还拿着旧Binder代理去调用会抛出DeadObjectException。处理方式是监听服务端死亡mMusicPlayer.asBinder().linkToDeath(deathRecipient, 0);deathRecipient回调在Binder线程里不能直接更新UI要post到主线程。收到死亡通知后可以自动清理资源、提示用户、或者重新等待下次快速重启服务。反过来也要做一件事在onServiceDisconnected()里重置状态。必须注意onServiceDisconnected()和linkToDeath的回调时机不完全一致前者是系统ServiceConnection通知后者是Binder底层通知有时候两个都会触发代码里要做好幂等处理别把同一个清理逻辑执行两遍导致空指针。5.3 AIDL文件生成失败怎么办最全排查清单很多新手在AS里写AIDL编译报“Failed to generate aidl file”或者生成的类找不到通常跑不出下面几个原因错误现象原因处理办法生成的Stub类找不到包名不一致检查AIDL文件里的package声明一定要和AIDL所在目录结构一致aidl文件里import不到自己的Parcelable缺少parcelable声明先写实体类.java再写同名的.aidl文件里面只要一行parcelable Xxx;编译报“cannot find symbol”Parcelable类没有放在与AIDL同包的目录把实现类放进和AIDL一样的包结构用src/main/java下的同包名AIDL用src/main/aidl下的同包名服务端和客户端在不同module类型不一致两边AIDL目录不同步把AIDL文件放到公共module两边依赖同一个module引入第三方库的AIDL接口冲突多渠道打包或组件化重复依赖统一依赖版本别用implementation在多个子模块重复引入编译开关没开老版本ASAIDL模块未启用检查module的build.gradle确认sourceSets包含aidl目录我最常踩的坑是Parcelable类没有在同包名下。AIDL在com.example.music实体类在com.example.entity编辑器不报错一编译就爆炸因为生成的代码要强转成com.example.music.Song结果发现找不到这个类。解决方案其实很简单把实体类和AIDL都放在同一个包名路径下Java代码在java目录AIDL代码在aidl目录包名保持一致就行。5.4 跨进程传输大对象失败的处理思路Binder事务缓冲区大小属于系统限制不能盲目调大。高版本Android提供了TransactionTooLargeException异常用来指示本次事务数据超过限制。如果业务确实需要传大对象我的实战方案是拆分数据分多次事务传比如按页拉取列表用文件传递客户端把文件写进getExternalFilesDir()或getCacheDir()传给服务端一个文件路径服务端自己读文件对实时性要求高的场景用MemoryFile创建匿名共享内存把文件描述符通过Binder传给对方双方直接读写共享内存。第三种方式是把Binder和共享内存结合使用性能最好但复杂度也最高需要处理同步和生命周期。如果只是传10MB以内的数据用文件方案最省心注意文件权限和清理逻辑。5.5 多进程下的单例与静态变量陷阱一旦Service设置了android:process:remote整个服务就跑在独立进程里。此时你Main进程里的静态变量、单例对象在Remote进程里是另一份全新的两边改一个不会同步因为物理内存都隔离了。很多人在AIDL回调里想直接操作主进程的内存缓存结果发现值对不上还以为是线程问题其实根本不是是进程问题。建议把所有跨进程需要共享的数据尽量通过AIDL方法传参、返回值传递或者在两端各自维护同步逻辑不要指望静态变量做跨进程全局状态。6. 工程化落地项目里怎么组织这套东西才不烂尾6.1 接口先行先把AIDL定义为团队契约跨进程通信最忌讳“先写实现后补接口”。一旦接口定义不清晰两个进程的开发各自为政联调时各种类型不匹配。我的习惯是先把.aidl文件当作契约文档来写定义方法时把参数方向in/out/inout标清楚把可能的异常在注释里说明然后交给客户端和服务端的同学先行Review。甲方改需求时第一件事就是审AIDL协议变更而不是一上来就改实现代码。AIDL文件因为要在客户端和服务端同步强烈建议抽成独立module比如lib_common客户端和服务端都依赖它。这样接口变更会直接触发编译错误而不会在运行时静默失败。比起之前的Messenger那种软协议AIDL牺牲了一些灵活度换来的是强类型和编译期安全这是工程化取舍。6.2 回调过密的性能问题批量回调与节流如果你的服务端要高频推送进度比如播放位置每秒回调一次而客户端UI压根跟不上这个频率Binder线程和主线程都会遭殃。解决方案服务端适当降低回调频率比如只回调状态变化而不是每帧数据客户端收到回调之后用空闲消息或者Choreographer做节流只保留最后一次状态刷新UI回调方法里不要做耗时计算拿到数据直接post到UI线程简单透传。回调节流还涉及到内存泄漏问题。客户端进程注册Listener后如果Activity销毁了却没有反注册服务端还把回调对象攒着每次回调都拉起一个Activity的Lifecycle操作轻则空指针重则内存暴涨。所以注册/反注册成对这一原则在任何跨进程场景下都要刻在脑子里。6.3 实例代码的Service配置清单最后给出一份可直接抄写的Service注册配置把关键属性都列清楚service android:name.music.MusicService android:enabledtrue android:exportedtrue android:process:music_remote intent-filter action android:namecom.example.music.REMOTE_SERVICE / /intent-filter /serviceexported要设为true否则外部进程无法绑定但如果你的服务只给本应用内的另一个进程用可以不加intent-filter用显式Intent绑定然后exportedfalse也不是不行同一App的不同进程访问不受exported限制。需要外部App调用时再开exported同时结合permission做权限校验。很多安全漏洞就出自于远程服务没做权限控制别人拿到包名就能调你的方法传脏数据。6.4 从Binder到Service这套知识的完整闭环回顾一下整个系列的思路Service是Android提供的四大组件负责后台任务和跨进程服务bindService让客户端拿到IBinder这个IBinder底层就是Binder驱动创建的通信管道我们为了不在代码里手写晦涩的transact逻辑引入AIDL做接口描述、Messenger做消息封装。把这套链路理解透再去看系统源码里的ActivityManagerService、PackageManagerService、WindowManagerService你会发现它们本质上就是一群特殊的Binder服务。Android搭建了整个应用生态Binder就是支撑所有服务暴露和调用的骨架。学的时候可能觉得枯燥但一旦理解你再看系统原理、看第三方库源码、看跨进程设计都会有种通透了的感觉。我在实际项目中踩坑最多的一个是AIDL中线程切换没做好导致卡UI另一个是远程服务被杀之后客户端没有及时重连。每次复盘都发现只要把Binder线程模型和进程模型想清楚很多问题其实在编码前就能避免。写代码之前先画一画哪些数据要跨进程、调用发生在哪个线程、谁负责注册和反注册项目会稳得多。最后再分享一个我自己常用的调试办法在客户端调用AIDL接口的地方统一封装一个RemoteCaller每次调用包一层try/catch遇到DeadObjectException自动做一次重绑定并重试遇到TransactionTooLargeException把数据降级写入文件。这样线上即使出问题大部分也能自愈比把错误暴露给用户强。做基建的工程稳定比功能多重要得多。
返回列表