ini
ICalculator calculator = ...;
杀掉远端服务后,这个 Java 变量不会自动变成 null,calculator.getClass() 也仍然是同一个 Proxy;可下一笔真实事务却可能抛出 DeadObjectException。于是"我还持有对象"和"对象仍可远程调用"第一次明确分裂。
再加上 isBinderAlive()、linkToDeath() 与 binderDied(),表面上似乎有三种"是否存活"的答案。它们分别能证明什么?远端死亡时,内核又不可能直接调用 Java callback,那条通知怎样跨过 Driver、libbinder 和 JNI?
本文沿四个追问推进:
javascript
为什么本地 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 仍会失败。
所以错误说法是:
csharp
只要 BinderProxy 不为 null,远端服务就一定活着。
linkToDeath() 与 binderDied() 之间隔着哪些层?

读图时分成两条链:
注册链:Java → JNI → BpBinder → Driver
通知链:Driver → BR_DEAD_BINDER → BpBinder → Java callback
不要把图理解成"驱动直接调用 Java"。驱动只向客户端 Binder 线程返回命令;用户态 libbinder 和 JNI 再逐层分发回调。
isBinderAlive() 返回 true,为什么下一行仍可能失败?
跨进程 BinderProxy.isBinderAlive() 会查询 native 代理状态,但它只能提供一个瞬时观察:
scss
if (binder.isBinderAlive()) {
// 这里远端仍可能立刻死亡
binder.transact(...);
}
官方 IBinder.isBinderAlive() 文档明确指出:即使返回 true,进程也可能在方法返回过程中死亡。
所以:
scss
isBinderAlive():一次 best-effort 快照;
linkToDeath():异步接收未来死亡事件;
transact() 异常:当前调用真实失败证据。
三者不能互相替代。
注册 DeathRecipient 时,远端已经死亡怎么办?
典型写法:
java
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。
ini
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 永久留在容器里。
业务 DeadObjectException 与 binderDied 也应进入同一套 expected binder / generation 失效逻辑,防止旧回调清掉已经安装的新引用。对于已经发布的 generation,入口可以从容器按 binder 找到其 recipient;对于上述发布前失败路径,必须显式把 recipient 一起传入,才能完成确定的本地清理。
这里需要保持:
bash
对目标 BinderProxy 的有效引用;
对 DeathRecipient 的合适强引用;
清晰的 unlink / 替换策略。
官方 IBinder 文档说明:当本地对该 Binder proxy 的所有引用都被释放时,死亡订阅会自动解除。反过来,如果业务仍希望接收通知,就不应提前丢弃相关本地对象。
驱动知道远端死了,怎样把消息送到 Java?
Java recipient 怎样先桥接到 Native BpBinder?
Java BinderProxy.linkToDeath() 调用 native:
scss
linkToDeathNative(recipient, flags);
JNI android_os_BinderProxy_linkToDeath() 创建 JavaDeathRecipient,然后:
scss
target->linkToDeath(jdr, nullptr, flags);
这里的 target 对 kernel Binder 远端端点通常是 BpBinder。
多个 recipient 为什么可以共享同一远端死亡订阅?
Android 16 的 BpBinder::linkToDeath() 在本地维护一个 obituary 列表:
一个 BpBinder
└─ 多个 DeathRecipient / cookie / flags
当列表第一次创建时,才向驱动请求:
rust
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 还有:
arduino
struct binder_ref_death *death;
也就是:
csharp
客户端哪个 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() 处理:
ini
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?
ini
mAlive = 0
mObitsSent = 1
取出本地 obituary 列表
然后逐个调用 Native DeathRecipient::binderDied()。
Java 路径中的 JavaDeathRecipient::binderDied() 再通过 JNI 调用业务 IBinder.DeathRecipient。
完整通知链是:
css
远端 proc / node 死亡
↓
驱动为客户端 binder_ref 排死亡 work
↓
客户端 Binder 线程读到 BR_DEAD_BINDER
↓
IPCThreadState::executeCommand()
↓
BpBinder::sendObituary()
↓
JavaDeathRecipient
↓
DeathRecipient.binderDied()
收到 binderDied() 后,为什么还不能宣布恢复成功?
binderDied() 在哪条线程回调,为什么不能现场做重活?
官方 IBinder.DeathRecipient 文档说明:
回调会像其他 Binder 事务一样,
从任意 Binder 线程调用。
因此:
css
不要假设在主线程;
不要假设与注册 linkToDeath 的线程相同;
多线程进程仍要同步共享状态;
不要在回调里执行长时间 I/O 或阻塞恢复。
推荐让 recipient 与被观察的 Binder generation 绑定,而不是只做无条件全局清空:
scss
private IBinder.DeathRecipient newDeathRecipient(IBinder watchedBinder) {
return () -> invalidateGenerationAndScheduleReconnect(watchedBinder);
}
这里必须使用无参数 Lambda 。DeathRecipient 的唯一抽象方法仍是 void binderDied();API 34 新增的 binderDied(IBinder who) 是 default 方法,单参数 Lambda 不能实现这个函数式接口。
如果恢复框架确实需要 who 来区分多个远端 Binder,应改用匿名类,而不是单参数 Lambda:
java
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() 的同步部分只负责:
- 校验 expected binder / generation;
- 原子失效当前代;
- 清理本地 recipient 状态;
- 投递一次去重的恢复任务。
真正的重连、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() 是空实现:
arduino
public void linkToDeath(DeathRecipient recipient, int flags) {
}
原因是:
本地 Binder 实体和调用方在同一个进程;
如果这个进程死亡,调用方代码也一起死亡;
不存在"客户端进程还活着,但同进程 Binder 宿主进程已经死了"的场景。
官方 IBinder API 也明确说明,只会收到 remote Binder 的死亡通知。
所以测试死亡通知前必须确认:
scss
asInterface() 返回的是远端业务 Proxy;
asBinder() 是 BinderProxy;
客户端和服务端 PID 不同。
旧 Proxy 还在时,哪种证据才能证明它已经失效?
BpBinder::transact() 检查远端代理状态。一旦底层返回 DEAD_OBJECT,它会把:
ini
mAlive = 0
后续调用直接返回死亡状态。JNI 再映射到 Java DeadObjectException / RemoteException。
但死亡通知和业务调用存在并发竞态:
线程 T1 收到 binderDied 回调;
线程 T2 可能已经在发起另一笔事务;
或者 T2 先看到 DeadObjectException,通知随后才被分发。
所以客户端状态机必须允许:
调用失败先于 death callback;
death callback 先于业务线程观察失败;
多个并发调用同时失败;
旧代理和新代理短时间并存。
不能依赖单一固定顺序。
为什么 DeathRecipient 不能替业务重新发现服务?
binderDied() 只证明:
旧 Binder node / 远端宿主已失效。
它不会自动:
javascript
启动服务;
重新查询 ServiceManager;
替换业务 Proxy;
重新注册 callback;
恢复订阅和会话;
重放未完成事务。
服务重启后,通常需要:
markdown
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 中直接发布"已重连"会制造竞态?
错误代码:
csharp
public void binderDied() {
calculator = ICalculator.Stub.asInterface(
ServiceManager.getService("calculator"));
calculator.registerCallback(callback);
}
问题包括:
binderDied 在任意 Binder 线程执行,可能阻塞线程池;
服务可能尚未完成重启,getService 仍为空;
多个死亡/失败观察者可能并发重连;
旧请求是否可重试没有定义;
新旧 Binder 身份没有原子替换;
callback 重注册可能重复。
更稳的结构是:
binderDied:
原子标记旧 binder generation 失效;
清理或冻结依赖旧会话的工作;
投递一个去重的重连任务;
重连执行器:
等待服务可用;
获取新 Binder;
校验 generation;
重新 linkToDeath;
恢复幂等状态。
怎样用真机证据区分旧代失效与新代发布?
服务端必须放在独立进程。客户端记录:
scss
T0:linkToDeath 注册完成
T1:最后一笔成功事务
T2:收到 DeadObjectException 的时间
T3:binderDied() 回调时间和线程
T4:重新发现或重新绑定后得到新 Binder 并发布新 generation 的时间
客户端可比较本地 Binder 对象并记录存活、注册和实际调用结果:
less
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 被拒绝。
验证点:
javascript
旧 Java Proxy 在 T2/T3 之后仍可能非 null;
isBinderAlive() 返回 true 不能消除后续死亡竞态;
binderDied 不保证在主线程;
重启后需要重新获得 Binder 并重新注册死亡通知;
本地同进程 Binder 不会触发这条远端死亡链。
BinderLab 用 ClientGeneration 封装 IBinder、业务 Proxy、DeathRecipient 和状态。candidate 完成 LINKED、REGISTERED 后才能原子发布为 ACTIVE;死亡回调捕获 expected generation,并以对象级 CAS 失效当前代。迟到的旧回调会被 C_STALE_DEATH_IGNORED 防御逻辑拒绝,但证据包没有人为构造这条迟到回调。
API 36 关键 marker记录了:
ini
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。
回到开头:九个回答怎样串起死亡通知链?
scss
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 概念,而是把前八篇的对象、线程和生命周期模型用于一条真实等待链:请求走到哪里,下一段由谁推进,哪些证据足以定责。