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 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 源码学习最终应该落到的工程能力。

源码与官方文档

相关推荐
三少爷的鞋10 小时前
AI 时代,我们都将成为通才型开发者:只懂 Android,已经不够了
android
Dovis(誓平步青云)14 小时前
家里设备越来越多,如何用一张空间地图控制灯光和温度![
android·java·前端·javascript·人工智能·电脑
晚风叙码16 小时前
MySQL 数据类型详解:从数值到字符串,一篇讲透
android·mysql·adb
传奇开心果编程16 小时前
【Compose Multiplatform 跨端开发学与练】第3课 布局与组件
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程19 小时前
【Compose Multiplatform 跨端开发学与练】第8课 资源管理与主题
android·windows·学习·ios·kotlin·web·composer
传奇开心果编程20 小时前
【Compose Multiplatform 跨端开发学与练】第9课 测试与调试
android·学习·macos·ios·kotlin·web·composer
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第4课 导航与路由
android·windows·学习·ui·ios·kotlin·composer
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第6课 状态管理与架构
android·学习·ui·ios·架构·kotlin·composer
事圆则缓21 小时前
Android AOSP 定制常见概念:源码目录、系统镜像与刷机流程
android
传奇开心果编程21 小时前
【Compose Multiplatform 跨端开发学与练】第2课 Compose 基础语法
android·windows·学习·ui·ios·kotlin·composer