3. Binder基础

Binder 是 Android 中最核心的进程通信机制。它并不是一个单纯的 Java 类,而是一套横跨用户态与内核态的通信基础设施:应用层通过 AIDL 或系统服务接口调用,框架层通过 Binder 代理与 Stub 完成编解码,内核层由 Binder 驱动负责对象传递、线程调度和内存映射。理解 Binder,必须同时看清这四层:架构分层、驱动职责、ServiceManager 命名服务、代理模式与一次拷贝。

一、Binder 架构分层

从调用方向看,Binder 通信可以分成客户端、服务端、ServiceManager 与 Binder 驱动四个角色。客户端持有服务端在自身进程中的“代理对象”,调用代理方法时,参数被写入 Parcel,经 Binder 驱动转发到服务端进程;服务端由 Binder 线程池中的线程执行真实方法,再把结果按相反路径返回。

  • 客户端进程:通过 IBinder 代理发起 transact 调用,不直接访问服务端对象。
  • 服务端进程:继承 BnInterface 或 Stub,在 onTransact 中解析 Parcel 并执行真实逻辑。
  • ServiceManager:全局服务注册表,负责“按名字找 Binder”。
  • Binder 驱动:位于内核,管理 Binder 节点、引用、线程和内存映射,是通信的最终执行者。

这四者不是简单的上下级关系,而是围绕“Binder 引用”形成的协作网络。客户端拿到的不是服务端对象本身,而是一个句柄;句柄只在当前进程有效,跨进程后由驱动翻译成目标进程中的 Binder 节点。

Binder 架构:代理调用、驱动转发与一次拷贝 客户端进程 Proxy(IBinder 代理) Parcel 写入参数 Binder 驱动(内核) Binder 节点 / 引用表 mmap 映射与线程调度 服务端进程 Stub(BnInterface) onTransact 执行真实逻辑 transact 转发 返回结果 reply 一次拷贝:数据从发送方用户空间到内核映射区,接收方直接读取 发送方用户空间 Parcel 数据 内核 Binder 映射区 copy_from_user 接收方用户空间 直接映射读取 第 1 次拷贝 0 次拷贝

二、Binder 驱动做了什么

Binder 驱动是整条链路的“交通枢纽”。它维护 Binder 节点、引用、死亡通知和线程池,并负责把一次 transact 请求准确投递到目标进程的 Binder 线程。驱动并不理解业务语义,它只认句柄和 Parcel 中的类型信息。

当客户端调用 transact 时,驱动会完成以下关键动作:

  1. 根据句柄找到目标进程中的 Binder 节点。
  2. 将发送方 Parcel 数据拷贝到内核映射区。
  3. 唤醒目标进程的 Binder 线程,把数据映射到其用户空间。
  4. 等待目标线程执行完毕,再把返回数据按同样路径送回。

关键点:Binder 驱动不负责“业务逻辑”,它只负责“把谁的数据、送到哪个节点、由哪个线程处理”。因此 Binder 的性能瓶颈通常不在驱动本身,而在 Parcel 大小、线程池并发和跨进程调用次数。

三、ServiceManager:按名字找 Binder

ServiceManager 是 Binder 机制中的“电话总机”。系统服务启动时,会把自己的 Binder 对象注册到 ServiceManager,并绑定一个字符串名字,例如 activity、package、window。客户端需要某个服务时,先向 ServiceManager 查询名字,拿到 Binder 代理后再发起真实调用。

// 服务端注册(简化示意)
ServiceManager.addService("my.service", new MyService());

// 客户端查询
IBinder binder = ServiceManager.getService("my.service");
IMyService service = IMyService.Stub.asInterface(binder);
service.doSomething();

这里有两个容易混淆的点:第一,getService 返回的是 IBinder,不是具体接口;第二,asInterface 会根据当前进程判断是本地对象还是代理对象。如果服务端和客户端在同一进程,返回本地对象;否则返回 Proxy,后续调用才真正跨进程。

四、代理模式:客户端为什么像调用本地方法

Binder 的易用性来自代理模式。客户端看到的 IMyService 接口,实际由 Proxy 类实现。Proxy 内部持有 IBinder,每个方法都做三件事:把参数写入 Parcel、调用 transact、从 reply Parcel 中读取返回值。

服务端则继承 Stub,在 onTransact 中根据 code 判断调用哪个方法,解析参数、执行逻辑、写入 reply。这样,客户端和服务端共享同一套接口定义,但各自运行在不同进程,由 Binder 驱动完成“翻译”。

角色 典型类型 核心职责 是否跨进程
客户端代理 Proxy 写 Parcel、调用 transact、读返回值 是
服务端桩 Stub / BnInterface 解析 code、执行真实方法、写 reply 是
命名服务 ServiceManager 注册与查询 Binder 名字 是
内核驱动 Binder Driver 节点管理、内存映射、线程调度 内核态

五、一次拷贝原理与常见误区

传统 Linux IPC 如管道、Socket,通常需要“用户空间 → 内核空间 → 对端用户空间”两次拷贝。Binder 通过 mmap 把内核中的一块缓冲区同时映射到发送方和接收方的用户空间,发送方只需把数据从自己的用户空间拷贝到这块共享映射区,接收方即可直接读取,因此称为“一次拷贝”。

但要注意,“一次拷贝”不等于“零成本”。Parcel 序列化、线程切换、驱动锁竞争、Binder 线程池排队仍然存在。如果一次调用传输大量数据,或频繁进行小包通信,性能依然会下降。工程上常见的优化是:合并调用、减少跨进程次数、控制 Parcel 大小、避免在主线程做重 Binder 调用。

常见误区:把 Binder 当成“零拷贝”。准确说法是“一次拷贝”,即数据从发送方用户空间到内核映射区的一次拷贝;接收方读取的是同一块映射内存,不再发生第二次用户态拷贝。

复盘一次 Binder 调用,可以按“客户端 Proxy → Binder 驱动 → 服务端 Stub → 返回”四段定位问题:如果调用卡住,先看 Binder 线程池是否耗尽;如果数据异常,先看 Parcel 读写顺序是否一致;如果服务找不到,先看 ServiceManager 中是否已注册。掌握这条链路,后续学习 AIDL、Messenger、ContentProvider 和系统服务调用时,都会回到同一套底层模型。