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 的句柄值
};

嗯,这里要注意——binderhandle 是共用体。什么意思呢?

  • 如果 typeBINDER_TYPE_BINDER,表示这是一个“本地 Binder 对象”,驱动会用 binder 字段来定位内核中的 Binder 节点。
  • 如果 typeBINDER_TYPE_HANDLE,表示这是一个“远程 Binder 引用”,驱动用 handle 字段来查找目标进程里的 Binder 实体。

你想想看,这个设计其实很巧妙——同一个结构体,既能描述“我是谁”,也能描述“我指向谁”。

传递过程:从用户态到内核态

咱们走一遍流程。假设 ServiceManager 要把一个 Binder 对象传给客户端。

  1. 用户态打包:客户端调用 writev() 写入 flat_binder_object,type 设为 BINDER_TYPE_BINDER,binder 字段填上本地 Binder 对象的地址。
  2. 驱动拦截:Binder 驱动在 binder_transaction() 里检测到 flat_binder_object,开始“翻译”。
  3. 创建节点或句柄:如果发送方是本地 Binder,驱动会在内核中创建一个 binder_node,然后给接收方分配一个 binder_ref(句柄)。
  4. 写入接收方缓存:驱动把 flat_binder_object 的 type 改成 BINDER_TYPE_HANDLE,handle 填上刚分配的句柄值,然后复制到接收方的缓冲区。
  5. 用户态解析:接收方从 readv() 中读到这个结构体,根据 type 知道这是一个远程引用,后续通过 handle 来调用。

核心要点:Binder 驱动是“翻译官”。它把进程 A 的本地对象,翻译成进程 B 能用的句柄。整个过程对用户态是透明的。

SVG 流程图:flat_binder_object 传递过程

进程 A(发送方) flat_binder_object type = BINDER_TYPE_BINDER 进程 B(接收方) flat_binder_object type = BINDER_TYPE_HANDLE Binder 驱动(内核态) binder_transaction() 创建 binder_node + binder_ref writev() readv() binder_node(内核节点) binder_ref(句柄映射) 驱动将 type 从 BINDER_TYPE_BINDER 改为 BINDER_TYPE_HANDLE handle 字段填入新分配的句柄值

两种传递场景

在实际项目中,我遇到过两种典型场景:

场景 发送方 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_nodebinder_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),而且没有保持强引用。驱动在传递时发现引用已经失效,直接传了个空句柄过去。

所以,我的建议是:

  1. 传递 Binder 引用前,确保它已经被 ServiceManager 注册,或者被某个存活的对象强引用。
  2. 如果传递匿名 Binder,一定要在发送方保持强引用,直到接收方处理完毕。
  3. 接收方拿到句柄后,尽快调用 asInterface() 转为本地代理,避免句柄失效。

总结

flat_binder_object 是 Binder 对象传递的“通用语言”。驱动用它来统一描述本地对象和远程句柄。你想想看,这个设计让用户态完全不用关心跨进程的细节——你只管写 writeStrongBinder(),剩下的交给驱动。

我个人觉得,理解 flat_binder_object 的翻译过程,是掌握 Binder IPC 的关键一步。搞懂了它,你就能明白为什么 Binder 传递是“引用传递”而不是“值传递”,也能理解为什么 Binder 对象不能跨进程直接访问内存。

好,今天就聊到这儿。下次遇到 Binder 传递的问题,记得先看看 flat_binder_object 的 type 和 handle 对不对。


公众号:蓝海资料掘金营,微信deep3321