14、Binder 的死亡通知机制:linkToDeath 与 unlinkToDeath 的实现原理

Binder 通信中,客户端和服务端是跨进程的。服务端进程随时可能崩溃、被杀死、或者正常退出。如果客户端不知道服务端已经挂了,还傻傻地继续发请求,那就会收到一个 DeadObjectException。嗯,这体验很糟糕。

所以 Android 提供了一套死亡通知机制——linkToDeathunlinkToDeath。说白了,就是让客户端能注册一个回调,当服务端 Binder 实体死亡时,内核会主动通知客户端。我个人觉得,这是 Binder 机制里最体现「内核态主动推送」能力的设计之一。

14.1 为什么需要死亡通知?

先想一个问题:客户端持有一个 Binder 代理对象,这个代理指向服务端的一个 Binder 实体。如果服务端进程挂了,内核中的 Binder 节点会变成「死亡节点」。但客户端并不知道——它手里的代理对象还在,只是发任何请求都会失败。

我在项目中遇到过这种情况:一个后台音乐播放服务被系统杀掉了,前台 UI 还在傻等回调,结果界面卡死,用户直接给差评。后来加了死亡通知,服务一挂,UI 立刻收到通知,优雅地显示「服务异常」。

所以死亡通知的核心价值就两点:

  • 及时感知:服务端挂了,客户端能立刻知道
  • 资源清理:客户端可以释放相关资源,避免内存泄漏

14.2 死亡通知的注册流程:linkToDeath

客户端调用 linkToDeath 时,实际上是在 Binder 代理对象上注册一个 DeathRecipient 回调。这个回调会被传递到内核,内核会在 Binder 节点上记录这个通知请求。

流程大致如下:

  1. 客户端调用 binderProxy.linkToDeath(recipient, flags)
  2. Binder 驱动收到 BC_REQUEST_DEATH_NOTIFICATION 命令
  3. 驱动在目标 Binder 节点上创建一个 binder_ref_death 结构体
  4. 这个结构体里保存了客户端的 PID、回调信息等
  5. 驱动返回成功,注册完成

来看一下内核中的关键数据结构:

struct binder_ref_death {
    struct binder_work work;          // 工作队列项
    struct binder_ref *ref;           // 指向对应的 Binder 引用
    void __user *cookie;              // 用户空间回调 cookie
    uint32_t flags;                   // 标志位
};

嗯,这里要注意:cookie 是用户空间传下来的,内核不会去解析它,只是原样保存。当死亡通知触发时,内核会把 cookie 原封不动地传回给用户空间。说白了,这就是一个「透传」的上下文指针。

14.3 死亡通知的触发时机

什么时候会触发死亡通知?

  • 服务端进程异常退出(崩溃、被 kill)
  • 服务端进程正常退出(exit)
  • 服务端 Binder 实体被显式销毁

内核检测到 Binder 实体死亡后,会做以下事情:

  1. 遍历所有引用该实体的 binder_ref
  2. 对每个注册了死亡通知的 binder_ref,创建一个 BINDER_WORK_DEAD_BINDER 工作项
  3. 把这个工作项插入到客户端的 todo 队列中
  4. 唤醒客户端进程,让它处理这个通知

我记得有一次调试一个诡异的问题:服务端挂了,但客户端收不到死亡通知。查了半天发现是客户端进程的 todo 队列被其他高优先级任务堵住了。嗯,这其实是个调度问题,不是 Binder 本身的问题。

14.4 用户空间的处理流程

当客户端进程被唤醒后,Binder 驱动会通过 BR_DEAD_BINDER 命令把死亡通知传给用户空间。用户空间的 IPCThreadState 会处理这个命令:

// frameworks/native/libs/binder/IPCThreadState.cpp
status_t IPCThreadState::executeCommand(int32_t cmd) {
    switch (cmd) {
        case BR_DEAD_BINDER: {
            BpBinder *proxy = (BpBinder*)mIn.readPointer();
            proxy->sendObituary();
            return NO_ERROR;
        }
        // ...
    }
}

sendObituary() 会遍历所有注册的 DeathRecipient,调用它们的 binderDied() 方法。你想想看,这个设计其实挺巧妙的——内核只负责通知「谁死了」,具体怎么处理,完全交给用户空间。

核心要点:死亡通知是异步的。内核把工作项插入队列就返回了,不会等待客户端处理完。客户端在下次处理 Binder 事务时才会收到通知。

14.5 unlinkToDeath:取消注册

有注册就有注销。当客户端不再需要死亡通知时,可以调用 unlinkToDeath。这个操作会告诉内核:把之前注册的死亡通知移除掉。

流程如下:

  1. 客户端调用 binderProxy.unlinkToDeath(recipient, flags)
  2. Binder 驱动收到 BC_DEAD_BINDER_DONE 命令
  3. 驱动找到对应的 binder_ref_death 结构体
  4. 从链表中移除,释放内存

这里有个坑,我曾经踩过:重复 unlink 会导致崩溃。因为内核不会检查你是否已经注销过,如果传了一个无效的 recipient,内核会尝试访问一个已经释放的内存。所以一定要保证 linkToDeathunlinkToDeath 成对出现。

避坑指南:我曾经在一个大型项目中,因为忘记在 Activity 销毁时调用 unlinkToDeath,导致服务端进程重启后,旧的回调还在,结果收到了两次死亡通知。嗯,从那以后我养成了一个习惯:在 onDestroy 里一定做清理。

14.6 死亡通知的完整生命周期

为了让你更直观地理解整个过程,我画了一张流程图:

Binder 死亡通知完整生命周期 客户端进程 Binder 驱动(内核) 服务端进程 ① linkToDeath 注册死亡通知 ② 进程死亡 检测到 Binder 实体死亡 ③ 创建 DEAD_BINDER 工作项 插入客户端 todo 队列 ④ 唤醒客户端进程 ⑤ 收到 BR_DEAD_BINDER 调用 binderDied() 时间

14.7 源码层面的实现细节

在 Java 层,linkToDeath 最终会调用到 BinderProxy.linkToDeath()

// frameworks/base/core/java/android/os/BinderProxy.java
public void linkToDeath(DeathRecipient recipient, int flags) {
    try {
        // 调用 native 方法
        nativeLinkToDeath(recipient, flags);
    } catch (RemoteException e) {
        // 如果服务端已经死了,这里会抛异常
        throw e;
    }
}

对应的 native 实现:

// frameworks/base/core/jni/android_util_Binder.cpp
static void android_os_BinderProxy_linkToDeath(JNIEnv* env, jobject obj,
        jobject recipient, jint flags) {
    // 获取 BpBinder 对象
    sp<BpBinder> b = getBPBinder(env, obj);
    // 创建 JavaDeathRecipient 包装
    sp<JavaDeathRecipient> jdr = new JavaDeathRecipient(env, recipient);
    // 调用 BpBinder::linkToDeath
    status_t status = b->linkToDeath(jdr, NULL, flags);
}

最终走到 BpBinder::linkToDeath

// frameworks/native/libs/binder/BpBinder.cpp
status_t BpBinder::linkToDeath(const sp<DeathRecipient>& recipient,
        void* cookie, uint32_t flags) {
    // 构造 Obituary 对象
    Obituary ob;
    ob.recipient = recipient;
    ob.cookie = cookie;
    ob.flags = flags;
    
    // 加锁,添加到 mObituaries 列表
    AutoMutex _l(mLock);
    mObituaries.add(ob);
    
    // 通过 IPC 发送 BC_REQUEST_DEATH_NOTIFICATION
    return IPCThreadState::self()->requestDeathNotification(
            mHandle, recipient, cookie, flags);
}

个人建议:如果你在写系统服务,一定要处理好死亡通知。我习惯在服务端 onBind 时记录客户端信息,在 binderDied 回调里做资源清理。这样即使客户端挂了,服务端也不会泄漏。

14.8 常见问题与避坑

问题 原因 解决方案
收不到死亡通知 客户端 todo 队列被阻塞 检查客户端是否有长时间运行的 Binder 事务
重复收到死亡通知 多次 linkToDeath 未 unlink 确保 link/unlink 成对出现
unlinkToDeath 崩溃 传入了无效的 recipient 使用标志位判断是否已注册
内存泄漏 DeathRecipient 对象未释放 在 onDestroy 中 unlink 并置空引用

嗯,最后说一句:死亡通知机制虽然看起来简单,但它是 Binder 通信可靠性的重要保障。没有它,跨进程通信就像在黑暗中走路——你不知道前面是悬崖还是平地。我个人觉得,理解了这个机制,你对 Binder 的整体把握就上了一个台阶。