Binder 、 AIDL 、 IPC 关系

456 阅读6分钟

一、先搞清三者的关系

是什么通俗理解
IPC跨进程通信(两进程间传数据、调方法)。
BinderAndroid IPC 的底层机制(/dev/binder + 驱动)。
AIDL写接口自动生成 Stub/Proxy 的工具,底层走 Binder。

一句话:IPC 是目的,Binder 是底层,AIDL 是基于 Binder 的代码生成器。系统服务通过 ServiceManager 注册(addService)与查询(getService),App 按服务名拿到 Binder 再通信。

关系图

  AIDL、Messenger、ContentProvider、Broadcast 等(上层用法)
                    │
                    ▼ 底层都走
              Binder(/dev/binder + 内核驱动)

二、Binder 机制一句话(必记)

每个进程打开 /dev/binder 并 mmap,得到一块「内核可写、本进程可读」的映射区(即该进程可读、驱动可往该区写);客户端发请求时,驱动在服务端该区分配 buffer、一次拷贝数据,服务端从该区读出并处理。

人话:进程向内核要一块收件区(mmap);发数据时,驱动一次拷进对方收件区,对方直接读。
比喻:快递——每人一个收件柜,快递员(驱动)把包裹直接投进对方柜子(一次拷贝),单上有寄件人(UID/PID)可鉴权。


三、为啥是「一次拷贝」?(含对比)

对比结论

方式拷贝次数原因
Socket / 管道等2 次数据先拷进内核缓冲,再从内核拷到对方用户空间。
Binder1 次对方已 mmap 好一块「收件区」,驱动直接拷进该区,对方直接读,无需再「内核→用户」一次。

流程图(对比)

  Socket 等(2 次):
  发送进程 ──copy1──► 内核缓冲 ──copy2──► 接收进程

  Binder(1 次):
  发送进程 ──copy1──► 接收进程的映射区(收件区)◄── 接收进程直接读
                      (驱动在「对方已映射」的区里分配 buffer 并写入)

四、一次 Binder 调用怎么走?(含流程图)

客户端通过 getService / bindService 拿到服务端的 Binder 引用(handle),发请求时驱动根据 handle 找到对端进程。

流程图

  客户端(ProxyBinder 驱动                   服务端(Stub)
  ┌─────────────┐             ┌─────────────────┐             ┌─────────────┐
  │ 1. 打包请求   │ ──────────► │ 2. 根据 handle   │             │             │
  │   (Parcel)   │             │    找服务端进程  │ ── 通知 ──► │ 3. 读请求    │
  │             │             │    在服务端映射区 │             │    执行方法  │
  │             │             │    划一块+拷贝   │             │ 4. 写回结果  │
  │ 6. 解包返回   │ ◄────────── │ 5. 把 reply      │ ◄────────── │             │
  └─────────────┘             │    交给客户端    │             └─────────────┘
                               └─────────────────┘

步骤:① 客户端把方法+参数打包进 Parcel(Binder 的序列化容器)经 Binder 发出 → ② 驱动根据 handle 找到服务端进程,在其映射区划一块、拷入数据、通知服务端 → ③ 服务端 Binder 线程读出请求、执行方法 → ④ 结果写回 reply → ⑤ 驱动把 reply 交给客户端 → ⑥ 客户端解包返回。

写代码只关心:客户端调接口、服务端实现接口;底层即上述流程。


五、需要知道的几个点

  • 线程:onTransact 跑在 Binder 线程里,别干重活,否则易 ANR;快速解析后丢到业务线程。
  • 大小:每进程 Binder 缓冲约 1MB(多笔共享),传大会 TransactionTooLargeException,大块用共享内存 + fd。
  • 掉线linkToDeath 可监听对端进程死亡,在回调里做清理、重连。
  • 鉴权:驱动带调用方 UID/PID,服务端用 getCallingUid()/getCallingPid() 做校验。

六、AIDL 帮你做了啥?(含调用链)

.aidl 接口(方法声明),编译器自动生成 Stub(服务端按方法编号分发)和 Proxy(客户端打包发请求、解包结果),省掉手写 transact/onTransact。AIDL 不替代 Binder,只是生成基于 Binder 的调用代码。

AIDL 调用链(与第四节对应)

  你写的接口调用
        │
        ▼
  Proxy.xxx() ── 打包 Parcel、transact() ──► Binder 驱动 ──► Stub.onTransact() 按 code 分发
        ▲                                                                        │
        │                                                                        ▼
        └──────────── 解包 reply、返回 ◄──────────────────────────── 你的实现类.xxx()
  • 复杂对象用 Parcelable,别用 Serializable。
  • oneway = 异步、不等返回;适合通知类,别滥用,否则对端队列会堵。
类型行为适用
同步(默认)客户端阻塞等 reply需要返回值
oneway发完即返回,不等 reply仅通知、不关心结果

七、系统里哪儿在用 Binder?

App 调系统服务的流程:App → getService("服务名") → ServiceManager 返回 Binder(handle)→ App 用该 Binder transact() → 驱动转发到对应系统服务。
调自己/别的 App 的服务:用 bindService() 拿到 Binder,再 transact(),底层同样是上面这套。

系统服务启动时向 ServiceManager 注册(如 AMS、WMS),和跨进程相关的底层多半都是 Binder,例如:

  • 系统服务:AMS、WMS、PMS、各种 Manager 和 App 通信;
  • 四大组件:Activity/Service 和系统通信,ContentProvider 的增删改查,广播下发;
  • 自己写bindService + AIDL、Messenger 等。

八、几种 IPC 方式怎么选?(含对比)

场景更合适的做法底层/备注
要调很多方法、接口复杂AIDLBinder
就发几条简单消息MessengerBinder
跨进程共享数据(按 URI 查)ContentProviderBinder
通知很多组件「发生了某件事」BroadcastBinder
数据量特别大(大图、大文件)共享内存 + 传 fd不直接走 Binder 传,避免 TransactionTooLargeException
跨设备、走网络Socket / WebSocket非 Binder

对比要点:前四种底层均为 Binder,适合本机跨进程;大数据用共享内存+fd;跨网络用 Socket。


九、实际写代码时别踩的坑

(对应第五节,写代码/面试前重点看。)

  1. onTransact 里别耗时 → 解析完丢线程池。
  2. 别传超大对象 → 大块用共享内存 + fd。
  3. 服务端校验调用方 → getCallingUid()/getCallingPid() + 权限。
  4. 对端可能死 → linkToDeath 里重连/降级。

十、面试被问到时的短答(与第二节一致的可背一句)

Binder 是什么?各进程对 /dev/binder 做 mmap 得映射区;驱动在服务端该区分配 buffer、一次拷贝,服务端从该区读。(即第二节「一句话」。)
为什么一次拷贝?数据直接拷进对方已 mmap 的内存,对方直接读,少一次「内核→用户」拷贝。
AIDL 生成了啥?接口 + Stub(服务端分发)+ Proxy(客户端打包/解包),底层 Binder。
TransactionTooLargeException?每进程缓冲约 1MB(多笔共享),传太大就崩;改共享内存 + 传 fd。
ServiceManager?服务名↔Binder 的通讯录:addService 登记,getService 按名查。
服务端怎么鉴权?getCallingUid()/getCallingPid() 拿调用方,再配合 checkCallingPermission、签名、SELinux。

十一、一张图收尾(总流程 = 第二节 + 第四节精简)

  【拿 Binder】App ── getService("服务名") ──► ServiceManager ── 返回 handle ──► App 拿到 Binder

  【发请求】  客户端(Proxy) ── transact 打包 ──► Binder 驱动 ── 在服务端映射区拷一份 ──► 服务端(Stub) 读出执行
                              ◄── reply 解包返回 ◄──              ◄── 结果写回 ◄──

(上:拿 Binder——系统服务用 getService,App 服务用 bindService;下:再用该 Binder 发请求,即第四节流程。)

背这一句(与第二节同一句):各进程 mmap /dev/binder 拿收件区 → 驱动在服务端收件区分配 buffer、一次拷贝 → 服务端从收件区读并处理。