Android Binder 调用链排障:Service 死锁与超时定位方法
## Android Binder 调用链:死锁与超时定位问题
Android Binder 调用链:死锁与超时定位最容易伪装成随机 ANR:表面看是某个界面点不动了,实际根因可能在 SystemServer 某条 binder 事务卡住,或者客户端和服务端互相等待,把一条短调用拖成整机卡顿。很多排障失败,不是因为证据少,而是因为没先抓住线程和锁。
## 方案
先抓主线程和 binder 线程池现场;再对齐事务调用链和超时点;最后补一层锁顺序和超时保护。这个顺序能先把最危险的死锁和长事务钉住,而不是一上来全局搜源码迷路。真正落到源码层时,建议把谁持锁、谁发起 binder 调用、谁在等待返回这三件事画成最小链路,再结合 bugreport 和 binder_calls_stats 把问题缩到一个服务或一个事务。
Binder 问题最怕的是只抓到一端栈就下结论。很多现场客户端栈看起来在等服务端,服务端栈又像在等下游服务,最后其实是锁顺序反了或者慢 I/O 被塞进了同步事务。方案这一层必须把线程、锁和事务三张表对齐,否则补丁范围会越来越大,最后连回归都失控。
## 示例代码
1. 事务入口
@Override
public void doWork() throws RemoteException {
synchronized (mLock) {
performCriticalTask();
}
}
2. 调试命令
adb shell dumpsys binder_calls_stats
adb shell kill -3 $(pidof system_server)
adb bugreport > bugreport.zip
3. 超时保护
public T withTimeout(Callable task, long timeoutMs) throws Exception {
FutureTask future = new FutureTask<>(task);
new Thread(future).start();
return future.get(timeoutMs, TimeUnit.MILLISECONDS);
}
## 注意点
先盯三类:一是服务端持锁期间继续发 binder 调用;二是客户端主线程直接等远端返回;三是 binder 线程池被长任务占满。很多源码排障不是信息不够,而是没先锁住线程和锁的关系。
## 报错与排查
1. 疑似死锁
先抓 kill -3 栈和 binder stats,再确认是不是锁顺序反了。别一开始就怀疑内核,先把 Java 层和 native 层调用链拼完整。
adb shell kill -3 $(pidof system_server)
adb shell dumpsys binder_calls_stats
2. 事务超时
如果只是偶发超时,先看 binder 线程池是不是被慢任务占满,再看是否有 I/O 混进同步事务。不要把所有超时都当成单点函数慢。
public boolean isBinderBusy(int running, int max) {
return running >= max;
}
## 可运行片段
1. 状态对象
public record BinderState(String service, int runningThreads, boolean timeoutRisk) {}
2. 演示入口
public void printBinderState() {
System.out.println(new BinderState("activity", 8, true));
}
3. 验证命令
adb shell dumpsys binder_calls_stats
adb bugreport > bugreport.zip
## 结论
Android Binder 调用链:死锁与超时定位真正值钱的不是记几个命令,而是把抓现场、拼调用链、缩补丁范围这三步固定下来。只要线程、锁和事务三张表能对应起来,绝大多数卡死问题都能在源码里收口,而不是停在猜测。
