记一次诡异的 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 问题,不妨试试:
- 看 traces,确认主线程在干啥
- 加日志,量化耗时
- 用 strace 看系统调用
- 翻源码,理解机制
Framework 开发就是这样,表面简单的 API 背后,可能藏着复杂的实现。
有问题欢迎评论区讨论,点赞收藏支持一下!