22. 死亡通知:Binder 的死亡通知机制(BC_REQUEST_DEATH_NOTIFICATION)

Binder 通信里,有个很现实的问题——服务端进程挂了怎么办?

你想想看,客户端正拿着一个 Binder 代理对象,高高兴兴地调用接口。突然,服务端进程因为某种原因崩溃了,或者被系统杀掉了。这时候客户端如果继续调用,就会收到一个 DeadObjectException。嗯,这还算好的,至少有个异常。但更麻烦的是,客户端可能根本不知道服务端已经死了,还在傻等结果。

所以,Binder 设计了一套死亡通知机制。说白了,就是让客户端能注册一个「监听器」,当服务端进程死亡时,系统会主动通知客户端。我个人觉得,这个机制在系统稳定性保障中,比很多人想象的要重要得多。

死亡通知的核心流程

整个死亡通知机制,其实就三个角色:

  • 客户端:想监听服务端是否还活着
  • Binder 驱动:负责监控服务端进程状态,并转发死亡通知
  • 服务端:被监听的对象,它死了驱动会知道

流程大致是这样的:

  1. 客户端通过 BinderProxy.linkToDeath() 注册一个 DeathRecipient 回调
  2. Binder 驱动收到 BC_REQUEST_DEATH_NOTIFICATION 命令,记录这个监听关系
  3. 当服务端进程死亡(比如 crash 或被 kill),驱动检测到 Binder 节点上的所有引用都释放了
  4. 驱动向客户端发送 BR_DEAD_BINDER 通知
  5. 客户端的 DeathRecipient.binderDied() 被回调

关键点:死亡通知是单向的。驱动只会通知客户端「服务端死了」,不会告诉客户端「服务端又活了」。服务端重启后,客户端需要重新获取 Binder 代理并重新注册死亡通知。

BC_REQUEST_DEATH_NOTIFICATION 命令详解

这个命令是客户端发给 Binder 驱动的。它的数据结构长这样:

struct binder_request_death_notification {
    binder_uintptr_t cookie;    // 客户端传入的上下文,通常是 DeathRecipient 对象的指针
    binder_uintptr_t target;    // 要监听的目标 Binder 节点
};

嗯,这里要注意 cookie 参数。它其实是一个 void* 指针,驱动不会对它做任何解释,只是原样返回。我习惯用它来传递 DeathRecipient 对象的地址,这样在回调时就能直接找到对应的对象。

驱动收到这个命令后,会在目标 Binder 节点上记录一个「死亡通知条目」。这个条目包含了:

  • 发起监听的客户端进程 PID
  • 客户端传入的 cookie 值
  • 一个标志位,表示是否已经发送过死亡通知

死亡通知的触发时机

驱动什么时候会触发死亡通知?不是服务端进程一死就立刻通知的。我遇到过一个问题,当时排查了很久才发现原因——驱动是在服务端进程的 Binder 节点所有引用都被释放后,才会发送死亡通知。

具体来说,触发条件包括:

  • 服务端进程的 Binder 线程池全部退出
  • 服务端进程的所有 Binder 节点引用计数归零
  • 服务端进程的 /proc/pid 目录被清理

说白了,驱动要确认服务端进程「死透了」,才会通知客户端。如果服务端进程只是暂时挂起或者被 ptrace 了,驱动不会触发死亡通知。

BR_DEAD_BINDER 与 BR_CLEAR_DEATH_NOTIFICATION

当驱动决定通知客户端时,会发送 BR_DEAD_BINDER 命令。这个命令的数据结构很简单:

struct binder_dead_binder {
    binder_uintptr_t cookie;  // 就是注册时传入的 cookie
};

客户端收到这个命令后,Binder 驱动层会调用 DeathRecipient.binderDied() 方法。注意,这个回调是在 Binder 线程池中执行的,不是在你自己的线程里。所以回调里不要做耗时操作,也不要在回调里直接尝试重新连接服务端——我吃过这个亏,会导致死锁。

另外还有一个 BR_CLEAR_DEATH_NOTIFICATION 命令,它是客户端主动取消死亡通知时驱动返回的确认。当你调用 unlinkToDeath() 时,驱动会回复这个命令,表示监听关系已经解除。

代码示例:如何正确使用死亡通知

来看看实际代码怎么写。我建议你封装一个工具类,避免每次都要写重复的模板代码:

public class BinderDeathMonitor {
    private final IBinder binder;
    private final DeathRecipient deathRecipient;
    private boolean isLinked = false;

    public BinderDeathMonitor(IBinder binder, DeathRecipient recipient) {
        this.binder = binder;
        this.deathRecipient = recipient;
    }

    public void link() {
        if (isLinked) return;
        try {
            binder.linkToDeath(deathRecipient, 0);
            isLinked = true;
        } catch (RemoteException e) {
            // 服务端已经死了,直接触发回调
            deathRecipient.binderDied();
        }
    }

    public void unlink() {
        if (!isLinked) return;
        binder.unlinkToDeath(deathRecipient, 0);
        isLinked = false;
    }
}

小技巧linkToDeath() 如果抛出 RemoteException,说明服务端已经死了。这时候不要忽略异常,应该直接触发 binderDied() 回调。我曾经见过有人忽略这个异常,结果客户端一直以为服务端还活着。

死亡通知的 SVG 流程图

Binder 死亡通知机制流程图 客户端进程 Binder 驱动 服务端进程 ① linkToDeath() ② BC_REQUEST_DEATH_NOTIFICATION 记录死亡通知条目 ③ 进程死亡 检测到节点引用归零 ④ BR_DEAD_BINDER ⑤ binderDied() 回调 收到死亡通知 时间

避坑指南

我在实际项目中遇到过几个死亡通知相关的坑,分享给你:

  • 不要重复注册:同一个 DeathRecipient 对象不要对同一个 Binder 注册多次。驱动不会去重,会导致回调被调用多次。我曾经在代码里不小心在 onResume 和 onCreate 里都调了 linkToDeath,结果服务端一死,回调被触发了两次。
  • 取消注册要及时:当客户端不再需要监听时,一定要调用 unlinkToDeath。否则驱动会一直持有客户端的引用,导致内存泄漏。特别是在 Activity 或 Fragment 的 onDestroy 里,一定要记得清理。
  • 回调线程问题:binderDied() 是在 Binder 线程池中调用的。如果你在回调里更新 UI,记得 post 到主线程。另外,不要在回调里做同步的 Binder 调用,容易死锁。
  • 服务端重启后的处理:死亡通知只发一次。服务端重启后,客户端需要重新获取 Binder 代理,重新注册死亡通知。我建议在 binderDied() 回调里启动一个重连机制,比如使用 Handler 延迟重试。

特别注意:在 Android 8.0 之后,系统对死亡通知的发送时机做了优化。如果服务端进程是被系统杀的(比如 low memory killer),驱动可能会延迟发送死亡通知,直到服务端进程的所有 Binder 资源都被清理完毕。所以不要假设收到死亡通知时服务端进程一定已经死了——它可能正在被清理中。

总结

死亡通知机制是 Binder 通信中不可或缺的一环。它让客户端能够感知服务端的生死状态,从而做出相应的处理。我个人觉得,用好死亡通知的关键在于三点:

  • 理解驱动层的触发时机和条件
  • 正确管理 DeathRecipient 的生命周期
  • 处理好回调线程和重连逻辑

嗯,如果你在项目中遇到了 Binder 相关的诡异问题,不妨先检查一下死亡通知有没有正确注册和取消。很多时候,问题就出在这些看似不起眼的细节上。