22、Binder 的常见问题:Binder 事务过大、Binder 线程耗尽、Binder 死锁的排查与解决。

Binder 用起来很顺手,但一旦出问题,往往让人抓狂。我在项目中遇到过好几次线上 Binder 异常,系统卡死、ANR 频发,最后定位到都是这三个问题之一:事务太大撑爆了缓冲区、线程池被占满、或者死锁导致互相等待。

今天我们就来聊聊这三个“Binder 杀手”。我会结合实战经验,告诉你它们是怎么发生的,怎么排查,以及怎么解决。

一、Binder 事务过大

Binder 传输数据是有大小限制的。默认情况下,一次 Binder 调用能传的数据不能超过 1MB(确切地说是 1MB - 2 个 page)。这个限制来自内核的 Binder 驱动,它用一块预分配的缓冲区来拷贝数据。

你想想看,如果你在 AIDL 接口里传一张 2MB 的图片,或者传一个包含几百个对象的 List,那很可能就超限了。这时候系统会抛出一个 TransactionTooLargeException

核心原因:Binder 内核缓冲区大小固定为 1MB,且由所有正在进行的 Binder 事务共享。单个事务超过这个阈值就会失败。

1.1 如何排查?

我个人习惯先看 logcat。当发生 TransactionTooLargeException 时,系统会打印出调用栈和传输数据的大小。你可以用以下命令过滤:

adb logcat | grep -i "TransactionTooLarge"

另外,Android 8.0 之后,系统在 ActivityManager 里也加了监控。如果某个进程频繁抛这个异常,AMS 会输出警告日志。

还有一个更直接的方法——在代码里主动检查。Binder 提供了 Binder.getTransactionSize() 方法(API 29+),你可以在发送前预估一下数据大小。

1.2 怎么解决?

解决思路无非两种:压缩数据分片传输

  • 压缩:如果传的是图片或序列化对象,先压缩再传。比如用 Bitmap.compress() 转成 JPEG,或者用 ProtoBuf 替代 Serializable。
  • 分片:把大 List 拆成多个小批次,每次传 100 条,分 10 次传完。接收端再合并。
  • 换通道:如果数据实在太大(比如几十 MB 的视频帧),就别走 Binder 了。用共享内存(MemoryFile)或者 ContentProvider 的 FileProvider 传文件路径。

避坑指南:我曾经在一个项目中,发现某个接口偶尔抛 TransactionTooLargeException,但大部分时候正常。后来一查,是因为那个接口传了一个 Bitmap,正常情况下只有 500KB,但用户拍了一张高分辨率照片后,Bitmap 解压出来直接飙到 3MB。嗯,从那以后我定了个规矩:所有 Binder 接口传图片,必须先压缩到 720p 以下。

二、Binder 线程耗尽

Binder 的线程池默认有 16 个线程(从 Android 8.0 开始,之前是 16 个,但不同版本略有差异)。如果同时有 16 个以上的 Binder 请求在等待处理,第 17 个请求就会阻塞,直到有线程空闲。

为什么会耗尽?说白了就是某个 Binder 服务端处理太慢,把线程都占住了。比如你在 UI 线程里同步调了一个 Binder 接口,而这个接口又去访问网络或者做磁盘 I/O,那它就会卡住一个 Binder 线程。如果同时有 16 个这样的调用,线程池就满了。

2.1 如何排查?

你可以用 adb shell dumpsys activity processes 查看进程的 Binder 线程状态。重点关注 binder threads 那一栏:

adb shell dumpsys activity processes | grep -A 10 "Process Record"

如果看到 Binder: 16/16 active,说明线程池已经满了。这时候任何新的 Binder 调用都会在驱动层排队,导致调用方 ANR。

另一个方法是看 /proc/[pid]/task/ 目录下的线程列表。Binder 线程的名字通常是 binder_1binder_2 这样的格式。你可以用 ps -T -p [pid] 查看它们的运行状态。

2.2 怎么解决?

  • 异步化:把同步 Binder 调用改成异步。服务端收到请求后,先返回一个 token,处理完再通过回调通知客户端。这样不会长时间占用 Binder 线程。
  • 增加线程池:如果你确定服务端处理很快,只是并发量太大,可以调大 Binder 线程池上限。通过 Binder.setThreadPoolMaxSize() 设置,但我不建议超过 32 个,否则上下文切换开销会很大。
  • 优化服务端逻辑:检查服务端有没有在 Binder 线程里做耗时操作。比如数据库查询、网络请求、文件读写,这些都应该放到工作线程里做。

注意:千万不要在 Binder 回调里直接做耗时操作。我见过有人把图片解码写在 onTransact() 里,结果一张大图解码 200ms,16 个线程瞬间被占满。正确的做法是:onTransact() 里只做参数校验和任务投递,真正的处理交给 HandlerThread 或线程池。

三、Binder 死锁

Binder 死锁是最难排查的问题之一。它通常发生在两个进程互相等待对方释放 Binder 资源的时候。

举个例子:进程 A 持有锁 L1,然后调用进程 B 的 Binder 接口。进程 B 在处理这个请求时,需要获取锁 L2,而 L2 又被进程 A 持有(比如通过另一个 Binder 调用)。这时候就死锁了。

更隐蔽的一种情况是:Binder 线程池耗尽导致的死锁。进程 A 的 16 个 Binder 线程都在等待进程 B 的响应,而进程 B 的某个 Binder 接口又需要调用进程 A 的接口——但进程 A 已经没有空闲线程来处理这个回调了。这就形成了“线程池死锁”。

核心原因:Binder 调用是同步的,如果调用链形成环,且线程池被占满,就会死锁。

3.1 如何排查?

死锁的排查,我一般分三步走:

  1. 看 ANR 日志:如果系统 ANR,会在 /data/anr/traces.txt 里记录所有线程的堆栈。搜索 "binder" 关键字,看看哪些线程卡在 Binder 调用上。
  2. 看内核日志:adb shell dmesg | grep -i binder,有时候内核会输出 Binder 驱动的状态信息,比如哪些节点在等待。
  3. lsof 看文件描述符:Binder 节点对应着 /dev/binder 的文件描述符。如果某个进程的 fd 被占满,也可能导致死锁。

还有一个高级技巧:在代码里加一个“看门狗”线程。如果某个 Binder 调用超过 5 秒没返回,就 dump 当前所有线程的堆栈。这样能快速定位死锁点。

3.2 怎么解决?

  • 避免循环调用:设计接口时,尽量不要让两个服务互相依赖。如果必须互相调用,考虑用异步回调或者消息队列解耦。
  • 使用 oneway 调用:如果某个 Binder 接口不需要等待返回值,声明为 oneway。这样调用方不会阻塞,也就不会占用 Binder 线程。
  • 设置超时:Binder 调用默认没有超时。你可以用 Binder.setTransactionTimeout() 设置一个超时时间(API 30+)。超时后驱动会主动断开连接,避免永久死锁。

避坑指南:我曾经遇到过一个死锁,排查了整整两天。最后发现是进程 A 的 Binder 线程在等待进程 B 的响应,而进程 B 在处理请求时,又通过 ContentProvider 回调了进程 A 的某个接口。但进程 A 的 Binder 线程池已经满了,所以回调永远得不到处理。解决方案很简单:把那个 ContentProvider 回调改成异步的,或者增加进程 A 的 Binder 线程池大小。嗯,从那以后我设计跨进程通信时,都会画一张调用关系图,确保没有环。

知识体系总览

下面这张图总结了 Binder 三大常见问题的核心原因、排查手段和解决方案。你可以把它当作一个快速参考。

Binder 三大常见问题排查体系 事务过大 TransactionTooLargeException 核心原因 Binder 内核缓冲区 1MB 限制 排查手段 • logcat 过滤 TransactionTooLarge • Binder.getTransactionSize() 解决方案 • 压缩数据(Bitmap 降分辨率) • 分片传输(分批发送) • 换通道(共享内存 / FileProvider) 线程耗尽 Binder Thread Pool Exhausted 核心原因 16 个 Binder 线程全部被占满 排查手段 • dumpsys activity processes • /proc/[pid]/task/ 查看线程状态 解决方案 • 异步化(回调替代同步等待) • 增加线程池(setThreadPoolMaxSize) • 优化服务端逻辑(避免耗时操作) Binder 死锁 Deadlock / 线程池死锁 核心原因 循环依赖 + 线程池占满 排查手段 • ANR traces.txt 分析堆栈 • dmesg | grep binder 解决方案 • 避免循环调用(接口设计检查) • 使用 oneway 声明 • 设置 Binder 调用超时 三者可能相互引发:事务过大 → 线程卡住 → 线程耗尽 → 死锁

这三个问题其实经常一起出现。事务过大导致某个 Binder 线程长时间卡在数据拷贝上,进而导致线程池耗尽,如果此时有循环依赖,就演变成死锁。所以排查的时候,我建议从最明显的异常日志入手,一步步缩小范围。

好了,Binder 的三大常见问题就聊到这里。记住:事务大小要控制,线程池要监控,调用链要避免环。做到这三点,大部分 Binder 问题都能提前规避。


公众号:蓝海资料掘金营,微信deep3321