从一次 ANR 问题深入理解 Binder 通信机制
前言
最近在做系统定制项目时,遇到一个诡异的 ANR 问题:应用在低端设备上频繁卡死,但高端设备完全正常。通过深入分析 Binder 通信机制,最终定位到是频繁的跨进程调用导致的性能瓶颈。这篇文章记录下整个排查过程和对 Binder 的一些思考。
问题现象
复现场景
项目需求是在 Launcher 中实时显示设备温度信息,每秒刷新一次。代码实现如下:
// Launcher 中的温度显示逻辑
private void updateTemperature() {
handler.postDelayed(() -> {
String temp = SystemProperties.get("persist.sys.cpu.temp");
temperatureView.setText(temp + "°C");
updateTemperature(); // 递归调用
}, 1000);
}
在 RK3288(4核 A17)设备上,使用一段时间后必现 ANR。
Traces 分析
通过 adb shell kill -3 <pid> 抓取 traces.txt,主线程堆栈如下:
"main" prio=5 tid=1 Native
| group="main" sCount=1 dsCount=0 flags=1 obj=0x74b96080 self=0x7f8c014c00
| sysTid=2845 nice=-10 cgrp=default sched=0/0 handle=0x7f9c8a49a8
| state=S schedstat=( 3250000000 1450000000 8920 ) utm=245 stm=80 core=2 HZ=100
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:503)
at android.os.IServiceManager$Stub$Proxy.getService(IServiceManager.java:175)
at android.os.SystemProperties.native_get(Native Method)
at android.os.SystemProperties.get(SystemProperties.java:95)
at com.android.launcher3.TemperatureMonitor.updateTemperature(TemperatureMonitor.java:45)
关键信息:
- 主线程状态是
Native,阻塞在 Binder 调用上 schedstat显示该线程已运行 3.25 秒,说明不是 CPU 饥饿- 调用链:
SystemProperties.get()→native_get()→ Binder 通信
Binder 通信机制回顾
为什么需要 Binder
Android 基于 Linux,进程间内存隔离。应用访问系统服务(如 ActivityManagerService)必须跨进程通信。传统 IPC 方案(管道、Socket、共享内存)存在问题:
- 管道/Socket:需要两次数据拷贝(用户空间 → 内核 → 用户空间)
- 共享内存:需要额外的同步机制,容易出错
- 安全性:难以验证调用方身份
Binder 的优势:
- 一次拷贝:通过内存映射(mmap)实现
- 安全性:内核自动添加 UID/PID,服务端可验证权限
- 面向对象:支持引用计数、死亡通知
Binder 通信流程
sequenceDiagram
participant App as 应用进程
participant Binder as Binder驱动
participant System as SystemServer
App->>Binder: ioctl(BINDER_WRITE_READ)
Note over App: 线程阻塞等待
Binder->>System: 唤醒Binder线程
System->>System: 执行服务方法
System->>Binder: 返回结果
Binder->>App: 唤醒调用线程
Note over App: 继续执行
关键点:
- 调用方线程会同步阻塞,直到服务端返回
- 如果服务端处理慢,调用方会一直等待
- Binder 驱动本身很快,瓶颈在服务端处理逻辑
源码分析:SystemProperties 的实现
Java 层
// frameworks/base/core/java/android/os/SystemProperties.java
public static String get(String key) {
if (TRACK_KEY_ACCESS) onKeyAccess(key);
return native_get(key);
}
private static native String native_get(String key);
每次调用都会进入 native 层。
Native 层
// frameworks/base/core/jni/android_os_SystemProperties.cpp
static jstring SystemProperties_getSS(JNIEnv *env, jobject clazz, jstring keyJ) {
const char* key = env->GetStringUTFChars(keyJ, nullptr);
std::string value = android::base::GetProperty(key, "");
env->ReleaseStringUTFChars(keyJ, key);
return env->NewStringUTF(value.c_str());
}
调用 GetProperty(),最终通过 socket 与 property_service 通信。
Property Service
// system/core/init/property_service.cpp
void property_service_thread() {
while (true) {
int s = accept4(property_set_fd, nullptr, nullptr, SOCK_CLOEXEC);
handle_property_set_fd(s);
}
}
property_service 运行在 init 进程,单线程处理所有属性请求。如果请求量大,会排队等待。
问题根因分析
使用 systrace 抓取性能数据
python systrace.py -t 10 sched freq idle am wm gfx view binder_driver -o trace.html
在 trace 中发现:
SystemProperties.get()平均耗时 15-30ms- 每秒调用 1 次,累积延迟不明显
- 但系统中有多个进程也在频繁读取属性,导致 property_service 繁忙
使用 strace 验证
adb shell strace -p <launcher_pid> -e trace=socket,connect,sendto,recvfrom
输出:
socket(AF_UNIX, SOCK_STREAM, 0) = 45
connect(45, {sa_family=AF_UNIX, sun_path="/dev/socket/property_service"}, 110) = 0
sendto(45, "persist.sys.cpu.temp", 20, 0, NULL, 0) = 20
recvfrom(45, "45", 2, 0, NULL, NULL) = 2 // 耗时 25ms
每次调用都要建立连接、发送请求、等待响应,在低端设备上延迟明显。
解决方案
方案一:本地缓存
public class TemperatureMonitor {
private String cachedTemp = "";
private long lastUpdateTime = 0;
private static final long CACHE_DURATION = 5000; // 5秒缓存
private String getTemperature() {
long now = SystemClock.elapsedRealtime();
if (now - lastUpdateTime > CACHE_DURATION) {
cachedTemp = SystemProperties.get("persist.sys.cpu.temp");
lastUpdateTime = now;
}
return cachedTemp;
}
}
效果:Binder 调用从每秒 1 次降低到每 5 秒 1 次,ANR 消失。
方案二:异步读取
private final HandlerThread workerThread = new HandlerThread("TempWorker");
private final Handler workerHandler;
public TemperatureMonitor() {
workerThread.start();
workerHandler = new Handler(workerThread.getLooper());
}
private void updateTemperature() {
workerHandler.post(() -> {
String temp = SystemProperties.get("persist.sys.cpu.temp");
mainHandler.post(() -> temperatureView.setText(temp + "°C"));
});
}
将 Binder 调用移到子线程,避免阻塞主线程。
方案三:Framework 层优化(系统级方案)
如果是系统应用,可以考虑在 Framework 层添加属性变化监听机制:
// frameworks/base/core/java/android/os/SystemProperties.java
public static void addChangeCallback(String key, Runnable callback) {
// 使用 inotify 监听属性变化,而不是轮询
}
这样只在属性真正变化时才触发回调,避免无效的 Binder 调用。
性能对比
| 方案 | 每秒 Binder 调用次数 | 主线程阻塞时间 | ANR 风险 |
|---|---|---|---|
| 原始方案 | 1 次 | 15-30ms | 高 |
| 本地缓存 | 0.2 次 | 3-6ms | 低 |
| 异步读取 | 1 次 | 0ms | 无 |
| Framework 优化 | 按需触发 | 0ms | 无 |
深入思考:Binder 的性能边界
Binder 调用的开销构成
通过多次测试,总结出 Binder 调用的耗时分布:
总耗时 = 用户态切换(5-10μs) + 内核处理(10-20μs) + 服务端处理(变量) + 数据拷贝(取决于大小)
在我们的案例中:
- 用户态/内核态切换:约 15μs
- property_service 处理:10-25ms(受系统负载影响)
- 数据拷贝:可忽略(只返回字符串)
瓶颈在服务端处理,而不是 Binder 机制本身。
Binder 线程池的限制
SystemServer 的 Binder 线程池配置:
// frameworks/native/libs/binder/ProcessState.cpp
#define DEFAULT_MAX_BINDER_THREADS 15
ProcessState::ProcessState(const char *driver)
: mDriverFD(open_driver(driver))
, mMaxThreads(DEFAULT_MAX_BINDER_THREADS)
{
// ...
}
只有 15 个 Binder 线程处理所有系统服务请求。如果:
- 多个应用同时调用系统服务
- 某个服务处理慢(如 I/O 操作)
- 低端设备 CPU 调度慢
就会导致 Binder 线程池耗尽,新的请求排队等待。
查看 Binder 状态的实用命令
# 查看进程的 Binder 线程状态
adb shell cat /sys/kernel/debug/binder/proc/<pid>
# 查看当前所有 Binder 事务
adb shell cat /sys/kernel/debug/binder/transactions
# 查看 Binder 统计信息
adb shell dumpsys binder_calls_stats
# 查看某个进程的 Binder 调用统计
adb shell dumpsys activity services | grep -A 20 "ServiceRecord"
实际输出示例:
proc 2845
context binder
thread 2845: l 00 need_return 0 tr 0
thread 2856: l 10 need_return 0 tr 0
thread 2857: l 11 need_return 0 tr 1
incoming transaction 12345: from 3456:3467 to 2845:2857
可以看到线程 2857 正在处理事务,如果长时间不返回就是性能问题。
总结与最佳实践
通过这次 ANR 问题的排查,对 Binder 有了更深的理解:
1. Binder 不是免费的午餐
虽然 Binder 比传统 IPC 高效,但每次调用仍有开销:
- 用户态/内核态切换
- 线程阻塞与唤醒
- 服务端处理时间
不要把 Binder 调用当成普通函数调用。
2. 主线程要避免同步 Binder 调用
主线程的任何阻塞都可能导致 ANR。能异步就异步,不能异步就加缓存。
3. 理解服务端的处理能力
Binder 线程池有限,服务端处理慢会影响所有调用方。设计系统服务时要考虑:
- 避免在 Binder 线程做耗时操作
- 使用异步处理 + 回调
- 限流保护
4. 善用工具定位问题
- traces.txt:看线程在哪里阻塞
- systrace:看整体性能瓶颈
- strace:看系统调用细节
- dumpsys:看 Binder 统计信息
5. 低端设备是试金石
高端设备性能好,很多问题被掩盖。低端设备会放大所有性能问题,是测试的重点。
后续计划
这次只是从应用层分析了 Binder 的性能问题,后续会继续深入:
- Binder 驱动的实现原理
- Binder 对象的生命周期管理
- ServiceManager 的启动流程
- 如何实现自定义系统服务
参考资料
- Android Binder 设计与实现
- AOSP 源码:
frameworks/native/libs/binder/ - 《深入理解 Android 内核设计思想》
欢迎交流讨论,如果文章对你有帮助,请点赞支持!