一、先搞清三者的关系
| 是什么 | 通俗理解 |
|---|---|
| IPC | 跨进程通信(两进程间传数据、调方法)。 |
| Binder | Android 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 次 | 数据先拷进内核缓冲,再从内核拷到对方用户空间。 |
| Binder | 1 次 | 对方已 mmap 好一块「收件区」,驱动直接拷进该区,对方直接读,无需再「内核→用户」一次。 |
流程图(对比):
Socket 等(2 次):
发送进程 ──copy1──► 内核缓冲 ──copy2──► 接收进程
Binder(1 次):
发送进程 ──copy1──► 接收进程的映射区(收件区)◄── 接收进程直接读
(驱动在「对方已映射」的区里分配 buffer 并写入)
四、一次 Binder 调用怎么走?(含流程图)
客户端通过 getService / bindService 拿到服务端的 Binder 引用(handle),发请求时驱动根据 handle 找到对端进程。
流程图:
客户端(Proxy) Binder 驱动 服务端(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 方式怎么选?(含对比)
| 场景 | 更合适的做法 | 底层/备注 |
|---|---|---|
| 要调很多方法、接口复杂 | AIDL | Binder |
| 就发几条简单消息 | Messenger | Binder |
| 跨进程共享数据(按 URI 查) | ContentProvider | Binder |
| 通知很多组件「发生了某件事」 | Broadcast | Binder |
| 数据量特别大(大图、大文件) | 共享内存 + 传 fd | 不直接走 Binder 传,避免 TransactionTooLargeException |
| 跨设备、走网络 | Socket / WebSocket | 非 Binder |
对比要点:前四种底层均为 Binder,适合本机跨进程;大数据用共享内存+fd;跨网络用 Socket。
九、实际写代码时别踩的坑
(对应第五节,写代码/面试前重点看。)
- onTransact 里别耗时 → 解析完丢线程池。
- 别传超大对象 → 大块用共享内存 + fd。
- 服务端校验调用方 → getCallingUid()/getCallingPid() + 权限。
- 对端可能死 → 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、一次拷贝 → 服务端从收件区读并处理。