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

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?

BpBinder::sendObituary() 先:

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() 的同步部分只负责:

  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() 是空实现:

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 概念,而是把前八篇的对象、线程和生命周期模型用于一条真实等待链:请求走到哪里,下一段由谁推进,哪些证据足以定责。

源码与官方文档

相关推荐
Dovis(誓平步青云)2 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码3 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程4 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程7 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程8 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程9 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓9 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程9 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer
supabc1239 小时前
Celium:连接 Windows、Mac、Linux 与 Android,让远程访问和设备管理更简单
android·linux·windows·macos·远程访问·网络管理·celium