工程现场经常只给出两条信息:主线程栈停在 BinderProxy.transactNative(),这次调用耗时两秒。于是结论很快被写成:
Binder 卡了。
这句话几乎没有诊断价值。同一份客户端栈,既可能对应目标事务仍在排队,也可能对应服务端已经进入业务、正等待锁,甚至 Binder 入口早已把任务交给 Handler,而真正的 1.9 秒发生在 MessageQueue。
"客户端正在等 Binder"只是现象;根因必须回答另一个问题:这笔请求已经越过哪个里程碑,下一步本该由谁推进?
沿着这个问题,排查顺序会自然形成:
先把端到端等待切成互不重叠的阶段;
再用后续 marker 排除请求尚未越过的边界;
然后到当前推进者的线程栈、调度和队列中找停滞;
最后用源码、trace、日志和对照实验排除替代解释。
本文不会从工具清单出发,因为 dumpsys、线程栈或 Perfetto 单独都不能自动给出因果链;它们只在回答某个阶段问题时才成为证据。
分析口径沿用导读"系列统一基线"。trace 类别、权限和内核配置会随设备变化,因此诊断应依赖阶段关系,而不是固定某一份 trace 布局。
客户端停在 transact,为什么还不能说 Binder 慢?
一条客户端等待栈究竟证明了什么?
客户端主线程可能停在:
scss
BinderProxy.transactNative()
这只证明:
当前线程正在一笔 Binder 调用中,或等待其返回。
它不能单独证明:
Binder Driver 耗时;
服务端没有收到事务;
服务端 Binder 线程池耗尽;
服务端业务慢;
Handler 队列慢;
嵌套调用死锁;
reply 已返回但客户端没被调度。
客户端等待点是起点,不是根因。
同样停在 transact,为什么可能有六种推进者?
| 阶段 | 可能耗时 | 首要证据 |
|---|---|---|
| 1. 客户端调用之前 | 锁、参数准备、主线程已有任务 | 客户端用户态栈、方法入口时间 |
| 2. 客户端准备与驱动处理 | Proxy / Parcel 编码、JNI / libbinder、ioctl、目标解析、buffer / 对象转换与 work 入队 | 客户端 trace、Binder flow、binder_driver 事件、错误状态 |
| 3. 目标进程等待执行 | Binder 线程池繁忙、事务排队、目标被冻结/调度延迟 | Binder trace、目标线程池状态、sched |
| 4. 服务端 Binder 入口 | 权限检查、锁、I/O、入口本地代码 | 服务端 Binder 线程栈、入口/退出埋点 |
| 5. 可选:服务内部继续转交或嵌套等待 | Handler、Future、子 Binder、其他执行主体 | Handler 消息、目标线程栈、嵌套 Binder flow |
| 6. reply 返回 | 服务异常、嵌套 reply、客户端调度延迟 | reply trace、客户端恢复时间、sched |
阶段 5 不是所有 Binder 方法的必经步骤。服务若直接在 Binder 线程完成,会从阶段 4 进入阶段 6;只有入口主动把工作交给 Handler / Future / 其他线程,或等待下一笔 Binder 时,才展开阶段 5。这样也避免把阶段 4 的"本地入口代码"和阶段 5 的"下一执行主体"重复计算。
六阶段是诊断概念模型,不等于六个阶段都能由后面的 Java 日志独立测量。实际能拆到多细,取决于 marker、Binder flow、binder_driver 与 sched 证据覆盖了哪些边界。
诊断不是先猜某一段,而是从已知证据不断缩小范围。
每越过一个里程碑,究竟排除了什么?
读图时从左向右问:
请求是否离开客户端?
驱动是否接收?
目标 Binder 线程何时开始?
服务端入口何时结束?
是否转交 Handler?
reply 何时让客户端恢复?
后续 marker 能证明前序阶段已经完成,不能证明前序阶段没有变慢。每找到一个同 requestId 的后续里程碑,可以排除"请求仍未越过此前阶段"的假设;此前阶段是否曾经耗时,仍要比较相邻时间差,或用 Binder flow、binder_driver 与 sched 证据继续拆分。
怎样用不重叠里程碑避免重复计算耗时?
不转 Handler 的最小边界是:
C0:客户端开始
S0:服务端入口开始
S1:服务端入口结束
C1:客户端返回
如果拥有接口定义,最好增加业务 requestId,把跨进程证据关联起来。下面是假设接口额外提供 addDirectWithRequestId() 时的概念示意;该方法不属于当前 BinderLab AIDL,也不作为仓库实测接口。它只用来展示不转 Handler 时的四点最小边界:
csharp
int requestId = nextRequestId();
long c0 = SystemClock.elapsedRealtimeNanos();
Log.i(TAG, "C0 requestId=" + requestId + " atNs=" + c0);
traceBegin("calc.add client id=" + requestId);
try {
return calculator.addDirectWithRequestId(requestId, a, b);
} finally {
long c1 = SystemClock.elapsedRealtimeNanos();
Log.i(TAG, "C1 requestId=" + requestId + " atNs=" + c1
+ " costNs=" + (c1 - c0));
traceEnd();
}
系统问题中往往不能修改既有 AIDL。此时不要为了排障随意改系统接口,可以优先使用:
css
接口已有 token / session ID / 业务对象 ID;
ATRACE cookie 或异步 trace ID;
Perfetto Binder flow;
调用线程、目标服务与窄时间窗口的联合关联。
服务端:
csharp
public int addDirectWithRequestId(
int requestId,
int a,
int b) {
long s0 = SystemClock.elapsedRealtimeNanos();
Log.i(TAG, "S0 requestId=" + requestId + " atNs=" + s0);
traceBegin("calc.add server id=" + requestId);
try {
return a + b;
} finally {
long s1 = SystemClock.elapsedRealtimeNanos();
Log.i(TAG, "S1 requestId=" + requestId + " atNs=" + s1
+ " serverNs=" + (s1 - s0));
traceEnd();
}
}
对于正常完成的 post + wait 路径,记录下面八个严格有序的里程碑:
go
C0:客户端开始
S0:服务端入口开始
H0A:调用 `Handler.post()` 前
H0B:`post()` 已返回,并记录正常分支为 `true`
H1:Handler 开始
H2:Handler 结束
S1:服务端入口结束
C1:客户端返回
只计算互不重叠的时间片:
css
客户端准备、驱动处理与目标等待 = S0 - C0
服务端投递前处理 = H0A - S0
post 调用开销上界 = H0B - H0A
Handler 可观测队列等待 = H1 - H0B
Handler 执行 = H2 - H1
服务端收尾 = S1 - H2
reply 与客户端恢复 = C1 - S1
只有 C0 / S0 这组 Java marker 时,阶段 2 与阶段 3 只能作为一个合并区间观察。C0 → S0 可能同时包含 Proxy / Parcel 编码、JNI / libbinder、ioctl、驱动目标解析与 buffer / 对象转换、目标 work 排队、Binder 线程唤醒,以及目标线程从 Runnable 到 Running 的调度延迟;这个差值不能单独判断其中哪一步最慢。
要继续拆成"客户端用户态准备 → 驱动路由与排队 → 目标线程 Runnable / Running",必须增加 Binder flow、binder_driver 事件和 sched 证据。八点 Java 日志只能建立业务边界,不能测量驱动内部各阶段。
css
C0 → S0 → H0A → H0B → H1 → H2 → S1 → C1
H0B 不是 MessageQueue 内部完成链表链接的精确纳秒时刻;Java 层没有直接暴露那个内部点。它是"post() 已返回"的清晰工程边界,因此 H1 - H0B 是可复现的队列等待边界,而不是把 post() 自身耗时也混进去。
BinderLab 的基线组和阻塞组都使用一个很短的 postReturned 测量闩锁:Runnable 被取出后,也要等调用线程写完 H0B 并释放闩锁,才记录 H1。这个闩锁只建立 H0B < H1 的可计算边界;阻塞组才额外在同一 Handler 前放入 1.5 s blocker。因而 H1-H0B 是"从 post() 返回边界到任务真正开始工作的可观测等待",包含必要的调度与测量握手,不是假装成 MessageQueue 内部纯排队纳秒值。没有这种握手的一般空队列中,Looper 可能在调用线程刚从 post() 返回、尚未记录 H0B 时就进入 Runnable;出现 H1 <= H0B 时,不应制造负"排队时间",而应写成"未观察到可分辨积压",或用 trace / 等价握手进一步细化。
不要再用"总耗时减去服务端入口、Handler 排队和 Handler 执行"计算未解释区间:H0A → H2 通常本来就包含在 S0 → S1 中,再减一次会重复扣除嵌套时间。只有把时间线切成不重叠片段,各段之和才应与 C1 - C0 对齐。
这不是所有故障路径都会完整出现的"八点定律"。异常要显式分支,不能把缺失点补成正常完成:
scss
正常完成:
C0 → S0 → H0A → H0B(true) → H1 → H2 → S1 → C1
Handler 拒绝接收:
C0 → S0 → H0A → H0B(false) → S1 → C1(error)
Binder 线程等待超时:
C0 → S0 → H0A → H0B(true) → TIMEOUT → S1 → C1(error)
└──→ H1 → H2(任务可能迟到,必须定义取消/丢弃语义)
远端死亡:
C0 → DEAD_OBJECT / binderDied
(可能没有正常 S1 或 C1)
Handler 任务抛异常时,H2 应定义为"任务离开执行区间",不自动表示业务成功;是否把异常传回等待线程必须由实验代码显式实现。看到里程碑缺失或乱序,本身就是诊断证据。
不要把不同进程的 System.currentTimeMillis() 当高精度时序证据;同一设备单调时钟和 Perfetto 统一时间线更适合关联。
请求停在哪一段,应该去谁的现场找证据?
客户端线程栈为什么只证明当前等待位置?
常见客户端等待栈:
lua
android.os.BinderProxy.transactNative
android.os.BinderProxy.transact
... ICalculator$Stub$Proxy.add
... 业务调用点
它能证明:
当前线程在一笔同步 Binder 调用中等待;
业务调用点和接口方法可以定位;
如果是主线程,等待会直接消耗响应时间预算。
它不能证明根因在哪一进程。
还要先检查客户端自己的调用前阶段:
sql
是否在持有本地锁时调用 Binder;
参数是否在调用前做了大规模序列化;
是否先等待本地 Future / Condition;
调用点是否被前一条主线程任务延迟。
只有栈真正进入 transactNative(),才能把"尚未发起 Binder"排除。
服务端 Binder 线程栈怎样与目标事务对应?
1. 线程在驱动中等待,通常是健康空闲
例如 Binder 线程停在:
css
IPCThreadState::talkWithDriver
ioctl(BINDER_WRITE_READ)
如果它没有正在处理的事务,这通常表示:
线程空闲,正在等新 work。
不能因为看到 ioctl 就说"Binder 线程卡在驱动"。
2. 多条线程都停在同一服务方法
例如:
scss
Binder thread 1 → ServiceA.update() → waitForLock()
Binder thread 2 → ServiceA.query() → waitForLock()
Binder thread 3 → ServiceA.open() → remote.transact()
这可能说明:
css
服务端锁竞争;
入口执行 I/O;
嵌套 Binder 等待;
线程池正在被慢业务占满。
3. Binder 线程等待 Handler
典型栈:
arduino
ServiceMethod
→ postAndWait / Future.get / ConditionVariable.block
→ Binder.execTransactInternal
这时必须继续找目标 Handler 线程,而不是停在 Binder 线程。
为什么一两条繁忙线程不能证明线程池耗尽?
线程池耗尽不是"线程数量看起来多"。理想情况下需要同时满足证据:
目标进程有待处理 Binder 事务;
当前可用 Binder 线程都在执行或等待其他工作;
没有空闲线程及时回到驱动取新事务;
线程数已到配置上限,或 spawn 尚未发生;
新事务的服务端开始时间明显晚于发送时间。
量产 user build 未必能读取完整 binderfs/debug 状态,因此结论应分级:
| 证据等级 | 至少需要什么 | 能写到什么强度 |
|---|---|---|
| 强 | 驱动待处理队列 + Binder flow + 线程池数量/上限 + 全部线程状态 | 可以把排队明确归因到线程池无可用执行者 |
| 中 | client→server start gap + 同时刻全部 Binder 线程栈 + 没有空闲线程 | 高度支持线程池被占满,但应注明缺少驱动队列/上限证据 |
| 弱 | 只有一个客户端线程卡在 transactNative() |
只能证明客户端在等,不能判断线程池耗尽 |
常见占用形态:
css
Binder 线程直接做慢 I/O;
多条线程竞争同一把锁;
多条线程同步等待同一 Handler;
多条线程同步调用另一个已拥塞服务;
回调链造成跨进程等待环。
如果目标进程仍有空闲 Binder 线程在驱动等待,而事务却没有到达,还要检查:
是否找错服务进程;
是否事务根本没有成功发送;
是否目标是另一个 Binder node / context;
trace 是否缺事件或权限;
服务是否被冻结、死亡或调度受限。
Handler 排队怎样沿同步 Binder 反向传播到客户端?
服务端模式:
csharp
public Result queryState() {
return postToHandlerAndWait();
}
等待链是:
css
客户端线程
等 Binder reply
↓
服务端 Binder 线程
等 Handler 结果
↓
目标 Handler
等前面消息、锁、I/O 或下一笔 Binder
因此 App 看到的是:
BinderProxy.transactNative 慢
根因却可能是:
Handler Slow delivery:任务排队太久;
Handler Slow dispatch:前一条或当前任务执行太久。
对应定位方法可回看 Handler(四):从 Slow delivery 到 Slow dispatch。
跨机制诊断必须明确延迟传播方向:
Handler backlog
→ 服务端 Binder 线程等待
→ 客户端同步 Binder 等待
→ App 主线程无响应
嵌套调用怎样把等待链扩展到第三个进程?
考虑:
java
App A1
→ system_server B1
→ native service C1
→ callback App A1 / A2
客户端只展示最外层等待,但根因可能在最深一层。
诊断时为每一段记录:
| 调用段 | 调用线程 | 目标进程 | 当前状态 |
|---|---|---|---|
| T1 | App main A1 | system_server | 等 reply |
| T2 | system_server Binder B1 | native daemon | 等 reply |
| T3 | native Binder C1 | App callback | 等 App 处理 |
如果 T3 回到 App,而 App 的回调又需要主线程 A1:
A1 正在等 T1,
T1 间接又在等 A1,
就形成调用环。
Perfetto 中应沿 Binder flow 逐段展开,而不是只看第一条客户端 slice。
同步与 oneway 为什么不能共用一个完成判据?
同步调用要等到哪个时刻?
客户端耗时包含:
diff
发送
+
目标排队
+
服务端执行
+
嵌套等待
+
reply 返回
因此,从客户端调用入口到方法返回的单调时钟耗时,是这笔同步调用的端到端可观测总时长。它包含发送、目标排队、服务端执行、嵌套等待、reply 返回和客户端重新获得 CPU 等阶段,但不能单独说明主要耗时发生在哪一段。
oneway 返回后,还缺哪一类服务端证据?
第六篇解释了 oneway 的执行语义。诊断时,客户端返回只表示调用方不再等待业务 reply,不能用客户端方法耗时代表服务端完成时间。
排查 oneway 需要服务端证据:
csharp
事务是否进入目标 node 的 async 队列;
服务端何时开始处理;
同 node 前序 oneway 是否占用串行通道;
目标进程是否冻结;
异步事务空间是否接近限制。
如果不是超时而是异常,错误名能定位到哪一层?
DeadObjectException:远端端点已经不可用
DeadObjectException 表示目标对象宿主已不存在,或发生底层 Binder 死亡类错误。
它能支持:
旧远端引用已经不能完成当前事务。
它不能单独回答:
进程为什么死;
服务何时重启;
当前业务请求是否可重试;
之前请求是否已经部分执行。
后续证据要补:
进程退出原因;
logcat crash / low memory / watchdog;
DeathRecipient 时间;
ServiceManager 是否出现新 Binder;
业务幂等性和事务里程碑。
TransactionTooLargeException:检查整笔事务与共享空间
官方 TransactionTooLargeException 文档说明:Binder transaction buffer 当前为约 1 MB,并由进程中所有进行中的事务共享。
因此失败可能来自:
arduino
单个请求过大;
单个 reply 过大;
多笔中等事务并发占用共享 buffer;
大量异步事务堆积;
Parcelable 隐含携带了比预期更多的数据。
而且客户端不一定能区分:
请求没能发出去;
服务已执行,但 reply 太大没能返回。
所以客户端应按"可能部分失败"处理,不能盲目重试非幂等操作。
解决方向通常是:
arduino
保持控制事务小;
分页 / 分块;
只传必要字段;
大数据使用 fd、共享内存或专门 buffer;
Binder 只传控制信息和句柄。
Binder 适合作为控制面,不等于所有大块数据都塞进 Parcel。
FAILED_TRANSACTION:错误码之外还要找上下文
Native 层 FAILED_TRANSACTION 是一类较宽的传输失败结果。它可能关联:
arduino
目标 buffer 分配失败;
数据或 offsets 非法;
特殊对象转换失败;
事务过大;
目标状态变化;
设备分支中的额外限制。
不能看到一个错误码就直接写:
arduino
Binder buffer 超过 1 MB。
还要结合:
vbscript
客户端和服务端日志;
Parcel / reply 大小;
并发事务数量;
驱动日志或 trace;
目标进程是否存活;
失败发生在 request 还是 reply 附近。
工具很多,怎样让证据互相校验?
基础命令各自回答一个问题
1. 列服务名
adb shell service list
adb shell dumpsys -l
回答:
lua
服务名是否注册;
当前设备有哪些可 dump 服务。
2. 找服务宿主 PID
Android 16 dumpsys 支持:
css
adb shell dumpsys --pid activity
回答:
这个 Binder 服务当前由哪个进程承载。
实现可见 Android 16 dumpsys.cpp。具体旧版本设备是否支持 --pid 需要以 adb shell dumpsys --help 为准。
3. 看服务自定义状态
adb shell dumpsys activity
adb shell dumpsys activity -h
回答:
服务自己暴露的队列、会话、状态和统计。
不同服务参数不同,不要把某个服务的私有字段推广为 Binder 通用字段。
4. 受控调用
css
adb shell service --help
adb shell service call <service> <code> ...
适合实验和验证事务入口,但 service call 使用原始 transaction code,必须确认接口版本和参数协议,不能在未知设备上盲发修改状态的命令。
AOSP AIDL overview 把 dumpsys 和 service 列为设备上与服务交互的基础工具。
Perfetto 把事务与线程调度放到同一时间线
使用官方 record_android_trace 或设备端 Perfetto,至少考虑:
bash
sched
binder_driver
相关 framework atrace 类别
目标 app / service 的自定义 trace
示例:
bash
adb shell perfetto `
-o /data/misc/perfetto-traces/binder-latency.perfetto-trace `
-t 10s `
sched binder_driver am wm
adb pull /data/misc/perfetto-traces/binder-latency.perfetto-trace
不同版本和构建的可用类别不同,先用设备和官方 Perfetto Android tracing 文档确认。
在 trace 中按顺序检查:
markdown
1. 客户端线程何时开始 Binder 调用;
2. Binder flow 指向哪个目标进程 / 线程;
3. 目标线程何时从 Sleeping 变为 Runnable / Running;
4. 服务端 AIDL slice 或业务 trace 何时开始、结束;
5. 服务端是否又发起嵌套 Binder;
6. 是否切到 Handler,排队多久;
7. reply 何时返回;
8. 客户端何时重新获得 CPU 并离开调用。
要区分:
css
Runnable 但没运行:调度延迟;
Sleeping:等待驱动、锁、条件、I/O;
Running:正在消耗 CPU;
Binder flow 无目标 slice:可能缺事件、事务未送达或版本限制。
线程栈、trace、日志和服务状态交叉验证
单一证据都有盲区:
| 证据 | 强项 | 盲区 |
|---|---|---|
| 客户端线程栈 | 定位调用点与等待线程 | 看不到服务端内部 |
| 服务端线程栈 | 看入口、锁、Handler 等待 | 只是采样瞬间 |
| Perfetto | 跨进程时间线、调度与 Binder flow | 需要正确采集类别和埋点 |
| 日志 | 业务 ID、错误和状态语义 | 时间/丢日志/异步顺序可能误导 |
| dumpsys | 服务当前状态和队列 | 可能在故障后才采集,且 dump 本身会超时 |
高质量 RCA 至少让两类证据互相支持,关键结论最好三角验证:
源码 / 生成代码:证明机制可能性;
trace / 线程栈:证明这次实际执行路径;
日志 / dumpsys:证明业务对象和状态。
怎样用 BinderLab 把假设变成可证伪的 RCA?
为什么要故意制造 Binder 线程等待 Handler 的对照组?
为了把"Binder 慢"和"Handler 排队"分开,不需要先复现复杂系统服务。第一篇的基础 add() 保持简单;对照实验使用 ICalculator.addWithRequestId(requestId, a, b, injectHandlerBlocker)。false 与 true 走同一 Handler 路径,只有 true 在目标任务前加入一个只用于实验的可控队头阻塞:
完整实现见 CalculatorService.java、MainActivity.java 和 run-experiment.ps1。
八个 marker 放在哪里
scss
C0:客户端进入调用
→ Binder transact
S0:服务端 Binder 入口
→ H0A:调用 Handler.post() 前
→ Handler.post()
→ H0B:Handler.post() 返回后
→ H1:目标 Runnable 真正开始
→ H2:目标 Runnable 执行结束
→ S1:服务端 Binder 入口准备返回
→ Binder reply
C1:客户端方法返回
| 区间 | 边界所在位置 | 表示什么 |
|---|---|---|
| C0 → C1 | 客户端调用线程 | 整笔同步调用的端到端可观测总时长 |
| S0 → S1 | 服务端 Binder 线程 | 服务端入口占用时间 |
| H0A → H0B | 服务端 Binder 线程 | Handler.post() 调用本身 |
| H0B → H1 | Binder 线程记录返回边界,CalculatorWorker 记录开始 |
post 返回后到 Runnable 开始工作的可观测等待 |
| H1 → H2 | CalculatorWorker |
Runnable 实际执行时间 |
为什么需要 postReturned
空队列时,Looper 可能在 Binder 线程刚从 post() 返回、尚未来得及记录 H0B 时就开始 Runnable。BinderLab 因此使用一个很短的 postReturned 测量闩锁:Runnable 被取出后,先等 Binder 线程记录 H0B,再记录 H1。
这个闩锁只建立可计算的 H0B < H1 边界,也会引入少量调度和握手开销。因此 H1 - H0B 表示"从 post() 返回边界到任务开始工作的可观测等待",不能冒充 MessageQueue 内部的纯排队纳秒值。
baseline 与 blocked 只差一个长 blocker
| mode | injectHandlerBlocker |
目标任务之前 |
|---|---|---|
handler-latency-baseline |
false |
不放长 blocker |
handler-latency-blocked |
true |
在同一 Handler 队头放入约 1.5 秒实验 blocker |
两组使用相同 AIDL、Handler、marker 和测量闩锁,只改变长 blocker 开关。API 36 实机分别运行了 5 轮 baseline 和 5 轮 blocked;每轮八个 marker 均严格有序,日志自报 duration 与相邻时间点差值一致。关键 marker和结构化分析显示两组 post() 自身都很短,blocked 的新增等待集中在 H0B → H1,并满足 blocked.queueNsMin - baseline.queueNsMax ~= injectedBlockerNs。
这个对照可以把组间新增的主要可观测延迟定位到 Handler 队头等待。生产问题仍需结合 Binder flow 或线程栈;单次总耗时和故障注入 Demo 不能单独确定真实根因。
怎样从等待链写出可复核、可反驳的 RCA?
下面用一个虚构场景演示证据怎样拼成 RCA;其中时间、线程和 Binder flow 不属于实测证据。
现象
scss
App 主线程执行 IDisplayService.queryState() 超时;
ANR 栈停在 BinderProxy.transactNative()。
这时不能结束为:
system_server Binder 卡顿。
证据 1:客户端栈
swift
App main
→ IDisplayService$Stub$Proxy.queryState
→ BinderProxy.transactNative
结论:App 主线程在等待同步 reply。
证据 2:Binder flow
erlang
事务已经进入 system_server;
目标 Binder 线程已开始执行 queryState。
结论:不是"事务仍在客户端",也不是单纯未送达。
证据 3:服务端 Binder 线程栈
arduino
DisplayService.queryState
→ postToDisplayThreadAndWait
→ Future.get
结论:Binder 入口正在等 DisplayThread。
证据 4:DisplayThread trace / 栈
erlang
DisplayThread 正在执行前一笔窗口状态任务;
本次 queryState 的消息已入队但尚未开始。
结论:延迟主要是 Handler Slow delivery。
可验证 RCA
erlang
App 主线程等待 IDisplayService.queryState 的同步 Binder reply;
请求已于 T1 进入 system_server,目标 Binder 线程于 T2 开始执行,
因此客户端发送和驱动路由不是主要延迟;
服务端 Binder 入口在 T3 把任务投递到 DisplayThread 并同步等待;
该任务直到 T4 才开始,排队时间为 T4-T3;
DisplayThread 在这段时间执行前一笔窗口状态任务,
导致延迟沿 DisplayThread → system_server Binder 线程 → App 主线程反向传播,
最终触发 ANR。
这比"Binder 卡了"多了:
调用方;
目标服务;
已完成里程碑;
具体等待对象;
时间区间;
根因线程;
延迟传播路径。
现场信息有限时,最短排障顺序是什么?
遇到同步 Binder 慢,按下面顺序推进:
markdown
1. 定位客户端业务调用点、线程和总耗时;
2. 确认目标服务名、接口和宿主 PID;
3. 找到对应 Binder flow 或服务端入口日志;
4. 判断事务是否已到达服务端;
5. 查看服务端 Binder 线程是在执行、等锁、等 I/O、等 Handler 还是等下一笔 Binder;
6. 若转 Handler,量化 enqueue → start → end;
7. 沿嵌套 Binder 继续追到最深等待点;
8. 检查远端死亡、事务过大和 buffer 压力;
9. 用第二类证据复核结论;
10. 写出包含时间和等待链的 RCA。
如果是 oneway,把第 1 步的"客户端总耗时"换成:
服务端接收、排队和业务完成证据。
因为客户端返回时间不代表业务完成。
为什么一次线程栈采样不能充当完整因果链?
错误结论:
arduino
抓栈时 system_server Binder 线程停在 Future.get,
所以 Future 本身有 bug。
线程栈只说明采样瞬间在等待 Future。还要回答:
css
谁负责完成这个 Future?
目标 Handler 是否收到任务?
任务排队多久?
Handler 当前执行什么?
Handler 是否又在等待 Binder / 锁 / I/O?
Future 是否有超时和取消语义?
真正的根因通常在"谁应该唤醒当前等待者"那一端。
回到开头:怎样把"Binder 卡了"改写成可验证 RCA?
读完九篇后,一次远程调用不再是:
javascript
Proxy → Binder Driver → Stub
而是一个五维、可诊断模型:
csharp
进程:当前在哪个进程,何时跨进程;
线程:客户端谁等待,服务端谁执行,是否转 Handler;
层次:Java、JNI、Native 用户态还是 Kernel;
对象:业务 Proxy、BinderProxy、BpBinder、ref 还是 node;
数据:方法、Parcel、BC/BR 命令、事务还是 reply。
不要把模块关系硬排成一条时间线。先看生命周期主链:
javascript
ServiceManager 根发现(或其他首个 IBinder 获取路径)
↓
获得通用 IBinder / BinderProxy
↓
Stub.asInterface()
↓
获得业务 Proxy
↓
发起一笔或多笔业务事务
其中每一笔业务调用阶段才是:
csharp
业务 Proxy 编码方法和参数
↓
BinderProxy / BpBinder 持有远端引用
↓
IPCThreadState 组织 BC_TRANSACTION
↓
Driver 用 handle → ref → node → proc 路由
↓
服务端 Binder 线程执行 Stub.onTransact
↓
同步 reply 沿事务栈返回
calling identity、线程池与重入、引用计数、DeathRecipient、SELinux / 权限以及 trace / dumpsys 都是贯穿生命周期或单笔事务的横切治理机制,不是 reply 之后依次发生的步骤。
前八篇提供对象、线程和生命周期模型;这一篇把它们变成一条可以证实、复核和修复的等待链。
最终诊断表达应从:
system_server 卡了。
Binder 卡了。
提升为:
css
谁在等哪一笔同步事务;
事务已经到哪个里程碑;
服务端哪条线程正在做什么;
是否又等待 Handler、锁、I/O 或嵌套 Binder;
哪两到三类证据证明这个结论;
延迟怎样沿调用链反向传播。
这才是 Binder 源码学习最终应该落到的工程能力。