从一次 ANR 问题深入理解 Binder 通信机制

0 阅读7分钟

从一次 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 的优势:

  1. 一次拷贝:通过内存映射(mmap)实现
  2. 安全性:内核自动添加 UID/PID,服务端可验证权限
  3. 面向对象:支持引用计数、死亡通知

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 的启动流程
  • 如何实现自定义系统服务

参考资料


欢迎交流讨论,如果文章对你有帮助,请点赞支持!