21. Binder 对象传递:如何在进程间传递 Binder 引用(flat_binder_object)?
好,咱们今天聊一个很实在的问题——Binder 引用怎么在进程间传递。说白了,就是 A 进程怎么把一个 Binder 对象“扔”给 B 进程用。
我记得刚接触 Binder 那会儿,我天真地以为“传递对象”就是把内存里的数据直接复制过去。后来被坑了一次才明白——Binder 对象不能直接复制,得靠 flat_binder_object 这个结构体来“打包”传递。
什么是 flat_binder_object?
先看定义。这个结构体在 binder.h 里,是 Binder 驱动用来描述一个 Binder 引用的“标准格式”。
struct flat_binder_object {
__u32 type; // BINDER_TYPE_BINDER 或 BINDER_TYPE_HANDLE
__u32 flags; // 一些标志位
__u64 binder; // 本地 Binder 对象的指针(内核空间)
__u64 cookie; // 附加数据,通常是 Java 层的对象指针
__u32 handle; // 远程 Binder 的句柄值
};
嗯,这里要注意——binder 和 handle 是共用体。什么意思呢?
- 如果
type是BINDER_TYPE_BINDER,表示这是一个“本地 Binder 对象”,驱动会用binder字段来定位内核中的 Binder 节点。 - 如果
type是BINDER_TYPE_HANDLE,表示这是一个“远程 Binder 引用”,驱动用handle字段来查找目标进程里的 Binder 实体。
你想想看,这个设计其实很巧妙——同一个结构体,既能描述“我是谁”,也能描述“我指向谁”。
传递过程:从用户态到内核态
咱们走一遍流程。假设 ServiceManager 要把一个 Binder 对象传给客户端。
- 用户态打包:客户端调用
writev()写入flat_binder_object,type 设为BINDER_TYPE_BINDER,binder 字段填上本地 Binder 对象的地址。 - 驱动拦截:Binder 驱动在
binder_transaction()里检测到flat_binder_object,开始“翻译”。 - 创建节点或句柄:如果发送方是本地 Binder,驱动会在内核中创建一个
binder_node,然后给接收方分配一个binder_ref(句柄)。 - 写入接收方缓存:驱动把
flat_binder_object的 type 改成BINDER_TYPE_HANDLE,handle 填上刚分配的句柄值,然后复制到接收方的缓冲区。 - 用户态解析:接收方从
readv()中读到这个结构体,根据 type 知道这是一个远程引用,后续通过 handle 来调用。
核心要点:Binder 驱动是“翻译官”。它把进程 A 的本地对象,翻译成进程 B 能用的句柄。整个过程对用户态是透明的。
SVG 流程图:flat_binder_object 传递过程
两种传递场景
在实际项目中,我遇到过两种典型场景:
| 场景 | 发送方 type | 接收方 type | 驱动行为 |
|---|---|---|---|
| 传递本地 Binder | BINDER_TYPE_BINDER | BINDER_TYPE_HANDLE | 创建节点,分配句柄 |
| 传递远程句柄 | BINDER_TYPE_HANDLE | BINDER_TYPE_HANDLE | 增加引用计数,复制句柄 |
第一种场景最常见——Service 把自己注册到 ServiceManager。第二种场景呢?比如你把一个已经拿到的 Binder 句柄再传给另一个进程,驱动不会重新创建节点,只是把引用计数加一。
个人经验:我曾经在调试一个跨进程回调时,发现回调总是收不到。查了半天,原来是我在传递 Binder 引用时,type 写错了,驱动没正确创建句柄。后来我养成了一个习惯——在 flat_binder_object 的 type 字段上打日志,确认驱动翻译的结果。
避坑指南:引用计数与生命周期
嗯,这里有个坑,我踩过不止一次。
当你传递一个 Binder 引用时,驱动会在内核中维护引用计数。每次传递,计数加一;每次 Binder 死亡或显式关闭,计数减一。如果计数归零,内核会销毁 binder_node 和 binder_ref。
我曾经在项目中遇到一个问题:一个 Service 被多个客户端引用,其中一个客户端异常退出了,但 Binder 驱动没有收到死亡通知,导致引用计数没减。结果 Service 以为还有客户端在用,一直不释放资源。后来我加上了 linkToDeath() 机制,才彻底解决。
警告:不要手动修改 flat_binder_object 的 handle 字段!驱动在翻译时会校验 handle 的合法性。我曾经见过有人试图“伪造”一个高权限的 Binder 句柄,结果驱动直接返回 -EINVAL,进程被杀死。
用户态视角:我们怎么用?
说实话,在 Java 层或者 Native 层,我们很少直接操作 flat_binder_object。框架帮我们封装好了。
- Java 层:通过
IBinder接口传递,底层自动序列化为flat_binder_object。 - Native 层:通过
Parcel::writeStrongBinder()和Parcel::readStrongBinder()。 - 内核层:驱动直接解析
flat_binder_object结构体。
但理解底层原理很重要。我记得有一次,一个同事问我:“为什么我把 Binder 对象放到 Bundle 里传过去,对方拿到的却是 null?”我一看代码,他传的是一个匿名 Binder(没有注册到 ServiceManager),而且没有保持强引用。驱动在传递时发现引用已经失效,直接传了个空句柄过去。
所以,我的建议是:
- 传递 Binder 引用前,确保它已经被
ServiceManager注册,或者被某个存活的对象强引用。 - 如果传递匿名 Binder,一定要在发送方保持强引用,直到接收方处理完毕。
- 接收方拿到句柄后,尽快调用
asInterface()转为本地代理,避免句柄失效。
总结
flat_binder_object 是 Binder 对象传递的“通用语言”。驱动用它来统一描述本地对象和远程句柄。你想想看,这个设计让用户态完全不用关心跨进程的细节——你只管写 writeStrongBinder(),剩下的交给驱动。
我个人觉得,理解 flat_binder_object 的翻译过程,是掌握 Binder IPC 的关键一步。搞懂了它,你就能明白为什么 Binder 传递是“引用传递”而不是“值传递”,也能理解为什么 Binder 对象不能跨进程直接访问内存。
好,今天就聊到这儿。下次遇到 Binder 传递的问题,记得先看看 flat_binder_object 的 type 和 handle 对不对。
公众号:蓝海资料掘金营,微信deep3321