13. Binder 节点:binder_node 与 binder_ref 的关系与生命周期

好,咱们今天来聊聊 Binder 节点。说白了,就是服务端在 Binder 驱动里注册的那个“身份标识”,以及客户端怎么找到它的“门牌号”。这两个东西,一个叫 binder_node,一个叫 binder_ref

我记得刚接触 Binder 那会儿,总觉得这俩概念差不多。后来在项目里踩了坑才明白——它们完全是两码事,但谁也离不开谁。

什么是 binder_node?

binder_node 是服务端的“本体”。当你的 Service 通过 ServiceManager.addService() 注册时,Binder 驱动会为它创建一个 binder_node 结构体。

这个结构体里存了什么?

  • 服务端进程的 pid/uid——谁是这个节点的主人
  • 内部引用计数——有多少人在用这个节点
  • 指向服务端 Binder 实体的指针——就是那个 Java 层的 Binder 对象
  • 一个红黑树节点——用于在驱动里快速查找

你可以把 binder_node 想象成服务端在驱动里的一间“办公室”。这间办公室一旦建好,只要服务端进程不挂,它就一直在那儿。

什么是 binder_ref?

binder_ref 是客户端的“门牌号”。客户端通过 ServiceManager.getService() 拿到一个句柄(handle),这个句柄背后对应的就是 binder_ref

每个 binder_ref 都指向一个 binder_node。但反过来,一个 binder_node 可以被多个 binder_ref 指向。

举个例子:

  • 服务端 A 注册了一个服务
  • 客户端 B 和客户端 C 都去获取这个服务
  • 驱动里只有一个 binder_node,但有两个 binder_ref

嗯,这里要注意:每个客户端进程里的句柄值是不同的。你在进程 B 里拿到的 handle 是 1,在进程 C 里可能也是 1,但它们指向的是不同的 binder_ref 结构体。

它们的关系图

我画了一张图,帮你理清这层关系:

服务端进程 Binder 实体对象 (Java/Kernel 层) 注册 binder_node 引用计数: 2 pid: 1234 指向 指向 binder_ref (进程B) handle = 1 指向 node 0x1234 binder_ref (进程C) handle = 1 指向 node 0x1234 客户端进程 B 通过 handle=1 通信 客户端进程 C 通过 handle=1 通信

生命周期:谁先死?

这个问题很关键。我见过不少开发者搞混了它们的销毁时机。

binder_node 的生命周期

binder_node 的生命周期跟服务端进程绑定。只要服务端进程活着,这个节点就存在。即使所有客户端都断开连接了,节点也不会被销毁。

什么时候销毁?

  • 服务端进程挂了(crash 或被 kill)
  • 服务端主动调用 destroy() 或类似接口
  • Binder 驱动内部清理(比如进程退出时)

你想想看,如果服务端进程死了,驱动会遍历它所有的 binder_node,然后给每个节点发一个“死亡通知”。这就是 linkToDeath 机制的基础。

binder_ref 的生命周期

binder_ref 的生命周期跟客户端进程绑定。客户端进程退出时,它持有的所有 binder_ref 都会被清理。

但有个特殊情况:

  • 客户端主动 close() 了 Binder 句柄
  • 或者客户端进程挂了
  • 驱动会减少对应 binder_node 的引用计数

我曾经在项目里遇到过一个 bug:客户端频繁创建和销毁 Binder 连接,导致 binder_ref 结构体泄漏。后来发现是客户端没有正确关闭句柄,驱动里的引用计数一直没减下去。

引用计数:核心中的核心

驱动里维护了两套引用计数:

计数类型 含义 增减时机
refs 指向该节点的 binder_ref 数量 客户端获取/释放服务时
internal_strong_refs 内部强引用计数 Binder 事务传输时

嗯,这里有个坑:refs 归零不代表节点会被销毁。驱动还会检查 internal_strong_refs。只有两个计数都归零了,节点才会被真正释放。

核心原则:binder_node 是服务端的“本体”,binder_ref 是客户端的“门牌”。本体不灭,门牌可换。

实战中的注意事项

我在做系统稳定性优化时,遇到过几个典型问题:

  1. 跨进程传递 Binder 对象:当你把 Binder 对象作为参数传给另一个进程时,驱动会在目标进程里创建一个新的 binder_ref,指向同一个 binder_node。这就是为什么一个服务可以被多个客户端共享。
  2. 死亡通知的注册时机:我建议在获取服务后立即注册 linkToDeath。如果等到第一次通信失败再注册,可能已经晚了——服务端可能已经挂了,你连通知都收不到。
  3. 句柄复用问题:当一个 binder_ref 被释放后,它的句柄值可能被后续新建的 binder_ref 复用。所以不要缓存句柄值,每次通信前重新获取。

警告:千万不要在 Binder 回调里做耗时操作。我曾经见过一个案例,服务端的 Binder 线程池被客户端的同步调用堵死,导致整个系统卡顿。原因就是客户端在回调里做了网络请求。

源码层面的印证

如果你去看 kernel 的 drivers/android/binder.c,会发现:

// binder_node 结构体(简化版)
struct binder_node {
    struct binder_proc *proc;      // 所属进程
    struct rb_node rb_node;        // 红黑树节点
    struct hlist_node dead_node;   // 死亡节点链表
    int debug_id;
    struct binder_work work;
    union {
        struct {
            struct list_head refs; // 指向该节点的 ref 链表
        } internal;
    };
    int internal_strong_refs;      // 内部强引用
    int local_strong_refs;         // 本地强引用
    int local_weak_refs;           // 本地弱引用
};

// binder_ref 结构体(简化版)
struct binder_ref {
    struct binder_node *node;      // 指向的节点
    struct binder_proc *proc;      // 所属进程
    struct rb_node rb_node_desc;   // 按描述符排序
    struct rb_node rb_node_node;   // 按节点排序
    uint32_t desc;                 // 句柄值(handle)
};

你看,binder_ref 里有个 node 指针,直接指向 binder_node。这就是它们的关系在代码层面的体现。

小技巧:调试 Binder 问题时,可以查看 /sys/kernel/debug/binder/proc 下的文件。里面会列出每个进程的 binder_refbinder_node 信息。我经常用这个来排查引用泄漏问题。

好了,关于 binder_nodebinder_ref 的关系与生命周期,就聊到这儿。记住一句话:节点是根,引用是叶。根在叶在,根亡叶散。


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