记一次诡异的 ANR 问题排查:主线程明明没干活,为啥还超时?

记一次诡异的 ANR 问题排查:主线程明明没干活,为啥还超时?

背景

上周测试那边提了个 bug,说 Launcher 用着用着就卡死了,必现。我一开始还不信,心想 Launcher 就显示个图标列表,能有啥问题?结果自己一试,还真是,点击应用图标后大概 3-4 秒就弹 ANR 了。

项目是基于 RK3288 做的定制 Launcher,需求是在桌面实时显示 CPU 温度。代码很简单:

java 复制代码
private void startMonitoring() {
    mHandler.postDelayed(new Runnable() {
        @Override
        public void run() {
            String temp = SystemProperties.get("persist.sys.cpu.temp");
            mTempView.setText(temp + "°C");
            mHandler.postDelayed(this, 1000);  // 每秒刷新
        }
    }, 1000);
}

看着没啥问题啊,就是读个属性,更新个 TextView,咋就 ANR 了呢?

第一步:看 traces

ANR 发生后,第一反应就是拉 traces 文件:

bash 复制代码
adb pull /data/anr/traces.txt

打开一看,主线程堆栈是这样的:

lua 复制代码
"main" prio=5 tid=1 Native
  at android.os.BinderProxy.transactNative(Native Method)
  at android.os.BinderProxy.transact(BinderProxy.java:503)
  at android.os.SystemProperties.native_get(Native Method)
  at android.os.SystemProperties.get(SystemProperties.java:95)

卡在 SystemProperties.get() 上了。我当时第一反应是:这不就是读个系统属性吗,能有多慢?

第二步:加日志验证

为了确认是不是真的慢,我加了点日志:

java 复制代码
long start = SystemClock.elapsedRealtime();
String temp = SystemProperties.get("persist.sys.cpu.temp");
long cost = SystemClock.elapsedRealtime() - start;
Log.d(TAG, "get property cost: " + cost + "ms");

跑起来一看,卧槽,每次调用居然要 20-30ms

我当时就懵了,读个属性要这么久?然后我又试了下高端设备(骁龙 865),发现只要 1-2ms。所以问题出在低端设备上。

第三步:翻源码找原因

我开始怀疑 SystemProperties.get() 的实现有问题。翻了下源码:

java 复制代码
// SystemProperties.java
public static String get(String key) {
    return native_get(key);
}

直接调 native 方法。继续看 native 层:

cpp 复制代码
// 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, "");
    // ...
}

最终发现,它是通过 socket 连接到 property_service 读取的。而 property_service 运行在 init 进程,单线程处理所有请求

这就解释通了:如果系统里有很多进程都在频繁读属性,property_service 就会排队,导致延迟变高。

第四步:用 strace 验证猜想

为了验证,我用 strace 抓了下系统调用:

bash 复制代码
adb shell strace -p <launcher_pid> -e trace=socket,connect,sendto,recvfrom -T

输出:

scss 复制代码
socket(AF_UNIX, SOCK_STREAM, 0) = 45 <0.000123>
connect(45, {sa_family=AF_UNIX, sun_path="/dev/socket/property_service"}, 110) = 0 <0.000089>
sendto(45, "persist.sys.cpu.temp\0", 21, 0, NULL, 0) = 21 <0.000056>
recvfrom(45, "45\0", 92, 0, NULL, NULL) = 3 <0.024531>  // 卡在这里 24ms

果然,recvfrom 等了 24ms。说明 property_service 处理慢了。

解决方案:加缓存

既然每次读都要跨进程通信,那就加个缓存呗:

java 复制代码
private String mCachedTemp = "";
private long mLastUpdateTime = 0;
private static final long CACHE_INTERVAL = 5000; // 5秒缓存

private void updateTemperature() {
    long now = SystemClock.elapsedRealtime();
    if (now - mLastUpdateTime > CACHE_INTERVAL) {
        mCachedTemp = SystemProperties.get("persist.sys.cpu.temp");
        mLastUpdateTime = now;
    }
    mTempView.setText(mCachedTemp + "°C");
    mHandler.postDelayed(this::updateTemperature, 1000);
}

改完后测试,ANR 消失了。Binder 调用从每秒 1 次降到每 5 秒 1 次。

踩坑总结

这次排查让我明白了几个事:

1. 不要小看"简单"的系统调用

SystemProperties.get() 看着简单,但它涉及跨进程通信。在低端设备上,这种开销会被放大。

2. 高端设备测试会掩盖问题

我一开始在自己手机(旗舰机)上测,完全没问题。结果到客户的低端设备上就炸了。性能测试一定要在目标设备上做。

3. 频繁调用会累积延迟

单次 20ms 看着不多,但每秒调用 1 次,再加上其他进程也在读属性,就会导致 property_service 繁忙,延迟进一步增加,最终触发 ANR。

4. 主线程要避免任何阻塞操作

哪怕是"看起来很快"的操作,也可能在某些情况下变慢。能缓存就缓存,能异步就异步。

延伸思考

后来我又想了下,为什么 property_service 要设计成单线程?查了下资料,发现是为了保证属性读写的一致性。如果多线程处理,就需要加锁,反而可能更慢。

另外,Android 11 之后引入了 property 的 mmap 机制,读取性能有所提升。但我们项目基于 Android 10,享受不到这个优化。

调试技巧分享

这次排查用到的几个命令,记录下:

bash 复制代码
# 实时查看 ANR 日志
adb logcat | grep "ANR in"

# 抓取 traces
adb pull /data/anr/traces.txt

# 查看系统调用耗时
adb shell strace -p <pid> -T -e trace=all

# 查看 Binder 调用统计
adb shell dumpsys binder_calls_stats

最后

这个问题看起来简单,但排查过程还是挺有意思的。从表象到本质,一步步深入,最终找到根因。

如果你也遇到类似的 ANR 问题,不妨试试:

  1. 看 traces,确认主线程在干啥
  2. 加日志,量化耗时
  3. 用 strace 看系统调用
  4. 翻源码,理解机制

Framework 开发就是这样,表面简单的 API 背后,可能藏着复杂的实现。


有问题欢迎评论区讨论,点赞收藏支持一下!

相关推荐
014-code2 小时前
Spring 事务原理深度解析
java·数据库·spring·oracle
毕设源码-钟学长2 小时前
【开题答辩全过程】以 基于SpringBoot的健康系统为例,包含答辩的问题和答案
java·spring boot·后端
曹牧2 小时前
@RequestBody 注解处理的数据类型
java
慧都小项2 小时前
Java开发工具MyEclipse发布v2026.1:支持Java25和Spring Boot4、AI功能升级
java·spring boot·myeclipse
L0CK2 小时前
实战篇 01. 达人探店 - 发布探店笔记学习文档
java
独断万古他化2 小时前
【抽奖系统开发实战】Spring Boot 项目的用户模块设计:注册登录、权限管控与敏感数据加密
java·spring boot·redis·后端·mvc·jwt·拦截器
一直学习的程序小白2 小时前
java进阶-优化GC垃圾回收机制
java·开发语言·jvm
安卓程序员_谢伟光2 小时前
如何用MAT(Eclipse Memory Analyzer)
java·ide·eclipse
喵喵蒻葉睦2 小时前
力扣 hot100 和为K的子数组 哈希&前缀和
java·数据结构·算法·leetcode·前缀和·哈希算法