Android Binder通讯类图全解析:从角色到时序图的清晰地图
发布时间:2026/10/20 7:17:24 锦皓数字建站

很多Android开发者学Binder第一步就是去网上搜“Binder架构图”结果搜回来的图五花八门——有的画成一条总线有的画成三层同心圆有的干脆用几个箭头糊弄过去。看完之后更晕了谁在跟谁说话ServiceManager到底在哪驱动那层到底发生了什么这篇《Android Binder 系列二通讯类图》就是专门把Binder通讯类涉及的“图”一张张掰开揉碎讲清楚。我会从架构角色图、驱动内部结构、完整时序图、关键类图四个维度展开并把每个环节里最容易画错、最容易理解偏的地方单独拎出来说。适合正在啃Binder源码、准备面试、或者排查跨进程调用问题的Android开发者参考。1. 先看全局一张Binder通讯图上的四个角色1.1 最小角色集Client、Server、ServiceManager、DriverBinder通讯图无论怎么画都逃不出四个角色发起调用的Client进程、提供服务的Server进程、负责登记和查询服务的ServiceManager进程以及所有跨进程数据都要经过的Binder Driver内核模块。这四个角色放在一起看很像一个公司里的电话系统Client是访客Server是工位上干活的员工ServiceManager是前台总机而来回传输声音的电话线路和交换机就是Binder Driver。角色所处位置通讯图中的含义典型代码对应Client任意进程服务调用发起方业务Proxy对象Server提供服务进程被访问的业务对象业务Stub对象ServiceManager系统核心进程服务注册与查询中心ServiceManager的native单例Binder Driver内核态所有事务的传输与中转/dev/binder、binder_ioctl记住一个关键结论通讯图上几乎每一条业务箭头都必须经过Driver。Client和Server之间不存在Java层面的直连通道所有跨进程数据本质上都是“先到内核再进对端”。很多刚接触Binder的人画图时喜欢在Client和Server之间直接画一个大箭头这是通讯图最典型的一个错误。1.2 为什么图上是“请求-响应”而不是“直连”回到业务日常你拿到一个AIDL接口对象直接调用它的方法感觉就像调用本地方法一样。这是因为通讯图里隐藏了Proxy-Stub模式——Client这边拿着的是一个代理对象真正的实现逻辑在另一个进程。这和去餐厅点菜很像。你坐在餐桌旁看菜单接口定义把需求告诉服务员Proxy代理服务员跑后厨把单子递给厨师Stub实现做完再端回来。你不会自己冲进后厨也不会要求厨师直接站到你桌边。Binder通讯图里的箭头天然就是“请求-响应”式往返Client发起一次调用等待Server处理后返回结果。所以画图时静态图上的Client和Server之间是虚的关系动态时序图里才会出现“请求箭头过去、响应箭头回来”的完整往返。1.3 所有通讯开始前的两条基础线open与mmap虽然通讯图强调业务调用但每个进程要进入Binder通讯网络必须先和Driver建立两条基础连线。第一步是打开驱动设备拿到文件描述符。第二步是内存映射映射出一块可以用作接收数据缓冲区的虚拟内存。这两个动作必须在业务调用之前完成否则后面所有箭头都走不通。// 每个进程进入Binder通讯网络之前的初始化动作示意 int fd open(/dev/binder, O_RDWR); void* map mmap(NULL, BINDER_MAP_SIZE, PROT_READ, MAP_PRIVATE, fd, 0);这里有个常被忽略的细节mmap的权限只有PROT_READ没有PROT_WRITE。为什么因为这块映射区的写操作由内核完成接收方只需要从中读取数据。这个设计正是Binder“一次拷贝”的基础后面专门讲。1.4 承接系列一通讯图上的handle到底代表什么系列一里应该已经提过Client通过一个整型句柄handle来指向目标服务。在通讯图上handle就是连接线上标注的“线路编号”。客户端持有的BpBinder内部保存这个handle每次调用都把handle传给驱动驱动根据handle找到对应的服务端实体。从通讯图视角看handle是Client侧对Server侧实体的引用编号它不等于Server在自身进程里的对象指针而是一个驱动维护的相对索引。2. 驱动层通讯图里的“交换机和红绿灯”2.1 binder_proc每个进程在驱动里的一张“档案卡”当一个进程打开/dev/binder时驱动会在内核里创建一个binder_proc结构相当于给这个进程建了一张档案卡。通讯图上进程方块和驱动之间的那条粗实线本质上就是这张档案卡。binder_proc里维护了该进程的线程列表、内存映射信息、待处理事务队列等。面试里常问的“Binder线程池”其实就是围绕每个进程维护的一组binder_thread它们负责通过binder_ioctl读取驱动发来的事务消息。画通讯图时可以把binder_proc理解成一个进程在驱动层的“门牌号”。所有发给该进程的事务都会先落到它的todo队列里然后唤醒其中一个线程去处理。2.2 binder_node与binder_ref实体与引用的两张表服务端进程有一个真正的Binder实体对象在驱动中对应binder_node。当一个Client需要访问这个实体时驱动会为Client建立一个binder_ref也就是引用节点。我们常说的handle就是binder_ref里的desc字段。这两个结构的关系可以用一个场景说明服务端实体是“工位上的员工”他有一个工牌编号binder_node的指针每个来找他的访客会拿到一个排队号binder_ref的handle。不同访客拿到的排队号可能不同但他们找的是同一个人。// 驱动内部的核心关系简化示意 struct binder_proc { ... }; // 进程档案 struct binder_node { // 服务端实体 struct binder_proc* proc; // 实体所在进程 void* ptr; // 指向BBinder int active_refs; // 引用计数 }; struct binder_ref { // 客户端引用 struct binder_node* node; // 指向哪个实体 int desc; // handle编号 };这也是通讯图上“同一个服务可以被多个Client同时访问”的内核基础。多个Client各自持有不同的binder_ref但都指向同一个binder_node。2.3 一次拷贝是怎么做到的Binder通讯图里最值得画清楚的一段是“一次拷贝”的数据路径。传统的管道、Socket传输通常要经过两次拷贝而Binder只用一次。流程分为三步发送方拷贝Client把参数写入Parcel后通过系统调用把数据从用户态Parcel拷贝到内核里的binder_buffer缓冲区。内核映射驱动把这块binder_buffer放到接收方进程通过mmap映射的地址空间对应的物理页上这一步没有额外拷贝只是映射关系调整。接收方直接读Server从自己映射出来的虚拟地址上直接读取数据。也就是说数据从Client到Server的过程中只有一次copy_from_user接收方那头完全靠mmap的映射关系零成本读取。有个比喻很贴切Client把文件内容写在一张白纸上递给窗口内核buffer窗口通过一个传送带把纸直接滑到Server工位mmap映射区Server不需要再手抄一遍直接拿走看。2.4 BC_命令与BR_命令通讯图上时序箭头的“暗语”画动态时序图时Client和Server与Driver之间的交互本质是通过binder_ioctl传入的命令字。命令字分两大类BC_开头的命令是用户态发给驱动BR_开头的命令是驱动发回用户态。命令方向命令字示例含义用户态→驱动BC_TRANSACTION提交一次事务请求用户态→驱动BC_REPLY提交事务响应用户态→驱动BC_FREE_BUFFER通知驱动释放缓冲驱动→用户态BR_TRANSACTION通知对端处理事务驱动→用户态BR_REPLY返回事务结果驱动→用户态BR_DEAD_BINDER通知对端服务实体死亡看懂时序图上的命令字整条调用链路就清晰了。这也是为什么官方文档里Binder交互经常直接画BC/BR命令的原因。3. ServiceManager通讯图里的“电话总机”3.1 ServiceManager为什么handle固定为0通讯图上有个特殊的约定ServiceManager的handle固定为0。这个约定非常关键因为Client第一次想找ServiceManager时并不知道它的handle是多少驱动必须内置一个“固定线路”。驱动规定handle 0指的就是Context Manager也就是ServiceManager本身。这就像你第一次进公司虽然还不知道前台电话分机号码但通讯录第一行永远写着“总机0”。没有这个约定整个服务发现机制就没法滚动起来。3.2 服务注册怎么画进通讯图服务注册发生在Server进程启动阶段不是每次业务调用都发生。时序图里这条链路是Server进程 → 驱动 → ServiceManager。Server调用addService时会把服务名字String和服务对象的IBinder引用一起打包成Parcel通过一次Binder事务发送给ServiceManager。ServiceManager解包后在内部维护一张“服务名→服务实体引用”的表。画图时要注意这条链路本身也是跨进程调用数据同样要经过驱动。ServiceManager不提供“开后门直连”的通道它只是一个处理特殊事务的服务端。3.3 服务查询怎么画进通讯图Client调用getService时把服务名通过驱动传给ServiceManagerServiceManager查表后返回目标服务的IBinder引用。这个引用在被驱动传输时驱动会在当前Client进程里创建一个新的binder_ref并把对应的handle返回给Client。于是通讯图上出现了一条逻辑链路Client通过驱动与Server建立了“间接联系”。这里的“间接”不是指走了ServiceManager这条路而是指Client和Server之间通过同一个binder_node关联但各自持有不同的引用句柄。记住ServiceManager只是帮你“找到入口”找到之后真正的业务通讯还是要走驱动直达Server。3.4 没有ServiceManager的世界Binder传递机制除了查询ServiceManager通讯图上还有一种获取服务引用的常见方式从已有Binder接口返回另一个Binder接口。比如一个服务接口的方法返回IBinder对象这个对象在跨进程传输时会自动完成实体的引用传递驱动会在接收进程里建立一个新的binder_ref。这种机制在Android系统里非常常见比如通过某个Service获取另一个Service的代理。它相当于“一个服务介绍你认识另一个服务”。画通讯图时这种传递关系应该单独画一条带“Binder传递”标注的线不要和普通业务调用混在一起。4. 时序图一次AIDL调用从头到尾的完整链路4.1 发起端三步接口方法、Parcel序列化、transact现在从动态时序图角度完整走一遍AIDL同步调用。假设Client调用服务端的setValue(int value)方法AIDL生成的Proxy代码内部会执行下面这些逻辑// AIDL方法生成的客户端调用代码简化示意 public void setValue(int value) throws RemoteException { Parcel data Parcel.obtain(); Parcel reply Parcel.obtain(); data.writeInterfaceToken(DESCRIPTOR); data.writeInt(value); mRemote.transact(TRANSACTION_setValue, data, reply, 0); reply.readException(); }这个过程有三步把参数写入data这个Parcel调用mRemote.transact等待reply返回。mRemote就是BpBinder它保存着目标服务的handle。4.2 驱动中的处理从BC_TRANSACTION到BR_TRANSACTIONClient线程执行transact后会进入系统调用把BC_TRANSACTION命令连同事务数据发送给驱动。驱动收到后做的事情在通讯图上是一条从“驱动”指向“Server进程”的箭头但这条箭头背后有一串查找逻辑根据handle找到Client的binder_ref。通过binder_ref找到目标binder_node。由binder_node找到目标进程的binder_proc。在目标进程里找一个空闲的binder_thread把事务挂到它的todo队列。唤醒目标线程。这个过程决定了Client线程随后会阻塞等待而Server线程开始从binder_ioctl返回收到BR_TRANSACTION。4.3 Server端处理onTransact如何分派Server线程拿到BR_TRANSACTION后会调用BBinder::transact最终进入业务Stub的onTransact。这里根据事务code判断是哪个接口方法然后从data Parcel中读取参数调用真正实现方法最后把返回值写入reply Parcel。// AIDL生成的服务端onTransact简化示意 public boolean onTransact(int code, Parcel data, Parcel reply, int flags) { switch (code) { case TRANSACTION_setValue: data.enforceInterface(DESCRIPTOR); int value data.readInt(); setValue(value); reply.writeNoException(); return true; } }4.4 返程BC_REPLY与BR_REPLY如何唤醒ClientServer把reply写完后会通过BC_REPLY把结果提交给驱动。驱动收到后找到正在等待的Client线程把结果放到Client的事务等待队列唤醒等待线程。Client线程从BR_REPLY中拿到数据transact调用返回整个同步调用结束。这解释了为什么同步Binder调用中Client线程会“卡住”在驱动处理事务期间Client线程在binder_ioctl里阻塞等待。如果Server端业务方法耗时太久看起来就像Client卡死了。4.5 时序图上的特殊线路动态时序图还要注意三类特殊线路oneway异步调用Client发出BC_TRANSACTION后立即返回不等BR_REPLY。时序图上只有半程箭头。死亡通知驱动发现Server进程或binder实体销毁会主动向Client发送BR_DEAD_BINDER触发已注册的死亡回调。binder线程池耗尽这是通讯图上的一种“堵车”状态——所有Server端binder_thread都在处理事务新事务排队等待外部表现为调用延迟甚至超时。画图时同步调用画完整往返异步调用只画请求方向死亡通知用单独方向的箭头标注这是很多人容易忽略的细节。5. 类图视角核心类之间的“血缘关系”5.1 根接口IInterface所有AIDL业务接口最终都继承自IInterface。这个接口只有一个方法public interface IInterface { IBinder asBinder(); }它把业务接口和Binder机制连接在一起。在类图里IInterface是所有业务接口的根节点也是通讯图里“接口层”与“传输层”的交界点。5.2 服务端类图BnInterface与业务StubAIDL编译时服务端会生成一个抽象Stub类。类图关系可以描述为业务Stub继承自BnInterface业务接口同时实现了业务接口。BnInterface继承自Binder并实现了业务接口的asBinder()。真正的业务实现类再继承业务Stub填充具体逻辑。这个继承链让业务Stub天然成为一个Binder实体可以直接被驱动识别。当驱动向Server投递事务时最终调用的是Binder.onTransact也就是业务Stub里重写的onTransact。5.3 客户端类图BpInterface与BpBinder客户端这边AIDL生成的Proxy类继承自BpInterface业务接口并持有IBinder mRemote这个mRemote的实际类型就是BpBinder。每个业务代理方法内部执行mRemote.transact(...)把code和Parcel交给BpBinder处理。所以在类图里Proxy是“瘦角色”它自己不干重活只是把Java层调用翻译成Binder事务。5.4 类图与通讯图的对应关系表类/接口通讯图中的位置核心职责IInterface业务接口根节点暴露asBinderBinder实体/代理的基类提供transact和onTransactBBinder服务端实体处理BR_TRANSACTIONBpBinder客户端代理发起BC_TRANSACTIONBnInterface服务端接口桥绑定接口与实体BpInterface客户端接口桥绑定接口与代理Stub / ProxyAIDL生成类业务参数序列化与分派面试经常问“为什么多次getService拿到的对象不一样”从类图和通讯图结合看就清楚了每次getService驱动都会在当前进程新建一个binder_ref返回一个新handleJava层包装成新的BinderProxy。不同进程之间持有的是各自的本地代理但内核里指向同一个binder_node。5.5 Java层和Native层怎么衔接严格来说Java层的Binder类和Native层的BBinder之间通过JNI绑定。Java层的BinderProxy对应Native层的BpBinder业务代码在Java层调用transact最终会通过JNI进入Native层的binder_transaction逻辑。画类图时如果只画Java层类漏掉了Native层的BpBinder和BBinder整张图是不完整的。通讯协议真正执行者都在Native层Java层只是“门面”。6. 通讯图怎么画、怎么看、怎么用6.1 画图前先想清楚要表达什么通讯类图不是一个单一图而是三种图的组合用途完全不同。架构角色图用于给新人讲Binder整体画四个角色即可别过度堆细节。时序图用于排查调用链路把BC/BR命令和线程唤醒画清楚。类图用于看AIDL生成代码和写业务逻辑重点在于继承关系。很多人一开始就想画一张“包罗万象”的大图结果节点铺满一页反而谁都没看懂。正确做法是每次只画一个主题其他内容作为引用。6.2 画通讯图时最容易犯的三个错第一个错误把Client和Server画成直连。这是最常见也最根本的错误。通讯协议里的所有数据都经过驱动即使Client和Server在同一个进程内使用Binder调用也是如此。直连画法会让人忽略内核中转和拷贝过程的代价。第二个错误漏掉mmap映射线。不画mmap就无法解释“一次拷贝”为什么能成立。接收方之所以能直接从内核缓冲区读数据就是因为提前mmap了那块内存区域。第三个错误oneway调用画了回程箭头。oneway的语义是只管发不管回时序图上只有请求方向。画了回程箭头就会误导读者以为它有同步等待行为。6.3 用通讯图排查两个真实问题我实际排查过类似这样的问题某业务模块反馈跨进程调用偶尔抛TransactionTooLargeException。把通讯图打开问题很快定位到“数据量超出了驱动缓冲区上限”这个环节。查代码发现调用方把一个几MB的Bitmap直接塞进了Parcel而Binder缓冲区单次事务是有上限的。优化方案改成传文件路径或缩略图问题消失。另一个问题是远端服务偶发调用失败异常是DeadObjectException。从通讯图看这个异常对应的是“服务端binder_node已销毁驱动向客户端发送死亡通知”。翻代码发现服务进程在某种低内存场景下被杀掉客户端没有注册linkToDeath也就没有及时感知后续调用自然失败。这些案例都说明一个道理通讯图不只是画给面试官看的它是排查跨进程问题的第一张地图。6.4 一张可以落地的排查速查表症状通讯图上出问题的位置常见原因DeadObjectException服务端节点销毁后的调用服务进程被杀、没有注册linkToDeath调用卡死无响应同步调用后BR_REPLY未返回onTransact耗时过长、binder线程池耗尽TransactionTooLargeException数据超过驱动缓冲区上限Parcel里写入大数组、大Bitmaponeway调用频繁丢失异步事务排队被丢弃或进程退出大量oneway事务积压服务端来不及处理服务注册失败ServiceManager注册流程异常服务名冲突、进程没有init Binder环境这张表不用背遇到问题再对照通讯图找链路一次两次自然就记住了。我在实际画通讯图时习惯先在白纸上画四个方块和一条代表驱动的横线然后根据当下要解决的问题逐步加线。发现问题就沿着时序图把BC/BR命令一步步走一遍写代码就打开类图看Stub和Proxy的继承关系给新人讲就只画架构角色图。画图的意义从来不是为了好看而是为了把“谁在跟谁说话、数据走哪条路、谁在等待谁”这三件事钉在脑子里。希望这篇《Android Binder 系列二通讯类图》能帮你把脑子里那些模糊的箭头整理成一张张能直接拿来解决问题的地图。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。