Binder(八):远端进程死了,BinderProxy 为什么还能收到通知?

0 阅读14分钟
ICalculator calculator = ...;

杀掉远端服务后,这个 Java 变量不会自动变成 nullcalculator.getClass() 也仍然是同一个 Proxy;可下一笔真实事务却可能抛出 DeadObjectException。于是“我还持有对象”和“对象仍可远程调用”第一次明确分裂。

再加上 isBinderAlive()linkToDeath()binderDied(),表面上似乎有三种“是否存活”的答案。它们分别能证明什么?远端死亡时,内核又不可能直接调用 Java callback,那条通知怎样跨过 Driver、libbinder 和 JNI?

本文沿四个追问推进:

为什么本地 Proxy 生命周期与远端 node / proc 生命周期彼此独立?
linkToDeath 注册的监视关系最终保存在哪里?
远端死亡怎样变成 BR_DEAD_BINDER,再扇出到 Java recipients?
收到 binderDied 后,为什么仍要用 generation 管理重新发现与状态恢复?

这里要解释的是“旧远端失效如何被观察和治理”,不是把 DeathRecipient 包装成保活或自动重连机制。

死亡通知的源码位置沿用导读“系列统一基线”中的固定 commit。

Proxy 还在,为什么下一笔 transact 仍会失败?

本地代理、驱动引用与远端 node 为什么有三套生命周期?

生命周期由谁决定“仍存在”能证明什么
Java 业务 Proxy / BinderProxy客户端 Java 引用和 GC客户端仍持有本地代理对象
BpBinder / binder_ref客户端 native 引用与驱动状态客户端仍有一条远端引用记录
远端 binder_node / proc服务端进程生命周期目标端点仍有宿主并可被路由

三者不是一荣俱荣:

客户端可以继续持有 BinderProxy;
驱动已经知道目标 node 死亡;
下一次 transact 仍会失败。

所以错误说法是:

只要 BinderProxy 不为 null,远端服务就一定活着。

linkToDeath()binderDied() 之间隔着哪些层?

image.png

读图时分成两条链:

注册链:Java → JNI → BpBinder → Driver
通知链:Driver → BR_DEAD_BINDER → BpBinder → Java callback

不要把图理解成“驱动直接调用 Java”。驱动只向客户端 Binder 线程返回命令;用户态 libbinder 和 JNI 再逐层分发回调。

isBinderAlive() 返回 true,为什么下一行仍可能失败?

跨进程 BinderProxy.isBinderAlive() 会查询 native 代理状态,但它只能提供一个瞬时观察:

if (binder.isBinderAlive()) {
    // 这里远端仍可能立刻死亡
    binder.transact(...);
}

官方 IBinder.isBinderAlive() 文档明确指出:即使返回 true,进程也可能在方法返回过程中死亡。

所以:

isBinderAlive():一次 best-effort 快照;
linkToDeath():异步接收未来死亡事件;
transact() 异常:当前调用真实失败证据。

三者不能互相替代。

注册 DeathRecipient 时,远端已经死亡怎么办?

典型写法:

private final IBinder.DeathRecipient mDeathRecipient = () -> {
    // 这里只做轻量失效和转交,不做长耗时恢复
    mHandler.post(this::reconnectService);
};
​
void watch(ICalculator calculator) throws RemoteException {
    IBinder binder = calculator.asBinder();
    binder.linkToDeath(mDeathRecipient, 0);
}

linkToDeath() 本身也存在注册竞态:客户端拿到 BinderProxy 后、真正注册前,远端可能已经死亡。官方 IBinder API 规定,这种情况下注册可以直接抛出 RemoteException。因此以下三种信号应归入同一条“旧 generation 已失效”状态机,而不是分别恢复:

linkToDeath 注册失败;
业务 transact 抛出 DeadObjectException;
已经注册的 DeathRecipient 收到 binderDied。
boolean watchOrInvalidate(ICalculator calculator) {
    IBinder binder = calculator.asBinder();
    IBinder.DeathRecipient recipient = newDeathRecipient(binder);
    retainRecipientForGeneration(binder, recipient);
    try {
        binder.linkToDeath(recipient, 0);
        return true;
    } catch (RemoteException alreadyDead) {
        invalidateGenerationAndScheduleReconnect(binder, recipient);
        return false;
    }
}

invalidateGenerationAndScheduleReconnect(binder, recipient) 必须是幂等失效入口。这里传入刚刚保留的 candidate 和 recipient,不是多余参数:linkToDeath 失败时,candidate 尚未成为可发布的 ACTIVE generation,入口必须从本地 generation 容器中移除它和 recipient 强引用,并对这个 (binder, recipient) 执行一次无条件、忽略返回值与异常的 best-effort unlinkToDeath。即使底层注册结果与异常发生竞态,这个清理也应安全且可重复;不能只安排重连,却把失败 candidate 永久留在容器里。

业务 DeadObjectExceptionbinderDied 也应进入同一套 expected binder / generation 失效逻辑,防止旧回调清掉已经安装的新引用。对于已经发布的 generation,入口可以从容器按 binder 找到其 recipient;对于上述发布前失败路径,必须显式把 recipient 一起传入,才能完成确定的本地清理。

这里需要保持:

对目标 BinderProxy 的有效引用;
对 DeathRecipient 的合适强引用;
清晰的 unlink / 替换策略。

官方 IBinder 文档说明:当本地对该 Binder proxy 的所有引用都被释放时,死亡订阅会自动解除。反过来,如果业务仍希望接收通知,就不应提前丢弃相关本地对象。

驱动知道远端死了,怎样把消息送到 Java?

Java recipient 怎样先桥接到 Native BpBinder?

Java BinderProxy.linkToDeath() 调用 native:

linkToDeathNative(recipient, flags);

JNI android_os_BinderProxy_linkToDeath() 创建 JavaDeathRecipient,然后:

target->linkToDeath(jdr, nullptr, flags);

这里的 target 对 kernel Binder 远端端点通常是 BpBinder

多个 recipient 为什么可以共享同一远端死亡订阅?

Android 16 的 BpBinder::linkToDeath() 在本地维护一个 obituary 列表:

一个 BpBinder
    └─ 多个 DeathRecipient / cookie / flags

当列表第一次创建时,才向驱动请求:

IPCThreadState* self = IPCThreadState::self();
self->requestDeathNotification(binderHandle(), this);
self->flushCommands();

概念上:

BC_REQUEST_DEATH_NOTIFICATION
    +
handle
    +
cookie(用于客户端找回 BpBinder)

驱动在当前客户端 binder_ref 上安装 binder_ref_death。多个 Java/Native recipient 可以复用同一条驱动死亡监视,再由 BpBinder 在本地扇出通知。

驱动用什么对象把死亡订阅绑定到客户端引用?

第三篇已经看到:

binder_ref
    → binder_node

binder_ref 还有:

struct binder_ref_death *death;

也就是:

客户端哪个 ref 订阅了死亡;
通知返回客户端时携带哪个 cookie。

当远端进程退出、node 释放时,驱动遍历指向该 node 的引用,给订阅了死亡的客户端 binder_proc 排入死亡 work。

这不是 Java 对象之间直接互相观察,而是:

node 生命周期变化
    ↓
驱动找到所有相关 binder_ref
    ↓
为订阅者进程排队通知

远端 node 消失后,death work 怎样变成 BR_DEAD_BINDER

客户端进程必须有 Binder 线程或其他 Binder 事件循环继续从驱动读取命令。

驱动在 binder_thread_read() 把死亡 work 返回为:

BR_DEAD_BINDER
    +
注册时的 cookie

Android 16 的 IPCThreadState::executeCommand() 处理:

case BR_DEAD_BINDER: {
    BpBinder* proxy =
            (BpBinder*) mIn.readPointer();
    proxy->sendObituary();
​
    mOut.writeInt32(BC_DEAD_BINDER_DONE);
    mOut.writePointer((uintptr_t) proxy);
    break;
}

所以 BR_DEAD_BINDER 首先到达客户端进程的一条 Binder 线程,而不是某个业务主线程。

一条 native obituary 怎样扇出到多个 Java recipients?

BpBinder::sendObituary() 先:

mAlive = 0
mObitsSent = 1
取出本地 obituary 列表

然后逐个调用 Native DeathRecipient::binderDied()

Java 路径中的 JavaDeathRecipient::binderDied() 再通过 JNI 调用业务 IBinder.DeathRecipient

完整通知链是:

远端 proc / node 死亡
    ↓
驱动为客户端 binder_ref 排死亡 work
    ↓
客户端 Binder 线程读到 BR_DEAD_BINDER
    ↓
IPCThreadState::executeCommand()
    ↓
BpBinder::sendObituary()
    ↓
JavaDeathRecipient
    ↓
DeathRecipient.binderDied()

收到 binderDied() 后,为什么还不能宣布恢复成功?

binderDied() 在哪条线程回调,为什么不能现场做重活?

官方 IBinder.DeathRecipient 文档说明:

回调会像其他 Binder 事务一样,
从任意 Binder 线程调用。

因此:

不要假设在主线程;
不要假设与注册 linkToDeath 的线程相同;
多线程进程仍要同步共享状态;
不要在回调里执行长时间 I/O 或阻塞恢复。

推荐让 recipient 与被观察的 Binder generation 绑定,而不是只做无条件全局清空:

private IBinder.DeathRecipient newDeathRecipient(IBinder watchedBinder) {
    return () -> invalidateGenerationAndScheduleReconnect(watchedBinder);
}

这里必须使用无参数 LambdaDeathRecipient 的唯一抽象方法仍是 void binderDied();API 34 新增的 binderDied(IBinder who)default 方法,单参数 Lambda 不能实现这个函数式接口。

如果恢复框架确实需要 who 来区分多个远端 Binder,应改用匿名类,而不是单参数 Lambda:

private final class WatchedDeathRecipient
        implements IBinder.DeathRecipient {
    private final IBinder watchedBinder;
​
    WatchedDeathRecipient(IBinder watchedBinder) {
        this.watchedBinder = watchedBinder;
    }
​
    @Override
    public void binderDied() {
        handleBinderDeath(watchedBinder);
    }
​
    @Override
    public void binderDied(IBinder who) {
        handleBinderDeath(who);
    }
}

上面的双重 override 以 API 34+ 的 DeathRecipient 接口为前提

这里的 generation 失效入口必须线程安全、幂等且足够轻量。invalidateGenerationAndScheduleReconnect() 的同步部分只负责:

  1. 校验 expected binder / generation;
  2. 原子失效当前代;
  3. 清理本地 recipient 状态;
  4. 投递一次去重的恢复任务。

真正的重连、listener 重注册和业务状态恢复应交给明确的 Handler 或执行器,不应直接阻塞 binderDied() 所在的 Binder 回调线程。

连接建立阶段也要防半初始化 generation 被业务线程看到。一个 candidate 至少应经过:

CONNECTING
  → linkToDeath 成功:LINKED
  → callback / listener 注册成功:REGISTERED
  → 原子发布:ACTIVE
  → 死亡、替换或连接失败:INVALID

getActiveService() 只能返回 ACTIVE;不能先把 candidate 放进全局引用,再补做 linkToDeath() 和注册。若 linkToDeath()、注册或 ACTIVE 发布失败,失效逻辑必须同时清理 candidate 状态与已经成功挂上的 recipient,不能只把业务 Proxy 设为 null。DeathRecipient 与 ACTIVE 发布还必须共享同步边界,避免 candidate 在“刚被死亡回调置为 INVALID”后又被发布。

还要注意 linkToDeath() 已成功、Java 状态尚未从 CONNECTING 写成 LINKED 的窄窗口。远端可能在两步之间死亡,回调先把 candidate 置为 INVALID;此时 deathLinked=false 之类字段并不是驱动注册状态的原子真相。BinderLab 因此不再用布尔值决定“要不要清理”,而是对每个失败/退休 candidate 都执行 best-effort unlinkToDeath(),接受未注册、已死亡或已解除的返回/异常。

为什么本地 Binder 不会触发同一条死亡通知链?

Android 16 的本地 Binder.linkToDeath() 是空实现:

public void linkToDeath(DeathRecipient recipient, int flags) {
}

原因是:

本地 Binder 实体和调用方在同一个进程;
如果这个进程死亡,调用方代码也一起死亡;
不存在“客户端进程还活着,但同进程 Binder 宿主进程已经死了”的场景。

官方 IBinder API 也明确说明,只会收到 remote Binder 的死亡通知。

所以测试死亡通知前必须确认:

asInterface() 返回的是远端业务 Proxy;
asBinder() 是 BinderProxy;
客户端和服务端 PID 不同。

旧 Proxy 还在时,哪种证据才能证明它已经失效?

BpBinder::transact() 检查远端代理状态。一旦底层返回 DEAD_OBJECT,它会把:

mAlive = 0

后续调用直接返回死亡状态。JNI 再映射到 Java DeadObjectException / RemoteException

但死亡通知和业务调用存在并发竞态:

线程 T1 收到 binderDied 回调;
线程 T2 可能已经在发起另一笔事务;
或者 T2 先看到 DeadObjectException,通知随后才被分发。

所以客户端状态机必须允许:

调用失败先于 death callback;
death callback 先于业务线程观察失败;
多个并发调用同时失败;
旧代理和新代理短时间并存。

不能依赖单一固定顺序。

为什么 DeathRecipient 不能替业务重新发现服务?

binderDied() 只证明:

旧 Binder node / 远端宿主已失效。

它不会自动:

启动服务;
重新查询 ServiceManager;
替换业务 Proxy;
重新注册 callback;
恢复订阅和会话;
重放未完成事务。

服务重启后,通常需要:

1. 标记旧引用失效;
2. 根据服务策略等待或触发重新发现;
3. ServiceManager.getService / waitForService 获取新 IBinder;
4. 对新 Binder 调用 Stub.asInterface;
5. 为新 Binder 重新 linkToDeath;
6. 重新注册 listener、callback、session 和状态;
7. 只重试业务上可安全重试的请求。

旧 BinderProxy 不会因为服务名相同就自动“指向新进程”。

组件绑定还要单独讨论:BinderLab 使用 bindService(..., BIND_AUTO_CREATE),组件框架具备创建 Service、并在服务再次运行时调用 onServiceConnected() 的路径;这不等于 DeathRecipient 自动重连,也不保证某次 kill 后在固定时间内必然重建。应用仍要定义去重、退避、何时重新 bind、callback/session 恢复和幂等重放。把“框架可能再次连接组件”写成“应用已经实现自动恢复”同样是错误的。

callback 引用为什么也必须纳入同一代际治理?

服务端保存客户端 callback:

客户端活着:callback BinderProxy 可调用;
客户端进程死亡:callback node 死亡;
服务端若不清理:业务列表会残留无效代理和 cookie。

Java 服务可使用 RemoteCallbackList 管理跨进程 callback。它会为注册接口关联死亡通知,并在客户端进程退出时清理列表。

但仍要定义业务语义:

死亡时是否释放会话资源;
是否取消任务;
是否保留可恢复状态;
新客户端重连后如何重新订阅。

Binder 只提供故障信号,不替业务决定恢复策略。

为什么在 binderDied 中直接发布“已重连”会制造竞态?

错误代码:

public void binderDied() {
    calculator = ICalculator.Stub.asInterface(
            ServiceManager.getService("calculator"));
    calculator.registerCallback(callback);
}

问题包括:

binderDied 在任意 Binder 线程执行,可能阻塞线程池;
服务可能尚未完成重启,getService 仍为空;
多个死亡/失败观察者可能并发重连;
旧请求是否可重试没有定义;
新旧 Binder 身份没有原子替换;
callback 重注册可能重复。

更稳的结构是:

binderDied:
    原子标记旧 binder generation 失效;
    清理或冻结依赖旧会话的工作;
    投递一个去重的重连任务;
​
重连执行器:
    等待服务可用;
    获取新 Binder;
    校验 generation;
    重新 linkToDeath;
    恢复幂等状态。

怎样用真机证据区分旧代失效与新代发布?

服务端必须放在独立进程。客户端记录:

T0:linkToDeath 注册完成
T1:最后一笔成功事务
T2:收到 DeadObjectException 的时间
T3:binderDied() 回调时间和线程
T4:重新发现或重新绑定后得到新 Binder 并发布新 generation 的时间

客户端可比较本地 Binder 对象并记录存活、注册和实际调用结果:

Log.i(TAG, "sameLocalBinder=" + (oldBinder == newBinder));
Log.i(TAG, "newAliveSnapshot=" + newBinder.isBinderAlive());
Log.i(TAG, "deathThread=" + Thread.currentThread().getName());

System.identityHashCode()oldBinder == newBinder 只能观察客户端当前 Java 对象身份,不能证明远端是不是同一个 binder_node,也不能证明新引用可用。更强的验证组合是:

新 Binder 的 linkToDeath 注册成功;
一笔只读探测事务真实成功;
业务层显式 generation 已切换;
旧 generation 的迟到 callback / reply 被拒绝。

验证点:

Java ProxyT2/T3 之后仍可能非 nullisBinderAlive() 返回 true 不能消除后续死亡竞态;
binderDied 不保证在主线程;
重启后需要重新获得 Binder 并重新注册死亡通知;
本地同进程 Binder 不会触发这条远端死亡链。

BinderLab 用 ClientGeneration 封装 IBinder、业务 Proxy、DeathRecipient 和状态。candidate 完成 LINKED、REGISTERED 后才能原子发布为 ACTIVE;死亡回调捕获 expected generation,并以对象级 CAS 失效当前代。迟到的旧回调会被 C_STALE_DEATH_IGNORED 防御逻辑拒绝,但证据包没有人为构造这条迟到回调。

API 36 关键 marker记录了:

C_LAST_SUCCESS_END requestId=1001 result=42
C_GENERATION_INVALID connectionEpoch=1 generationId=1 oldState=ACTIVE wasCurrent=true reason=binderDied
C_OLD_PROXY_DEAD_OBJECT connectionEpoch=1 generationId=1 requestId=1002 exception=DeadObjectException
C_GENERATION_STATE connectionEpoch=2 generationId=2 newState=ACTIVE
C_EXPERIMENT_NOT_RESTARTED generationId=2

这次受控实验的顺序是 T1 最后成功 → T3 binderDied → 受控 T2 旧代理失败 → T4 显式 rebind。它证明旧代在死亡回调后失效、旧 Proxy 的真实事务失败,并且测试重绑发布了隔离的新代;它不证明 T3 永远早于自然发生的 T2,也不等于生产级自动重连、业务 session 恢复或多绑定生命周期管理。完整日志见 binder-death.log

回到开头:九个回答怎样串起死亡通知链?

1. BinderProxy.linkToDeath() 通过 JNI 把 Java recipient 包成 native DeathRecipient;
2. BpBinder 在本地保存 recipient 列表,并为 handle 请求驱动死亡通知;
3. 驱动把 binder_ref_death 关联到客户端 binder_ref;
4. 远端 proc / node 释放时,驱动为所有订阅引用排死亡 work;
5. 客户端 Binder 线程读到 BR_DEAD_BINDER;
6. IPCThreadState 调用 BpBinder.sendObituary();
7. BpBinder 向本地 recipients 扇出;
8. JavaDeathRecipient 最终调用 binderDied();
9. 回调只表示旧远端已死亡,重连和状态恢复由业务完成。

死亡通知把“远端已经消失”带回客户端,却不解释故障发生前后每一段时间花在哪里。最后一篇不再增加 Binder 概念,而是把前八篇的对象、线程和生命周期模型用于一条真实等待链:请求走到哪里,下一段由谁推进,哪些证据足以定责。

源码与官方文档