Android应用ANR无响应的排查方法(Input dispatching timed out定位、/data/anr堆栈抓取与主线程卡顿修复)

App点一下按钮半天没反应,隔几秒弹出"XXX无响应"的对话框,用户只能选择等待或者关闭。这时候logcat里对应的记录是这样:

E ActivityManager: ANR in com.example.app (com.example.app/.MainActivity)
E ActivityManager: PID: 21423
E ActivityManager: Reason: Input dispatching timed out (5005ms)
E ActivityManager: Load: 8.31 / 6.42 / 5.17
E ActivityManager: CPU usage from 0ms to 7015ms later:
E ActivityManager:   43% 21423/com.example.app: 38% user + 4.7% kernel / faults: 8211 minor
E ActivityManager:   12% 1000/system_server: 9.2% user + 3.1% kernel

Reason那一行的报错串决定了排查方向,先把这行抄下来。


四类ANR的超时阈值不一样

输入事件是5秒,用户点了屏幕而主线程5秒内没把事件消费掉就触发,这类用户在现场就能看见,处理优先级最高。广播接收器分前后台,Android 13及更低版本是前台10秒、后台60秒,Android 14起改成前台10到20秒、后台60到120秒,具体取值看进程是否已经把CPU配额耗尽。服务是前台20秒、后台200秒,指的是onCreate、onStartCommand这类生命周期回调没跑完。ContentProvider是10秒,多在publish阶段超时。判断广播属于哪一类,看ANR正文里intent的flg=有没有0x10000000这一位,那是FLAG_RECEIVER_FOREGROUND。

还有一类容易被当成应用问题的:无焦点窗口ANR。它其实属于输入调度ANR,只是Reason写的是Waiting because no window has focus,根因往往是应用启动太慢、首帧一直没画出来,或者窗口挂了FLAG_NOT_FOCUSABLE。


把traces文件拿到手

Android 8.0之后ANR堆栈不再统一写进/data/anr/traces.txt,而是按次生成单独的文件。高版本上/data/anr这个目录没root权限根本读不出来,实测过一批真机,Android 11之后adb pull基本都是Permission denied。这时候走bugreport,它把ANR堆栈和系统日志一起打包:

adb bugreport anr_report.zip
# 等它跑完,解压后看 FS/data/anr/ 目录
adb shell pidof com.example.app
adb shell kill -3 21423

如果设备能root,adb shell ls -l /data/anr再adb pull /data/anr/最省事。不想等ANR自然发生,可以用kill -3主动给进程发SIGQUIT,ART会立刻dump一份堆栈,本地复现时很好用。另外adb shell dumpsys dropbox --print data_app_anr能直接把历史ANR记录打出来,不用等下一次复现。

顺手确认一下platform-tools版本,adb 34.0.5、35.0.0、35.0.1、35.0.2、36.0.0这几个版本对无线调试和mDNS的处理差别不小,2023年到2025年间无线配对失败却返回成功码这类问题就是在34.0.5前后修掉的,太老的adb在Android 14以上会出现设备连不上。


traces里先看这三处

第一处是main线程的状态。Blocked说明在等锁,堆栈里会跟着一行waiting to lock <0x0...> (a java.lang.Object) held by thread 21,拿着这个tid去文件里搜thread 21,能顺藤摸瓜找到占着锁不放在干什么。Waiting和TIMED_WAITING是主动等待,Native加nativePollOnce表示主线程其实闲着在等下一条消息,这种堆栈基本没有参考价值。Runnable反而最麻烦,说明主线程一直在跑,得看它正在执行什么。

第二处是CPU usage段。iowait占比高说明IO在抢资源,total接近100%说明整机CPU被吃光,这时候锅可能不在本应用,而是在别的进程。之前遇到过一次ANR,本应用主线程只占7%,真正的元凶是同一台机器上另一个进程把八个核全部占满。

第三处是faults数字。minor fault数量异常大(几万级)通常是内存紧张、频繁换页,配合logcat里lmkd的杀进程记录一起看。


堆栈经常不是真正的元凶

系统判定的是事件从进队列到被处理完的总时长。假设队列里先排了一条耗时4.8秒的消息B,输入事件排在它后面,5秒超时一到ANR触发,此时抓到的堆栈可能是B刚跑完、正在处理的下一条消息。这类堆栈看着人畜无害,按它去改代码改到死也改不出来。

破解办法是给主线程每个消息记耗时,Looper自带这个口子:

Looper.getMainLooper().setMessageLogging(msg -> {
    if (msg.startsWith(">>>>> Dispatching")) {
        start = SystemClock.uptimeMillis();
    } else if (msg.startsWith("<<<<< Finished")) {
        long cost = SystemClock.uptimeMillis() - start;
        if (cost > 300) Log.w("SlowMsg", cost + "ms " + msg);
    }
});

有了慢消息记录,就能把ANR发生前5秒主线程到底在干什么拼出来。实测在几台低端机上这个埋点本身开销可以忽略,只在线上开也行。


主线程上真正拖时间的那些操作

数据库查询排第一位,尤其是没走Room而在主线程直接rawQuery的大表扫描;Room本身在主线程查询会直接抛异常,反而更早暴露。SharedPreferences要单独拎出来说,getSharedPreferences首次加载会把整个xml读进内存,文件大就卡;apply()虽然是异步写盘,但写入任务排队在QueuedWork里,Activity stop的时候系统会等这批任务跑完,攒多了照样触发ANR,用DataStore或者MMKV替掉能明显改善。

剩下常见的是Gson反序列化大JSON、Binder同步调用(比如在主线程查另一个进程的ContentProvider)、主线程和后台线程抢同一把锁、WebView首次初始化、布局层级过深导致inflate慢。见过一个页面把三个WebView放在启动时一起初始化,冷启动直接顶到4秒多,改成懒加载之后ANR率掉了一大截。


开发期就把卡点逼出来

StrictMode开线程策略最省事,磁盘读写和网络访问一旦落在主线程,logcat立刻报出来:

StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
    .detectDiskReads()
    .detectDiskWrites()
    .detectNetwork()
    .penaltyLog()
    .build());

penaltyDeath在debug包里可以直接崩,逼着改。掉帧用Choreographer.FrameCallback统计两帧间隔,超过16.67毫秒就记一笔。想看系统侧的原因(system_server锁竞争、binder对端慢),用Perfetto抓trace比看traces文件直观得多。


线上靠集群签名而不是单条堆栈

Google Play管理中心的Android Vitals和Firebase Crashlytics会把ANR按堆栈聚成签名,排在最前的那个集群才最可能接近根因。签名里显示nativePollOnce或者主线程空闲的集群可以直接忽略,那是堆栈dump得太晚造成的噪声,真实问题在别的集群里。广播类ANR不要忽略这类签名,因为接收器可能跑在非主线程,得去堆栈里找对应的BG Thread。

数据截至2026年10月,超时阈值参考AOSP默认值,各厂商定制ROM会有出入;platform-tools版本参考34.0.5(2023年10月)、35.0.2(2024年7月)、36.0.0(2025年4月)。