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_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 是客户端的“门牌”。本体不灭,门牌可换。
实战中的注意事项
我在做系统稳定性优化时,遇到过几个典型问题:
- 跨进程传递 Binder 对象:当你把 Binder 对象作为参数传给另一个进程时,驱动会在目标进程里创建一个新的
binder_ref,指向同一个binder_node。这就是为什么一个服务可以被多个客户端共享。 - 死亡通知的注册时机:我建议在获取服务后立即注册
linkToDeath。如果等到第一次通信失败再注册,可能已经晚了——服务端可能已经挂了,你连通知都收不到。 - 句柄复用问题:当一个
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_ref 和 binder_node 信息。我经常用这个来排查引用泄漏问题。
好了,关于 binder_node 和 binder_ref 的关系与生命周期,就聊到这儿。记住一句话:节点是根,引用是叶。根在叶在,根亡叶散。
公众号:蓝海资料掘金营,微信deep3321