Binder 实战(九):一次调用变慢,到底卡在客户端、驱动、Binder 线程还是 Handler?

工程现场经常只给出两条信息:主线程栈停在 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 overviewdumpsysservice 列为设备上与服务交互的基础工具。

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)falsetrue 走同一 Handler 路径,只有 true 在目标任务前加入一个只用于实验的可控队头阻塞:

完整实现见 CalculatorService.javaMainActivity.javarun-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 源码学习最终应该落到的工程能力。

源码与官方文档

相关推荐
恋猫de小郭2 小时前
Flutter hit,一个可以灵活控制溢出点击的第三方包
android·前端·flutter
我命由我123452 小时前
Kotlin 面向对象 - 枚举排序
android·java·开发语言·java-ee·kotlin·android studio·android-studio
fengci.19 小时前
Microweber CMS 未授权路径穿越漏洞(CVE-2026-65694)
android·开发语言·前端·学习·php
stevenzqzq20 小时前
Android 矢量图(Vector Drawable)核心坐标与尺寸机制技术文档
android
plainGeekDev21 小时前
运行时获取依赖 → 编译时注入
android·java·kotlin
Wiktok21 小时前
Vue3(Vite)打包安卓APP 完整最佳实践
android·vue
略略略咯咯21 小时前
LocalDateTime
android
Co_Hui21 小时前
Android 系统服务的添加
android
蜡台1 天前
Flutter Container 与装饰完整讲解
android·javascript·flutter·dart