ARTICLE DETAIL

资讯详情

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

Android跨进程通信全解析:Binder、AIDL与Messenger的选型与实践

Android跨进程通信全解析:Binder、AIDL与Messenger的选型与实践 说实话Android 跨进程通信这一块很多人是越学越懵Service 生命周期刚搞明白紧接着 Binder、Messenger、AIDL 三个名词砸过来每个看起来都能做 IPC但底层到底怎么转的适合什么场景网上资料常常各说各话。这篇文章是《聊聊 Android 中的 ServiceBinder、Messenger、AIDL》的第二篇默认你对 Service 的基本用法已经有概念重点说清楚 Binder 的通信模型、AIDL 从编写到运行的完整链路、Messenger 在什么情况下比 AIDL 更省事以及我实际项目里踩过的坑和沉淀下来的选型经验。我需要先抛一个很多人忽略的结论Binder 不是 Service 的附属品而是整个 Android 系统跨进程交互的地基。Service 只是把 Binder 暴露给外界的壳AIDL 和 Messenger 都是在这个壳之上简化开发的工具。把这层关系想明白后面所有代码都是顺理成章的。1. 为什么 Android 坚持用 Binder 做跨进程通信1.1 传统 IPC 方案在移动端的尴尬Linux 本身有管道、消息队列、共享内存、Socket 这些跨进程通信手段但拿它们直接做 Android 的应用层 IPC都有明显的短板。管道和消息队列本质是内核缓冲区上的数据流数据要经历“用户态 → 内核态 → 用户态”的多次拷贝字节流的语义也不适合表达“调用某个对象的方法”这种请求。Socket 虽然通用但每次通信都要走收发缓冲区和网络协议栈性能和开发成本都不占优。共享内存是性能最好的可它没有统一的权限控制机制多进程并发读写时的同步问题也得自己解决一旦出错非常隐蔽。Android 要面对的是大量第三方应用共享同一个系统安全问题优先级极高。开发者绝不应该拿到哪个进程就能随便读共享内存反过来系统也不希望应用之间互相窥探。传统 IPC 方案在权限方面都太薄弱了。所以 Android 需要一套“性能不差、权限可控、调用方式像函数本地调用”的跨进程方案这就是 Binder 的设计初衷。1.2 Binder 的通信模型一次拷贝、代理对象、内核驱动Binder 最初来自 OpenBinder 项目Android 在 Linux 内核里加了 binder 驱动并把它演变成了现在这套机制。很多人只记住“一次拷贝”这个优点但真正让 Binder 好用的是它的代理模型。客户端进程里拿到的 IBinder 对象并不是服务端那个真实对象的引用而是一个 BinderProxy。客户端调用某个方法时实际是调用 BinderProxy.transact()把方法编号、参数序列化到 Parcel 里交给内核中的 binder 驱动驱动把数据传递给服务端进程里对应的 Binder 实体触发 onTransact() 回调服务端解析参数、执行业务逻辑再把结果写回 Parcel沿原路返回。传统 IPC 通常要两次数据拷贝Binder 通过 mmap 把服务端进程的用户空间和内核缓冲区做了内存映射数据从发送方进入内核后只需要直接拷贝到接收方的映射区域这就是“一次拷贝”的由来。传输效率比管道和 Socket 高出不少。至于权限binder 驱动在每次通信时都会带上调用方进程的 UID 和 PID服务端可以根据这些信息决定是否响应。App 开发时虽然很少直接感知这一层但理解 UID/PID 跟随事务走就会明白为什么服务端能通过 checkCallingPermission 判断调用者身份。1.3 Service 和 Binder 到底是什么关系很多人学到这里会绕进去Service 不是四大组件吗怎么又跟 Binder 扯上了你在 Activity 里调用 bindService 时系统 AMS 会唤醒目标进程的 Service然后 Service 通过 onBind 返回一个 IBinder 对象。AMS 把这个 IBinder 通过 Binder 通信传回客户端进程再以 ServiceConnection 回调里的参数形式交给你。也就是说Service 只是负责产生和持有 Binder 实体的容器真正跨进程的通道是那个 IBinder。可以这样理解Service 是“电话总机”Binder 是“电话线”AIDL 是“通话协议”Messenger 是“对讲机”。不同电话线承载着不通形式的对话但底层都是 Binder。2. AIDL从编写到跑通第一个远程调用2.1 AIDL 文件怎么写、放哪、代码生成在哪AIDL 全称 Android Interface Definition Language它存在的原因很简单Binder 的原生接口 transact/onTransact 只认 Parcel 里的字节顺序如果所有业务逻辑都要开发者自己手动序列化和反序列化够痛苦。AIDL 相当于帮你写了一层脚手架你定义接口签名编译时自动生成 Stub 和 Proxy 两端代码。编写步骤我建议固定成这样在src/main/aidl目录下创建以.aidl结尾的文件。文件里声明包名例如package com.example.music.player;。使用 interface 关键字定义方法方法参数要用方向修饰符标记。如果有自定义对象先实现 Parcelable并在同包名下编写对应的.aidl声明文件。点击 Android Studio 菜单栏的 Build Sync Project或者直接 Run。这份代码示例是一个音乐播放器的远程接口// IMusicPlayer.aidl package com.example.music.player; import com.example.music.player.MusicInfo; interface IMusicPlayer { void open(in MusicInfo info); void play(); void pause(); MusicInfo getCurrentInfo(); void registerCallback(IMusicCallback callback); void unregisterCallback(IMusicCallback callback); }自定义对象 MusicInfo 必须可在进程间传递所以实现 Parcelable并额外写一个同名入股声明文件// MusicInfo.aidl package com.example.music.player; parcelable MusicInfo;编译之后你在app/build/generated/aidl_source_output_dir/对应路径下能看到自动生成的IMusicPlayer.java。里面包含三层东西接口本身、静态内部类 Stub服务端继承实现、静态内部类 Proxy客户端自动持有并用于调用。建议你实际打开这个文件看一眼里面的 transact 和 onTransact 代码就是 Binder 通信的真实逻辑。2.2 AIDL 文件生成失败最常见的几个原因我见过太多人在这步卡住文件后缀是.java或者把.aidl放在java目录下编译自然找不到。排查顺序很有讲究检查文件请确认位于src/main/aidl而不是src/main/java。检查后缀是.aidl不是.aidl.txt。检查接口里的 import 路径和项目里实际的包名是否完全一致注意大小写。执行 Build Clean Project再 Rebuild Project。最后再试 File Invalidate Caches / Restart。如果你把 AIDL 放到自定义源码目录记得在模块的build.gradle里显式声明 sourceSetsandroid { sourceSets { main { aidl.srcDirs src/main/aidl } } }多模块共享 AIDL 时最稳妥的做法是把.aidl和 Parcelable 类拆到一个公共 library module客户端和服务端都依赖它并保持全项目包名一致。2.3 服务端实现不是重写接口而是继承 Stub服务端要做的不是直接实现 AIDL interface而是继承自动生成的Stub类。因为 Stub 本身是一个 Binder 实体它内部持有了执行方法分发逻辑所需的 onTransact 实现。public class MusicService extends Service { private final IMusicPlayer.Stub mBinder new IMusicPlayer.Stub() { Override public void open(MusicInfo info) throws RemoteException { // 注意这里运行在 Binder 线程池不是 UI 线程 // 如果 open 流程很长需要丢到自己的工作线程去处理 } Override public void play() { // 播放逻辑 } Override public void pause() { // 暂停逻辑 } Override public MusicInfo getCurrentInfo() { return mCurrentInfo; } Override public void registerCallback(IMusicCallback callback) { mCallbacks.add(callback); } Override public void unregisterCallback(IMusicCallback callback) { mCallbacks.remove(callback); } }; Nullable Override public IBinder onBind(Intent intent) { return mBinder; } }这里有个关键认知服务端 AIDL 方法默认跑在 Binder 线程池不是创建 Service 的线程也不是主线程。这意味着如果你的方法身体里有耗时操作会占住当前 Binder 线程导致其他远程调用在这个线程上排队。并发稍高时容易把 Binder 线程池吃满所以耗时逻辑要么主动开线程要么把一个轻量级接口拆成多个方法让调用方分步查询。2.4 客户端调用bindService 和 Stub.asInterface客户端流程很固定先把 Service 绑起来再拿到 IBinder 转换成接口。public class MainActivity extends AppCompatActivity { private IMusicPlayer mPlayer; private final ServiceConnection mConnection new ServiceConnection() { Override public void onServiceConnected(ComponentName name, IBinder service) { mPlayer IMusicPlayer.Stub.asInterface(service); try { mPlayer.play(); } catch (RemoteException e) { // 服务端进程可能已经挂掉需要做重连或提示用户 } } Override public void onServiceDisconnected(ComponentName name) { mPlayer null; } }; Override protected void onStart() { super.onStart(); Intent intent new Intent(this, MusicService.class); intent.setPackage(getPackageName()); bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } }Android 8.0 之后绑定服务必须显式指定组件名和包名直接用隐式 Intent bindService 会抛异常。客户端这里最容易犯的错是在主线程直接调远程方法。AIDL 调用是同步的客户端调用线程会阻塞等待服务端返回。如果服务端逻辑超过 5 秒可能在 Android 新版本上引发 ANR。「oneway」关键字能把调用变成单向异步但代价是不再等待返回值所以更适合回调、通知这类场景。2.5 数据方向in、out、inout 的底层差异AIDL 参数方向不是摆设它直接影响 Parcel 的序列化和反序列化逻辑。in参数从客户端流向服务端默认值是 in。客户端传入什么服务端就收到什么。out参数从服务端流向客户端。服务端拿到的对象是“空的”只在返回时才把服务端修改后的结果写回客户端。inout双向传递客户端传入的原始对象会先到服务端服务端修改后再回传客户端。inout看起来最方便但它会让 Parcel 同时写入和读取两边数据开销最大。普通业务里能用 in 就用 in避免为性能埋雷。自定义 Parcelable 对象做 inout 时经常出现服务端收到某个字段为空或者客户端收到被清空的对象。原因大多出在 writeToParcel 和 CREATOR.createFromParcel 字段顺序不一致这个必须两边严格对齐。2.6 回调监听器无效的 oneway 设计等于白写远程调用往往需要服务端主动通知客户端比如播放器状态变化。做法是把客户端的回调接口传给服务端服务端在状态改变时调用这个接口。回调接口建议用 oneway 修饰// IMusicCallback.aidl package com.example.music.player; import com.example.music.player.MusicInfo; oneway interface IMusicCallback { void onStateChanged(int state, MusicInfo info); }oneway 能防止服务端同步回调时被客户端拖住。但 if 你没把回调接口设计成 oneway客户端 onStateChanged 里的耗时逻辑会阻塞服务端 Binder 线程导致整个播放器接口响应变慢。客户端注册时传的是本地的 Stub.asInterface 包装对象try { mPlayer.registerCallback(mCallback); } catch (RemoteException e) { // 处理异常 }服务端注册完 callback 后要留意客户端进程崩溃时留下的 zombie proxy。可以给客户端 binder 添加 DeathRecipient或者最直接地在调用 callback 的方法里捕获 RemoteException、DeadObjectException 后从列表里移除。for (IMusicCallback callback : mCallbacks) { try { callback.onStateChanged(state, info); } catch (DeadObjectException e) { mCallbacks.remove(callback); } catch (RemoteException e) { // 非致命错误 } }3. MessengerAIDL 之上更省心的封装3.1 Messenger 到底封装了什么很多人觉得 Messenger 和 AIDL 是完全平行的两套方案其实不是。Messenger 内部底层仍然是 Binder 和 AIDL它只是把繁琐的接口定义、Stub 实现、返回值处理全都包起来了让你只跟 Handler 和 Message 打交道。从源码看Messenger内部持有一个IMessenger对象而IMessenger就是系统内置的 AIDL 接口。服务端通过new Messenger(handler)创建实例然后getBinder()返回 IBinder客户端拿到 IBinder 后构建Messenger对象就能调用send(Message)发送请求。这套封装带来的最大改变是线程模型被简化了。服务端并不会每次请求都开新线程而是把 Message 交给 Handler进入 Handler 所在的 Looper 队列。如果你是用主线程 Looper 创建 Handler那么所有请求都会在主线程串行执行如果用 HandlerThread 的自定义 Looper就在子线程串行执行。3.2 客户端给服务端回传数据replyTo 的使用一个完整的 Messenger 通信不只是客户端单向发消息还经常需要服务端回传结果。这套双向通信用到的是 Message 里的 replyTo 字段。服务端代码public class MessengerService extends Service { private static final int MSG_PLAY 1; private static final int MSG_REPLY_STATE 2; private final Messenger mMessenger new Messenger(new Handler(Looper.getMainLooper()) { Override public void handleMessage(Message msg) { if (msg.what MSG_PLAY) { // 收到播放请求业务处理 // 通过 msg.replyTo 反向通知客户端 Message reply Message.obtain(null, MSG_REPLY_STATE); Bundle data new Bundle(); data.putString(state, playing); reply.setData(data); try { msg.replyTo.send(reply); } catch (RemoteException e) { // 客户端可能已经断开 } } } }); Override public IBinder onBind(Intent intent) { return mMessenger.getBinder(); } }客户端需要提前创建一个用于接收回复的 Handler 和 Messengerprivate final Messenger mClientMessenger new Messenger(new Handler(Looper.getMainLooper()) { Override public void handleMessage(Message msg) { if (msg.what MSG_REPLY_STATE) { // 更新 UI } } });发送时把 replyTo 带上Message msg Message.obtain(null, MSG_PLAY); msg.replyTo mClientMessenger; mService.send(msg);服务端拿到msg.replyTo后反过来将答复发给客户端的 Handler。整个链路依然走 Binder但业务代码里完全不出现 Parcel、transact 这些细节。注意两个坑。Message.obtain 能复用对象但不要往队列里塞同一个 Message 引用后还去修改它。另外Message 的 setData 只支持 Bundle 能承载的数据跨进程时同样受 Binder 事务大小限制大文件不能走这条路。3.3 什么场景下应该用小 Messenger 而不是硬上 AIDL我见过一些团队业务就三五个动作非要把整个接口写成 AIDL。开发成本被拉高不说接口变动时还得同步改 AIDL 文件和 Stub 实现特别痛苦。Messenger 的优势在于不需要手写 AIDL 文件新同学上手快。请求天然按消息队列串行不需要考虑并发同步。双向通信只依赖 replyTo模式固定改造成本低。适合高频小消息例如播放器控制、游戏房间信令、简单状态同步。局限也很明显接口能力受限只有 Message 一种协议。默认串行处理不适合大量并发独立请求。无法直接调用一个带复杂返回值的方法只能靠异步消息回传。所以如果需求是以“动作”和“事件”为主例如“播放”、“暂停”、“状态变了”用 Messenger 会省很多事如果需求是以“查询”和“结果”为主例如“获取批量订单”、“提交一个对象并返回校验结果”AIDL 更合适。4. 三种方式的选型思路别只盯着技术先想清楚场景4.1 先回答三个问题再做技术选型我可以把平时习惯的决策链路整理一下。你拿到一个“Service 通信”需求先别急着上 AIDL问自己三个问题客户端和服务端在不在同一个进程通信是消息驱动还是请求-响应式的复杂接口调用同一时刻并发量高不高服务端愿不愿意串行处理如果不在同一个进程再往下判断。如果只是简单动作 状态通知直接选 Messenger。如果要做 CRUD、批量查询、多客户端并发各自拿到独立结果AIDL 是正解。还有一种情况客户端和服务端本来就是同一个进程只是你把代码写进 Service 里而已。这时候连 Binder 对话都不需要直接用 LocalBinder 强转返回 Service 实例省掉跨进程的全部开销。把 LocalBinder 方案放在心里可以避免很多无意义的性能损耗。4.2 LocalBinder同进程场景的“作弊”方案LocalBinder 是 Binder 的子类但它不是给跨进程对话设计的。用法是在 Service 里定义内部 LocalBinderonBind 时返回它客户端拿到 IBinder 后直接强转public class LocalService extends Service { private final IBinder mBinder new LocalBinder(); public class LocalBinder extends Binder { LocalService getService() { return LocalService.this; } } Override public IBinder onBind(Intent intent) { return mBinder; } public void doHeavyWork() { // 直接调用的本地方法 } }客户端在 onServiceConnected 里强转拿到 Service 实例直接调用 doHeavyWork。没有序列化、没有 Parcel、没有 Binder 线程池本质上就是普通对象调用。代价是彻底失去跨进程能力服务端和客户端必须同进程。4.3 一次 Binder 事务 1MB数据量太大怎么办无论 AIDL 还是 Messenger数据最终都要写入 Parcel 交给 Binder 驱动。单个 Binder 事务可用的缓冲区是有限制的量级在 1MB 左右超了就会抛TransactionTooLargeException。传大 Bitmap、传几十 M 的日志文本都是踩这个坑的高发区。解决办法不是加缓冲区而是换思路把数据写到临时文件用 FileProvider 生成 content:// Uri再把这个字符串 Uri 传给服务端。把数据存进数据库或 ContentProvider跨进程只传主键 ID 或查询条件。对图像、大日志这种场景别奢望用 AIDL 直达先落盘再传路径。依赖 FileProvider 时服务端要根据 Uri 调用 ContentResolver.openInputStream 读取文件。要记得给对方进程配置合适的 grantUriPermission 或 ClipData避免权限校验失败。4.4 进程被杀了DeadObjectException 的应对Binder 远端进程随时可能因为重启、异常崩溃、系统回收而被杀。此时客户端调用远端方法会抛DeadObjectException它是 RemoteException 的子类。处理套路有三种。第一种是在所有调用处捕获 RemoteException统一弹提示或者重试。第二种是绑定 Service 时给 binder 挂上 DeathRecipient进程死后及时收到通知并触发重连。第三种是 onServiceDisconnected 回调它表示绑定断了要改绑定状态并清理资源。这三种方式各有侧重实际项目里通常组合使用。服务端可能重启客户端最好在 onServiceDisconnected 后延迟一段时间再自动重新 bindService避免高频重试打崩系统。5. 高频踩坑与调试实录5.1 典型问题速查表为了方便查阅我把自己在项目里踩过的坑整理成了表格按现象排布现象常见原因解决方向AIDL 文件无法生成 Stub放错目录、后缀不对、包名不一致放进 src/main/aidlClean 后 RebuildbindService 返回 falseAndroid 8.0 用隐式 Intent使用显式 Intent绑定包名和组件名调用远端方法卡住或 ANR主线程同步调用且服务端耗时异步线程调用方法用 oneway服务端逻辑另开线程DeadObjectException服务端进程被杀或重启捕获 RemoteException重连DeathRecipientTransactionTooLargeException单次 Binder 事务超过约 1MB文件传输、URI、数据库回调事件收不到回调接口没声明 oneway服务端线程阻塞回调接口加上 oneway检查注册/反注册服务端方法异常却不抛错自定义 Parcelable 字段顺序不一致对齐 writeToParcel 和 CREATOR5.2 Binder 死锁别在 Binder 线程里再做 Binder 调用这是我性能问题最深刻的一次。服务端某个 AIDL 方法内部去调用了另一个 Service 的远端方法结果两个 Binder 线程互相等待。普通应用进程的 Binder 线程池默认上限是十几这个量级一旦被类似嵌套等待占满整个进程对外通信基本瘫痪。关键原则是Binder 线程池线程是共享的不要在 Binder 线程里同步等待另一个 Binder 调用。如果确实需要聚合多个服务的数据把任务拆成一个异步编排或者用 oneway 接口发起多个独立调用再统一通过回调汇总。5.3 调试技巧日志线程名和 dumpsys排查 AIDL 相关问题时最便宜的调试手段是在服务端方法入口打日志注意观察日志输出的线程名。binder:1234_5如果看到这种线程名说明这段代码已经运行在 Binder 线程池不是主线程可以帮你判断是有没有误用 UI 线程。需要确认 Service 绑定状态时用 adb 命令adb shell dumpsys activity services 你的包名这个命令会把当前进程注册的所有 Service、绑定情况、启动参数列出来。排查 onServiceDisconnected 没触发、绑定丢失这类问题很有用。5.4 一个比较稳妥的项目演进路径最后分享我个人的长期做法新项目里先不要为远期复杂度做设计。如果跨进程通信场景不复杂先用 Messenger 把主链路跑通。等到真的出现需要细粒度接口、复杂对象、多客户端并发获取独立结果的场景再升级到 AIDL。原因很实际Messenger 改成 AIDL 的成本并不高你自己手动实现 Parcelable、写一个 Stub 并注册回调即可。但反过来如果一开始就选了 AIDL接入方和调用方都会面对更高的接口复杂度开发周期明显变长。另一个习惯是如果公司内有多个模块共用同一套跨进程接口务必把 AIDL 和 Parcelable 单独放到公共模块里。所有客户端依赖同一个版本避免出现服务端升级接口、客户端包里面还残留旧 Parcelable 的低级问题。踩坑踩多了你会发现跨进程通信的所有问题本质上都是对“Binder 是双向通道但每个方向都有容量和生命周期”这句话理解不到位。无论选哪条技术路线有一条铁律永远不变把跨进程异常当常态处理不要假设服务端永远活着这样你的 Service 才算真正健壮。
返回列表