Android Performance

Android Perfetto 系列 10 - Binder 调度与锁竞争

Word count: 9.4kReading time: 35 min
2025/11/16
loading

Perfetto 系列来到第十篇,聚焦 Binder 这一 Android 跨进程通信的核心机制。Binder 承载着大部分系统服务与应用的交互,也常常是性能瓶颈的源头。本文站在系统开发与性能调优的视角,结合 android.binderschedthread_stateandroid.java_hprof 等数据源,给出一套可直接落地的诊断流程,帮助初学者和进阶开发者定位耗时、线程池压力与锁竞争等问题。

本文目录

Perfetto 系列文章

  1. Android Perfetto 系列目录
  2. Android Perfetto 系列 1:Perfetto 工具简介
  3. Android Perfetto 系列 2:Perfetto Trace 抓取
  4. Android Perfetto 系列 3:熟悉 Perfetto View
  5. Android Perfetto 系列 4:使用命令行在本地打开超大 Trace
  6. Android Perfetto 系列 5:Android App 基于 Choreographer 的渲染流程
  7. Android Perfetto 系列 6:为什么是 120Hz?高刷新率的优势与挑战
  8. Android Perfetto 系列 7 - MainThread 和 RenderThread 解读
  9. Android Perfetto 系列 8:深入理解 Vsync 机制与性能分析
  10. Android Perfetto 系列 9 - CPU 信息解读
  11. Android Perfetto 系列 10 - Binder 调度与锁竞争(本文)
  12. 视频(B站) - Android Perfetto 基础和案例分享

Binder 基础与案例

对于首次接触 Binder 的读者,理解它的角色和参与者至关重要。可以先把 Binder 粗暴地理解成“跨进程的函数调用”:你在一个进程里像调用本地接口一样写代码,真正的调用和数据传输则由 Binder 帮你完成。整体上它是 Android 的主力跨进程通信(IPC)机制,核心包含四个组件:

  1. Client:应用线程通过 IBinder.transact() 发起调用,将 Parcel 序列化的数据写入内核。
  2. Service(Server):通常运行在 SystemServer 或其他进程中,通过 Binder.onTransact() 读取 Parcel 并执行业务逻辑。
  3. Binder Driver:内核模块 /dev/binder 负责线程池调度、缓冲区管理、优先级继承等,是连接双方的“信使”。
  4. Thread Pool:服务端通常维护一组 Binder 线程。需要注意的是,线程池并不是一开始就创建满的,而是按需创建。Java 层默认最大线程数约为 15 个 Binder 工作线程(不含主线程),Native 层通过 ProcessState 也可以配置最大线程数(默认值通常也是 15)。当所有 Binder 线程都忙碌时,新的请求就会在驱动层排队等待空闲线程。

为什么需要 Binder?

Android 采用多进程架构来隔离应用、提升安全性与稳定性。每个 APK 运行在独立的用户空间,当需要访问系统能力(相机、位置、通知等)时,必须跨进程调用 Framework 或 SystemServer。

传统 IPC 方案的局限:

IPC 方式 问题
Socket 开销大,缺少身份校验
Pipe 仅支持父子进程,单向通信
共享内存 需要额外的同步机制,缺少访问控制

Binder 在内核层解决了这些问题,提供了三个关键能力:一是身份与权限(基于 UID/PID 校验,确保调用方合法);二是同步与异步调用(同步模式下 Client 等待 Server 返回,这是最常见的模式,而异步模式下 Client 发送后立即返回,适用于通知、状态上报等场景);三是优先级继承(当高优先级 Client 调用低优先级 Server 时,Server 会临时提升优先级,避免优先级反转问题)。

因此,当我们在应用里写 locationManager.getCurrentLocation() 这样的语句时,底层必然借助 Binder 把调用安全、可靠地传递给 SystemServer。

从 App 开发者视角的案例

假设我们在应用中调用 LocationManager#getCurrentLocation()。这个 API 的真实实现位于 system_server 中的 LocationManagerService。调用路径可以概括为:首先,在 Proxy 侧,App 线程通过 Context.getSystemService 拿到一个 ILocationManager 的代理对象(BinderProxy);然后进行序列化,调用 getCurrentLocation() 时,代理会把参数写入 Parcel,执行 transact();接着是内核传输,Binder 驱动将该事务排入 system_server 的 Binder 线程队列,并唤醒一个空闲线程(例如 Binder:1460_5);随后在 Stub 侧LocationManagerService(Stub)所在的线程被唤醒,读取参数、执行定位逻辑(可能涉及 HAL 层交互);最后是返回阶段,Service 执行完毕,将结果写入 Parcel,驱动唤醒原 App 线程,App 线程从 waitForResponse() 返回拿到数据。

在 Perfetto 中,这条链路会显示为:android.binder 轨道上的 service = android.location.ILocationManager 事务;App 线程在 thread_state 里处于 S (Sleeping) 状态,且 blocked_function 通常涉及 binder_thread_readepoll;SystemServer 的 Binder 线程出现 Running 切片;以及 Flow 箭头(Perfetto 会用箭头把 Client 的 transact 和 Server 的 run 连接起来)。

Perfetto 观测准备

要在 Perfetto 中诊断 Binder,需要提前准备好数据源与 Trace 配置。

数据源与轨道总览

Perfetto 的 Binder 相关信号主要来自三个数据源,它们分别工作在不同层级,提供的信息粒度和适用场景也有所不同。

linux.ftrace(内核层) 是最通用、最基础的数据源,兼容所有 Android 版本。它直接读取内核的 ftrace 事件,包括 binder_transaction(事务开始)、binder_transaction_received(服务端收到事务)、binder_lock(内核 Binder 锁,通常不用太关注,除非你在调试驱动本身)等。配合调度相关的事件(sched_switchsched_waking),可以完整还原出”Client 发起调用 → 内核唤醒 Server 线程 → Server 处理 → 返回”这条链路。如果你手中的设备是 Android 12 或 13,基本上只需要依赖这个数据源就够了,Perfetto UI 会自动把 ftrace 的 binder 事件解析成直观的 “Transactions” 视图。

android.binder(用户层) 是一个较新的数据源,主要完善于 Android 14/15 及之后的版本。它利用内核的新接口或 tracepoint 提供了更丰富的语义信息,比如可以直接区分 reply(回复)和 transaction(请求),提供 blocking_dur_ns(客户端阻塞时长)这样的预计算指标,还能区分 lazy_async(延迟派发的异步事务)等。如果你的设备是 Android 14 以上,建议同时开启这个数据源,可以获得更详细的分析维度。

android.java_hprof(锁竞争) 用于捕获 Java 层的 Monitor Contention(也就是 synchronized 关键字产生的锁竞争)。虽然名字里带 hprof,但在这个场景下它主要是通过 ART 虚拟机的 instrumentation 机制来记录锁等待事件,而不是做内存快照。需要注意的是,开启这个数据源会有一定的性能开销(因为每次锁竞争都要记录),所以建议只在问题可以稳定复现的情况下、在较短的时间窗口内启用,避免 Trace 文件过大或影响复现场景的真实性。

Trace Config 推荐

以下配置兼顾了兼容性与新特性,建议作为标准的 Binder 分析模板。将配置保存为 binder_config.pbtx 即可使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# ============================================================
# Binder 分析专用 Perfetto 配置
# 适用范围:Android 12+ (部分数据源需要 Android 14+)
# ============================================================

# --- 缓冲区与时长设置 ---
buffers {
size_kb: 65536 # 64MB 缓冲区,适合中等复杂度场景
fill_policy: RING_BUFFER
}
duration_ms: 15000 # 15 秒抓取时长,可根据需要调整

# --- 数据源 1:android.binder (Android 14+) ---
# 提供用户层 Binder 语义,包括 reply/transaction 区分、blocking_dur_ns 等
data_sources {
config {
name: "android.binder"
android_binder_config {
intercept_transactions: true # 拦截事务
intercept_late_reply: true # 捕获延迟回复
# 可选:过滤特定进程以减少 Trace 体积
# filter { name: "system_server" }
# filter { name: "com.android.systemui" }
# filter { name: "你的应用包名" }
}
}
}

# --- 数据源 2:linux.ftrace (内核层) ---
# 最通用的数据源,兼容所有 Android 版本
data_sources {
config {
name: "linux.ftrace"
ftrace_config {
# Binder 核心事件
ftrace_events: "binder/binder_transaction" # 事务开始
ftrace_events: "binder/binder_transaction_received" # 服务端收到事务
ftrace_events: "binder/binder_transaction_alloc_buf" # 缓冲区分配(诊断 TransactionTooLarge)
ftrace_events: "binder/binder_set_priority" # 优先级继承
ftrace_events: "binder/binder_lock" # 内核锁(通常可省略)
ftrace_events: "binder/binder_locked"
ftrace_events: "binder/binder_unlock"

# 调度事件(串联 Client/Server 线程)
ftrace_events: "sched/sched_switch"
ftrace_events: "sched/sched_waking"
ftrace_events: "sched/sched_wakeup"
ftrace_events: "sched/sched_blocked_reason" # 阻塞原因

# 可选:应用层 Trace 点(需要 atrace)
atrace_categories: "binder_driver" # Binder 驱动层
atrace_categories: "sched" # 调度
atrace_categories: "am" # ActivityManager
atrace_categories: "wm" # WindowManager
# atrace_categories: "view" # 如需分析 UI 可开启

# 符号化内核调用栈
symbolize_ksyms: true

# 优化调度事件存储,减少 Trace 体积
compact_sched {
enabled: true
}
}
}
}

# --- 数据源 3:android.java_hprof (Java 锁竞争) ---
# 捕获 synchronized 锁等待,有一定性能开销
data_sources {
config {
name: "android.java_hprof"
java_hprof_config {
track_contended_locks: true # 开启锁竞争追踪
track_allocation_contexts: false # 关闭内存分配追踪(减少开销)
track_java_heap: false # 关闭堆采样
}
}
}

# --- 数据源 4:linux.process_stats (进程信息) ---
# 提供进程名、PID 等基础信息
data_sources {
config {
name: "linux.process_stats"
process_stats_config {
scan_all_processes_on_start: true
}
}
}

配置项说明

数据源 作用 Android 版本要求 开销
android.binder 用户层 Binder 语义(blocking_dur、reply 等) 14+
linux.ftrace (binder/*) 内核层 Binder 事件 所有版本
linux.ftrace (sched/*) 调度事件,串联线程唤醒 所有版本
android.java_hprof Java 锁竞争(Monitor Contention) 10+ 中-高
linux.process_stats 进程名称和 PID 映射 所有版本 极低

提示:如果设备是 Android 12/13,android.binder 数据源可能功能有限,主要依赖 linux.ftrace 即可。Perfetto UI 会自动把 ftrace 的 binder 事件解析成直观的 Transactions 视图。

快速上手:3 步抓取与查看 Binder Trace

  1. 抓取 Trace

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 推送配置
    adb push binder_config.pbtx /data/local/tmp/

    # 开始抓取
    adb shell perfetto --txt -c /data/local/tmp/binder_config.pbtx \
    -o /data/misc/perfetto-traces/trace.pftrace

    # ... 操作手机复现卡顿 ...

    # 取出文件
    adb pull /data/misc/perfetto-traces/trace.pftrace .
  2. 打开 Trace:访问 ui.perfetto.dev,拖入 trace 文件。

  3. 添加关键视图

    • 左侧点击 TracksAdd new track
    • 搜索 “Binder”,添加 Android Binder / TransactionsAndroid Binder / Oneway Calls
    • 搜索 “Lock”,添加 Thread / Lock contention(如果有数据)

其他 Binder 分析工具

除了 Perfetto 外,还有一些工具可以辅助分析 Binder 问题。这里介绍两个比较实用的:am trace-ipc(Android 自带,无需额外安装)和 binder-trace(开源工具,功能更强但配置门槛较高)。

am trace-ipc:Java 层 Binder 调用追踪

am trace-ipc 是 Android 系统自带的命令行工具,用于追踪 Java 层的 Binder 调用堆栈。它的工作原理是在 BinderProxy.transact() 处插桩,记录所有经过这里的调用,并统计每种调用模式出现的次数。这个工具最大的优点是零配置、即开即用,不需要 root 权限,也不需要安装额外软件。

基本用法很简单,就是”开始 → 操作 → 停止导出”三步:

1
2
3
4
5
6
7
8
9
10
# 1. 开始追踪(此时系统开始记录所有进程的 Binder 调用)
adb shell am trace-ipc start

# 2. 在手机上执行你要分析的操作(比如启动某个应用、触发卡顿场景等)

# 3. 停止追踪并导出结果到文件
adb shell am trace-ipc stop --dump-file /data/local/tmp/ipc-trace.txt

# 4. 把结果文件拉到电脑上查看
adb pull /data/local/tmp/ipc-trace.txt

导出的文件是纯文本格式,内容类似这样:

1
2
3
4
5
6
7
Traces for process: com.example.app
Count: 15
java.lang.Throwable
at android.os.BinderProxy.transact(BinderProxy.java:xxx)
at android.app.IActivityManager$Stub$Proxy.startActivity(...)
at android.app.Instrumentation.execStartActivity(...)
...

从输出可以看到,它会按进程分组,列出每种 Binder 调用的完整 Java 堆栈以及出现次数(Count)。这对于回答”我的应用在这个操作过程中到底调用了多少次 Binder、都调了哪些服务”这类问题非常直接。

与 Perfetto 配合使用:一个很有用的特性是,开启 am trace-ipc 后,Binder 调用信息也会同步输出到 Perfetto/Systrace 的 trace events 中。这意味着你可以一边用 Perfetto 看时间线上的耗时分布,一边通过 trace-ipc 的输出确认具体是哪个 Java 方法发起的调用。两者结合,既有宏观的时间维度,又有微观的调用栈细节。

这个工具特别适合以下场景:怀疑 ANR 或卡顿是由于频繁 IPC 调用引起时,可以用它快速验证;想统计某个用户操作(如启动、滑动)过程中总共发生了多少次 Binder 调用;需要拿到 Binder 调用的完整 Java 调用栈来定位代码位置。

binder-trace:实时 Binder 消息解析

binder-trace 是一个开源的 Binder 分析工具,可以实时拦截和解析 Android Binder 消息。它的定位类似于”Wireshark for Binder”——就像 Wireshark 可以抓取和解析网络数据包一样,binder-trace 可以抓取和解析 Binder 事务的具体内容,包括接口名、方法名、甚至传递的参数值。

这个工具基于 Frida 进行动态注入(Frida 是一个流行的动态插桩框架),因此使用前需要满足一些条件:设备需要已经 root(或者使用模拟器),并且要先在设备上部署 frida-server。另外本地电脑需要 Python 3.9 以上版本。配置好环境后,使用方式如下:

1
2
# 追踪指定应用的 Binder 通信(-d 指定设备,-n 指定进程名,-a 指定 Android 版本)
binder-trace -d emulator-5554 -n com.example.app -a 11

运行后会打开一个交互式界面,实时显示目标进程的所有 Binder 交互。你可以通过配置文件按接口、方法、事务类型等条件进行过滤(避免信息过载),也可以用快捷键暂停/继续记录、清除屏幕等。工具内置了 Android 9 到 14 各版本的 AIDL 结构定义,能够自动解析大部分系统服务的调用参数。

这个工具更适合安全研究和逆向工程场景,比如你想深入分析某个应用和系统服务之间具体传递了什么数据,或者想了解某个未公开 API 的调用细节。不过对于日常的性能分析来说,binder-trace 的配置门槛偏高(需要 root、需要部署 frida),而且它关注的是”消息内容”而非”耗时分布”,所以通常 Perfetto 配合 am trace-ipc 就足够了。如果你在做安全审计或者需要逆向分析某个应用的 IPC 行为,binder-trace 会是一个很强大的工具。

Binder 分析工作流

拿到 Trace 后,不要直接在大海捞针。推荐按照“找目标 → 看耗时 → 查线程 → 找锁”的顺序进行。

步骤一:定位事务耗时

分析的第一步是找到你关心的那次 Binder 调用。在 Perfetto 中有几种常用的定位方式:如果你已经知道是哪个进程发起的调用,可以直接在 Transactions 轨道里找到你的 App 进程作为 Client 的区域;如果你知道调用的接口名或方法名,可以按 / 键打开搜索框,输入 AIDL 接口名(如 ILocationManager)或方法名来快速定位;如果你是在排查 UI 卡顿问题,最直接的方式是先看 UI 线程的 thread_state 轨道,找到处于 S(Sleeping)状态且时长较长的片段——如果这段时间主线程几乎没有在执行代码,那很可能就是在等待 Binder 调用返回,这里就是分析的起点。

选中一个 Transaction Slice 后,右侧的 Details 面板会显示这次事务的详细信息。其中有三个关键的耗时指标需要重点关注:latency_ns 表示总耗时,即客户端从发出请求到收到回复的完整时间;server_latency_ns 表示服务端处理耗时,即服务端线程实际执行业务代码的时间;blocking_dur_ns 表示客户端阻塞耗时,即客户端线程在内核里等待的时间。

理解这几个指标之间的关系非常重要,因为它直接决定了你下一步应该往哪个方向深挖。如果 latency_ns 很长,但 server_latency_ns 很短,说明时间并没有花在服务端处理上,而是耗在了 Binder 驱动调度或服务端排队上(通常意味着 Service 的线程池很忙,新请求需要等待空闲线程)。这种情况下你需要去检查服务端的线程池状态,这就是步骤二要做的事情。如果 latency_nsserver_latency_ns 差不多且都很长,说明服务端处理本身就很慢,这时候你需要跳转到服务端的 Binder 线程,看它在这段时间里到底在干什么——是在跑业务代码、等锁、还是等 IO。

步骤二:评估线程池与 Oneway 队列

如果步骤一的分析发现耗时主要不在服务端处理,而是在”排队”上,那就需要进一步检查 Binder 线程池的状态了。在深入分析之前,先回答一个经常被问到的问题:**”每个进程大概会有多少个 Binder 线程?system_server 的 Binder 线程池规模大致是什么量级?什么情况下会’耗尽’?”**

system_server 的 Binder 线程池规模

在上游 AOSP(Android 14/15)中,Binder 线程池的设计思路是:按需增长、可配置、没有单一固定数字

  • 线程池是按需增长的:每个服务端进程在 Binder 驱动中维护一个线程池,实际线程数会根据负载按需增减,上限由内核中的 max_threads 字段和用户态 ProcessState#setThreadPoolMaxThreadCount() 等配置共同决定。
  • 典型上限在 15~16 个工作线程:大部分 AOSP 版本中,应用进程的 Java Binder 线程池上限约为 15~16 个工作线程system_server 这类核心进程的 Binder 线程池默认也在这个数量级,处于“十几条线程”的范围内。
    某些厂商 ROM 或定制内核会根据自身负载模型,把上限调大或调小(例如调到几十条线程),因此你在不同设备上通过 ps -T system_servertop -H 或 Perfetto 数 Binder: 线程时,看到的具体数字可能会有差异。
  • 以实际观测为准,而不是死记一个数字:在 Perfetto 里,更推荐的做法是直接展开某个进程,看有多少个 Binder:xxx_y 线程轨道,以及它们在抓 Trace 期间的活跃程度,以此来评估线程池的“规模”和“繁忙度”。

Binder 线程数、缓冲区与“Binder 耗尽”

在性能分析中,大家提到“Binder 个数”时,往往会混在一起谈三类不同的资源限制:

Binder 线程池耗尽是指某个进程内所有 Binder 工作线程都处于 Running / D / S 等忙碌状态,没有空闲线程可以被驱动唤醒处理新事务。其现象包括 Client 线程在 thread_state 轨道里长时间停留在 S 状态(调用栈停在 ioctl(BINDER_WRITE_READ) / epoll_wait),以及 android.binder 轨道中对应服务的 queue_len 持续偏高(说明请求在排队)。对于 system_server 这类关键进程,线程池被打满意味着系统服务响应能力下降,很容易放大为全局卡顿或 ANR

Binder 事务缓冲区耗尽涉及每个进程在 Binder 驱动里的一块有限大小的共享缓冲区(典型值约 1MB 量级),用于承载正在传输的 Parcel 数据。典型场景包括一次事务传输过大的对象(如大 Bitmap、超长字符串、大数组等),以及大量并发事务尚未被消费完,导致缓冲区中堆积了太多尚未释放的 Parcel。可能的结果包括内核日志中出现 binder_transaction_alloc_buf 失败、Java 层抛出 TransactionTooLargeException,以及后续事务在驱动层长时间排队甚至失败(看起来像是“Binder 被用光了”)。解决这类问题的思路不是通过“多开线程”,而是控制单次传输的数据量(拆包、分页、流式协议),并对大块数据优先使用 SharedMemory / 文件 / ParcelFileDescriptor 等机制。

Binder 引用表 / 对象数量方面,Binder 驱动会为每个进程维护引用表和节点对象,这些也有上限,但在大多数实际场景中,很少首先撞到这里。常见风险是长时间持有大量 Binder 引用却不释放,更多体现为内存/稳定性问题,而不是 UI 卡顿。

在 Perfetto 里分析时,可以带着一个判断框架:
“现在的慢,是因为线程池被打满,还是事务过大/缓冲区被用光?”
前者主要看 **Binder 线程数与它们的 thread_state**,以及 queue_len;后者则关注 单次事务的大小、并发事务数量和是否伴随 TransactionTooLargeException / binder_transaction_alloc_buf 相关日志


现在回到我们的分析场景:

Binder 线程池的繁忙程度直接决定了服务的并发处理能力。对于同步事务来说,如果服务端所有 Binder 线程都处于 RunningUninterruptible Sleep (D) 状态,新的同步请求就会在内核里排队等待,客户端线程会长时间阻塞在 ioctl(BINDER_WRITE_READ)epoll_wait 上。在 Perfetto 里的表现就是:主线程长时间处于 S 状态,但看它的 Java 代码执行情况却几乎是空白的——时间都花在等待上了。对于 Oneway(异步)事务,情况稍有不同:虽然客户端发完就返回、不会阻塞等待,但同一个 IBinder 对象上的 Oneway 请求在服务端往往是串行消费的(有一个队列)。如果某个 App 在短时间内发送了大量 Oneway 请求,这个队列就会被拉长,不仅这个 App 后续的 Oneway 执行会延迟,同一服务上其他调用者的响应也可能受到影响。

在 Perfetto 中诊断线程池问题,有几个指标值得关注。首先是 Queue Length(队列长度),在 android.binder 轨道中可以观察到 queue_len 这个指标,如果它持续偏高,说明请求的生产速度远大于消费速度,线程池处于饱和状态。其次要留意缓冲区相关的迹象(如前面提到的 TransactionTooLargeException 或内核日志中的 binder_transaction_alloc_buf 失败),这通常意味着单次事务过大或并发事务过多。最直观的方式是直接观察 Binder 线程状态:找到 system_server 进程并展开所有线程,如果发现以 Binder: 开头的线程大部分都处于忙碌状态(时间线上密密麻麻全是 Slice,几乎没有空隙),说明线程池已经饱和了。

关于 Oneway 调用在 Perfetto 中的识别:同步调用(Two-way)和异步调用(Oneway)在 Perfetto 中的表现有明显区别,学会区分它们对分析很有帮助。同步调用时,客户端会阻塞等待(thread_state 显示 S),Perfetto 会画出双向的 Flow 箭头(transaction → reply);而 Oneway 调用客户端发完就返回、几乎无阻塞,Flow 箭头只有单向的 transaction,没有 reply 回来。另外,Oneway 调用的 Slice 名称后面可能会带 [oneway] 标记,它的 latency_ns 也只代表发送耗时,而不是往返时间。

在分析 Oneway 相关问题时,重点关注两件事:一是服务端的队列深度(如果同一 IBinder 对象上的 Oneway 请求堆积,后续请求的实际执行时机会被不断延后);二是是否存在批量发送的模式(短时间内大量 Oneway 调用会形成”尖峰”,在 Perfetto 中表现为服务端 Binder 线程上密集排列的短 Slice)。

值得一提的是,SystemServer 的 Binder 线程不仅要处理来自各个 App 的请求,还要处理系统内部的调用(比如 AMS 调 WMS、WMS 调 SurfaceFlinger 等)。如果某个”行为不端”的 App 在短时间内疯狂发送 Oneway 请求,可能会把某个系统服务的 Oneway 队列塞满,进而影响到其他 App 的异步回调时延,造成全局性的卡顿感。

步骤三:排查锁竞争

如果你跳转到服务端的 Binder 线程,发现它在处理你的请求期间长时间处于 S(Sleeping)或 D(Disk Sleep / Uninterruptible Sleep)状态,那通常意味着它在等待某个资源——要么是在等锁,要么是在等 IO。锁竞争是 SystemServer 中非常常见的性能瓶颈来源,因为 SystemServer 里运行着大量服务,它们之间共享很多全局状态,而这些状态往往通过 synchronized 锁来保护。

Java 锁(Monitor Contention) 是最常见的情况。SystemServer 中有不少全局锁,比如 WindowManagerService 的 mGlobalLock、ActivityManagerService 的一些内部锁等。当多个线程同时需要访问被这些锁保护的资源时,就会产生竞争。在 Perfetto 中,如果你看到某个 Binder 线程状态为 S,并且 blocked_function 字段包含 futex 相关的符号(如 futex_wait),那基本可以确定是在等 Java 锁。要进一步确认是在等哪个锁、被谁持有,可以查看 Lock contention 轨道。Perfetto 会把锁竞争的关系可视化出来:用连接线标出 Owner(持有锁的线程,比如 android.display 线程)和 Waiter(等待锁的线程,比如处理你请求的 Binder:123_1)。点击 Contention Slice,还可以在 Details 面板里看到锁对象的类名(比如 com.android.server.wm.WindowManagerGlobalLock),这对于理解问题的根源非常有帮助。

Native 锁(Mutex / RwLock) 的情况相对少见一些,但在某些场景下也会遇到。表现形式类似:线程状态为 DS,但调用栈里出现的是 __mutex_lockpthread_mutex_lockrwsem 等 Native 层的符号,而不是 Java 的 futex_wait。分析这类问题通常需要结合 sched_blocked_reason 事件来看线程具体在等什么,属于比较进阶的内容,这里就不展开了。

使用 SQL 统计 system_server 中的锁竞争(可选)

如果你已经比较熟悉 Perfetto SQL,这里给出一个可以直接在 Perfetto UI 中运行的查询,用于统计 system_server 进程中 Java monitor 锁竞争(Lock contention on a monitor lock)的情况。
其中 lock_depth 表示发生锁竞争时,参与同一个对象锁竞争的线程个数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
select count(1) as lock_depth, s.slice_id,s.track_id,s.ts,s.dur,s.dur/1e6 as dur_ms,ctn.otid,s.name

from slice s, (select slice_id,track_id,ts,dur,name,substr(name, 46, instr(name,')')-46) as otid
from slice t
WHERE name like 'Lock contention on a monitor lock %'
order by dur) ctn
JOIN thread_track ON s.track_id=thread_track.id JOIN thread USING(utid) JOIN process USING(upid)
WHERE
process.name = 'system_server'
and s.name like 'Lock contention on a monitor lock %'
and substr(s.name, 46, instr(s.name,')')-46) = ctn.otid
and ctn.slice_id <> s.slice_id
and ctn.ts >= s.ts and (ctn.ts+ctn.dur) <= (s.ts+s.dur)
group by s.slice_id
order by s.dur desc;


案例演练:窗口管理延迟

下面通过一个真实案例来演示前面介绍的分析流程。场景是:应用启动时出现明显的动画卡顿

1. 发现异常

首先在 Perfetto 中打开 Trace,找到 App 的 UI Thread 轨道。观察发现有一帧的 doFrame 耗时达到了 30ms(正常情况下 60Hz 屏幕一帧应该在 16.6ms 以内)。放大看这一帧对应的 thread_state,发现主线程有 18ms 处于 S(Sleeping)状态,说明这段时间主线程并没有在执行代码,而是在等待什么。

2. 追踪 Binder 调用

点击这 18ms 的 S 片段,查看右侧的 Details 面板,可以看到关联的 Slice 信息。从中发现主线程当时正在调用 IActivityTaskManager.startActivity——这是一个跨进程的 Binder 调用,调用方是 App,被调方是 system_server。Perfetto 的 Flow 箭头清晰地指向了 system_server 进程中的 Binder:1605_2 线程,说明这个请求是由这个 Binder 线程在处理。

3. 服务端分析

沿着 Flow 箭头跳转到 system_serverBinder:1605_2 线程。观察发现,这个线程确实在运行(Running 状态),但它执行了整整 15ms 才结束。进一步观察这个 Binder 线程的 Lock contention 轨道(有些版本显示在线程轨道上方的小条里),发现有一段红色的锁竞争标记。

4. 锁定元凶

点击这段锁竞争标记,Details 面板显示了关键信息:这个线程在等待 com.android.server.wm.WindowManagerGlobalLock 这把锁,锁的持有者(Owner)是 android.anim 线程(系统动画线程),等待时长(Duration)是 12ms。

结论:App 启动时发起的 startActivity Binder 请求,在 SystemServer 端需要获取 WindowManagerGlobalLock 锁才能继续执行,但这把锁当时正被系统动画线程持有(用于更新窗口状态),导致 Binder 线程等了 12ms 才拿到锁、完成处理。这 12ms 的锁等待加上其他开销,就导致了 App 主线程被阻塞了 18ms,最终表现为一帧卡顿。

优化方向:这种情况属于系统层面的锁竞争,App 端很难直接修复。但 App 端可以做一些规避:避免在系统动画密集执行期间(比如启动动画、转场动画)发起复杂的 Window 操作,或者想办法减少冷启动阶段的 IPC 调用频率,降低撞上锁竞争的概率。


案例演练:Binder 线程池饱和

再来看另一个案例。场景是:多个应用同时启动,系统整体变得卡顿

1. 发现异常

在 Perfetto 中观察发现,多个 App 的主线程在同一时间段内都处于 S(Sleeping)状态,而且从调用栈来看,它们都在等待各自的 Binder 调用返回。用户反馈是”点什么都没反应,过几秒才有动静”,整个系统响应迟缓。

2. 检查 system_server 的线程池状态

既然多个 App 都在等 Binder 返回,那问题很可能出在服务端。展开 system_server 进程,观察所有以 Binder: 开头的线程轨道。发现情况不妙:几乎所有 Binder 线程(从 Binder:1460_1Binder:1460_15)都处于 Running 状态,每个线程的时间线上 Slice 密密麻麻、几乎没有间隙,完全没有空闲线程可以处理新请求。这是典型的线程池饱和现象。

3. 分析排队情况

android.binder 轨道中进一步观察,发现 queue_len(队列长度)指标持续偏高(大于 5,意味着始终有请求在排队),而且多个 Client 的 blocking_dur_ns(阻塞时长)远大于 server_latency_ns(服务端处理时长)——这说明请求大部分时间都花在排队等待上,而不是实际处理上。

4. 定位根因

进一步检查各个 Binder 线程具体在处理什么事务,发现一个有趣的现象:某个后台应用在短时间内发起了大量的 IPackageManager 查询请求。每次查询本身耗时不长(大约 5ms),但数量太多(几百次),这些请求把线程池占满了,导致其他应用(包括前台应用)的请求都要排队等待。

结论:某个”行为不端”的应用通过批量 Binder 调用”挤占”了 system_server 的线程池资源,导致其他应用的正常请求被大幅延迟,表现为全局性的卡顿。

优化方向:从应用侧来说,应该避免这种批量循环调用的模式——如果需要批量查询,应该使用系统提供的批量接口(比如用 getPackagesForUid 代替循环调用 getPackageInfo),或者把请求分散到不同时间点、异步执行。从系统侧来说,可以考虑对特定服务增加 rate limiting(限流),或者优化热点服务的处理效率,减少单次请求的耗时。

最新平台特性与优化建议

随着 Android 版本的迭代,Binder 机制本身也在不断进化,引入了一些新特性来改善性能和稳定性。了解这些特性有助于理解 Perfetto 中某些现象的成因,也能帮助你写出更”友好”的代码。

Binder Freeze(Android 12+) 是一个对减少 ANR 很有帮助的特性。当一个进程被系统判定为 Cached(缓存状态)并被”冻结”时,它的 Binder 接口也会随之冻结。此时如果有其他进程试图同步调用这个被冻结进程的 Binder,系统会更快地让这次调用直接失败(在 logcat 中会看到 FROZEN 相关的错误信息),而不是让调用方傻等着、最终走向 ANR。这个设计的好处是:与其让调用方挂在那里不知道何时能返回,不如快速失败、让调用方有机会做错误处理。

Lazy Async(Android 14/15) 优化了异步事务(Oneway)的派发策略。在之前的版本中,系统一收到 Oneway 请求就会立刻尝试唤醒目标线程去处理,这在短时间内大量 Oneway 涌入时会造成”唤醒风暴”,带来不必要的功耗开销。Lazy Async 的做法是根据系统负载情况”攒一攒”再派发,让 Oneway 的处理节奏更加平滑。在 Perfetto 里,你会发现开启这个特性后,Oneway 队列长度的波动更小,服务端线程的唤醒频率也更加均匀。

Binder Heavy Hitter Watcher 是系统层面的一个监控机制,会自动检测那些过度使用 Binder 的”问题进程”。如果某个进程在短时间内发起了过多的 Binder 调用,系统会在 logcat 中打印警告信息。这对于发现潜在的性能问题很有帮助——如果你在日志里看到自己的应用被点名,就该检查一下是不是哪里的 IPC 调用太频繁了。

给开发者的一些建议

关于 Oneway 的使用,需要特别谨慎。Oneway 调用确实”看起来”更快(因为发完就返回、不用等结果),但它并不适用于所有场景。只有在你”明确不关心返回结果,也不需要知道操作何时完成”的情况下才应该使用 Oneway,比如日志上报、状态通知这类”发了就行”的场景。如果你把本该同步的调用改成 Oneway 只是为了”让 UI 线程更快返回”,可能会引入难以调试的时序问题——因为你无法确定服务端什么时候真正处理完,而且 Oneway 请求是在服务端排队串行处理的,大量 Oneway 可能会互相影响。

关于 传输大数据,一定要避免通过 Binder 直接传输大对象(尤其是 Bitmap)。前面提到过,Binder 每个进程的共享缓冲区只有约 1MB,传一张稍大的图片就可能把缓冲区撑爆,触发 TransactionTooLargeException。正确的做法是使用 SharedMemory(底层通常基于 ashmem 或 memfd,可以在进程间高效共享大块内存)、通过文件传递,或者使用 ParcelFileDescriptor 传递文件描述符让对方自己读取。

关于 主线程的 Binder 调用,基本原则是:不要在主线程调用那些你无法预估耗时的 Binder 服务。很多系统服务的响应时间是不确定的,它可能依赖网络、IO、甚至其他进程的状态。一旦对方卡住了,你的主线程就会跟着卡住,几秒钟之后就是 ANR。如果必须调用这类服务,应该放到后台线程去做,拿到结果后再切回主线程更新 UI。

总结

Perfetto 是分析 Binder 问题的重要工具。通过本文,你应该对以下几个方面有了基本的了解:如何配置 ftraceandroid.binder 数据源来抓取 Binder 相关的事件;如何在 Perfetto UI 中利用 Flow 箭头把 Client 的请求和 Server 的处理串联起来,形成完整的调用链路;以及如何通过观察 latency_nsserver_latency_ns、线程状态、锁竞争等信息,区分”排队慢”(线程池饱和)、”处理慢”(服务端代码耗时)和”等锁”(锁竞争)这几种常见的性能瓶颈。

在实际开发中,如果你遇到了难以解释的 UI 卡顿或 ANR,不妨用 Perfetto 抓一份 Trace 来看看:主线程是不是在等 Binder 调用返回?如果是,服务端在干什么?是在排队等线程、是在执行业务逻辑、还是在等锁?顺着这条思路一步步往下查,往往能找到问题的根源。当然,Binder 分析只是 Perfetto 功能的一部分,结合前面几篇文章介绍的 CPU、调度、渲染等方面的知识,你可以更全面地理解系统的运行状态,定位各种性能问题。

参考

  1. 理解Android Binder机制1/3:驱动篇
  2. Perfetto Documentation - Android Binder
  3. Perfetto Documentation - Ftrace
  4. Android Source - Binder
  5. Android Developers - Parcel and Bundle
  6. binder-trace - Wireshark for Binder
  7. am trace-ipc 源码分析

附件

关于我 && 博客

  1. 博主个人介绍
  2. 本博客内容导航
  3. Android性能优化知识星球

一个人可以走的更快 , 一群人可以走的更远

微信扫一扫

CATALOG
  1. 1. 本文目录
  2. 2. Perfetto 系列文章
  3. 3. Binder 基础与案例
    1. 3.1. 从 App 开发者视角的案例
  4. 4. Perfetto 观测准备
    1. 4.1. 数据源与轨道总览
    2. 4.2. Trace Config 推荐
      1. 4.2.1. 配置项说明
    3. 4.3. 快速上手:3 步抓取与查看 Binder Trace
    4. 4.4. 其他 Binder 分析工具
      1. 4.4.1. am trace-ipc:Java 层 Binder 调用追踪
      2. 4.4.2. binder-trace:实时 Binder 消息解析
  5. 5. Binder 分析工作流
    1. 5.1. 步骤一:定位事务耗时
    2. 5.2. 步骤二:评估线程池与 Oneway 队列
      1. 5.2.1. system_server 的 Binder 线程池规模
      2. 5.2.2. Binder 线程数、缓冲区与“Binder 耗尽”
    3. 5.3. 步骤三:排查锁竞争
      1. 5.3.1. 使用 SQL 统计 system_server 中的锁竞争(可选)
  6. 6. 案例演练:窗口管理延迟
  7. 7. 案例演练:Binder 线程池饱和
  8. 8. 最新平台特性与优化建议
  9. 9. 总结
  10. 10. 参考
  11. 11. 附件
  12. 12. 关于我 && 博客