ANR产生原理
ANR(Application Not Responding)是 Android 对 "应用主线程卡死" 的系统级保护机制 :
当 输入事件、广播、服务 等在规定时间内未被处理完毕,SystemServer 会弹框并杀进程,防止整个系统跟着假死。
计时起点:事件离开 SystemServer 的瞬间
// InputDispatcher.cpp
void InputDispatcher::startDispatchCycleLocked(...) {
// 主线程 5s 超时(默认)
timeout = AMS::getInputDispatchingTimeout(timeout);
mLooper->sendMessageDelayed(timeout, ...);
}
-
按键/触摸:5s
-
前台广播:10s
-
后台广播:60s
-
前台服务:20s
-
后台服务:200s
计时终点:App 主线程处理完事件并回执
App 主线程在 ViewRootImpl$WindowInputEventReceiver
里消费完事件后,会调用:
// ViewRootImpl.java
final class WindowInputEventReceiver extends InputEventReceiver {
public void onInputEvent(InputEvent event) {
... // 派发、measure、layout、draw
finishInputEvent(event, true); // ← 通知系统"我干完了"
}
}
通过 Binder IPC 回到 InputDispatcher,取消第 1 步的超时任务。
超时未回执 → InputDispatcher 直接发SIGQUIT给 App
若第 2 步没在规定时间内到达,native 层会:
// InputDispatcher.cpp
void InputDispatcher::onDispatchTimeoutLocked(...) {
pid_t pid = connection->getPid();
kill(pid, SIGQUIT); // ① 发送 SIGQUIT(3)
// 同时把超时信息写入 dropbox
}
-
SIGQUIT 默认行为是终止进程(可被信号处理器捕获)。
-
系统利用这个信号触发 Dalvik/ART dump trace ,生成 /data/anr/traces.txt。
SystemServer 收到"超时报告"→ 弹 ANR 对话框 → 杀进程
// ActivityManagerService.java
void inputDispatchingTimedOut(int pid, boolean aboveSystem, String reason) {
synchronized (this) {
ProcessRecord proc = findProcessLocked(pid);
if (proc == null) return;
// ④ 弹出系统对话框
mUiHandler.post(() -> showAnrDialog(proc, reason));
// ⑤ 10s 后若用户未选择"等待"则杀进程
mHandler.sendMessageDelayed(MSG_KILL_ANR, proc, 10_000);
}
}
-
用户点 "关闭应用" → 立即
killProcessQuiet(pid)
-
用户点 "等待" → 取消杀进程,但计时器重新启动(再卡一次就再来 5s)
场景 | 超时 | 信号 | 最终动作 |
---|---|---|---|
主线程死循环 | 5s | SIGQUIT | 弹框 + 杀进程 |
onCreate() 超 10s | 10s | 广播超时 | 同上 |
后台服务 200s | 200s | 服务超时 | 同上 |
一句话背板
"事件离开系统时开始倒计时,主线程处理完回执取消;超时未回 → InputDispatcher 发 SIGQUIT,AMS 弹框并 10s 后杀进程;主线程别做长活,用子线程/Handler 异步。"
ANR Log分析-binder
ANR 日志
----- pid 1887 at 2010-06-23 00:20:50.043623522+0800 -----
Cmd line: com.android.systemui
Build fingerprint: 'chery/cheryidcpro/cheryidcpro:14/UQ1A.240205.002/eng.autoli.20250416.022302:userdebug/dev-keys'
ABI: 'arm64'
Build type: optimized
suspend all histogram: Sum: 41.458ms 99% C.I. 1us-1736.960us Avg: 73.768us Max: 5367us
DALVIK THREADS (133):
"main" prio=5 tid=1 Runnable
| group="main" sCount=0 ucsCount=0 flags=0 obj=0x72c5fd28 self=0xb400007c496897b0
| sysTid=1887 nice=-10 cgrp=top-app sched=0/0 handle=0x7e022f84f8
| state=R schedstat=( 723564350007 288749055735 4147215 ) utm=44375 stm=27981 core=3 HZ=100
| stack=0x7fd2ed3000-0x7fd2ed5000 stackSize=8188KB
| held mutexes= "mutator lock"(shared held)
native: #00 pc 0055be4c /apex/com.android.art/lib64/libart.so (art::DumpNativeStack+172) (BuildId: 55b9536b61b755b7bae82e95aa498dab)
native: #01 pc 006773a8 /apex/com.android.art/lib64/libart.so (art::Thread::DumpStack const+340) (BuildId: 55b9536b61b755b7bae82e95aa498dab)
native: #02 pc 00694f38 /apex/com.android.art/lib64/libart.so (art::DumpCheckpoint::Run+916) (BuildId: 55b9536b61b755b7bae82e95aa498dab)
native: #03 pc 006782d4 /apex/com.android.art/lib64/libart.so (art::Thread::RunCheckpointFunction+184) (BuildId: 55b9536b61b755b7bae82e95aa498dab)
native: #04 pc 00729b28 /apex/com.android.art/lib64/libart.so (artJniMethodStart+88) (BuildId: 55b9536b61b755b7bae82e95aa498dab)
native: #05 pc 0020f8f8 /apex/com.android.art/lib64/libart.so (art_jni_method_start+40) (BuildId: 55b9536b61b755b7bae82e95aa498dab)
at android.os.Parcel.nativeFreeBuffer(Native method)
at android.os.Parcel.freeBuffer(Parcel.java:5249)
at android.os.Parcel.recycle(Parcel.java:577)
at android.bluetooth.IBluetooth$Stub$Proxy.getRemoteName(IBluetooth.java:2624)
at android.bluetooth.BluetoothDevice.getName(BluetoothDevice.java:1751)
at android.bluetooth.BluetoothDevice.getAlias(BluetoothDevice.java:1818)
at com.android.settingslib.bluetooth.CachedBluetoothDevice.getName(CachedBluetoothDevice.java:599)
at android.app.LoadedApk$ReceiverDispatcher$Args.lambda$getRunnable$0(LoadedApk.java:1811)
at android.app.LoadedApk$ReceiverDispatcher$Args.$r8$lambda$gDuJqgxY6Zb-ifyeubKeivTLAwk(unavailable:0)
at android.app.LoadedApk$ReceiverDispatcher$Args$$ExternalSyntheticLambda0.run(unavailable:2)
at android.os.Handler.handleCallback(Handler.java:958)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8248)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:981)
上面这段 ANR trace 堪称「教科书级」的 主线程-Binder 被拖死 现场,把整条证据链完整地摆在了我们面前
进程快照
----- pid 1887 at 2010-06-23 00:20:50+0800 -----
Cmd line: com.android.systemui
-
发生 ANR 的正是 SystemUI(状态栏、导航栏、蓝牙弹窗都由它负责)。
-
时间戳虽然是 2010,这是编译时写死的 mock 时间,不影响分析。
主线程状态快照
"main" prio=5 tid=1 Runnable
state=R schedstat=( 723564350007 288749055735 4147215 )
held mutexes= "mutator lock"(shared held)
-
state=R
表示 正在 CPU 上跑,不是传统「阻塞」。 -
但 utm+stm=72 s 已吃掉 72 s CPU 时间 ,说明 死循环 / 长时间原地打转。
-
还抱着 mutator lock ,GC 想停都停不下来,整个进程世界都被它拖住。
调用栈快照(关键帧)
at android.os.Parcel.nativeFreeBuffer(Native method)
at android.os.Parcel.recycle(Parcel.java:577)
at android.bluetooth.IBluetooth$Stub$Proxy.getRemoteName(IBluetooth.java:2624)
at android.bluetooth.BluetoothDevice.getName(BluetoothDevice.java:1751)
at android.bluetooth.BluetoothDevice.getAlias(BluetoothDevice.java:1818)
at com.android.settingslib.bluetooth.CachedBluetoothDevice.getName(CachedBluetoothDevice.java:599)
at com.autolink.systemui.plugin.status.view.BlueToothPopView.getData(BlueToothPopView.java:316)
-
最顶层 正在 JNI 释放 Parcel 内存 (
nativeFreeBuffer
)。 -
该 Parcel 是 刚走完 Binder 调用
getRemoteName()
的返回包。 -
意味着:
BluetoothDevice.getName() → Binder → bluetooth@service → 等待回复 → 一直等到现在。 -
主线程 同步等结果 ,结果 remote 端不返回 ,于是 卡在释放返回包 这一步。
业务入口快照
-
触发点在 蓝牙状态广播 (
BluetoothAdapter.ACTION_STATE_CHANGED
或ACL_CONNECTED
之类)。 -
收到广播后 直接在主线程 遍历已配对设备并 依次 getName()/getAlias()。
-
如果 远端设备无响应 (耳机/车机断电、固件 bug、驱动卡死),Binder 就会阻塞,主线程同步被拖住。
ANR Log分析-input
Subject: Input dispatching timed out (9b06661 NavigationBar0 (server) is not responding. Waited 5001ms for MotionEvent(deviceId=-1, eventTime=105593089000000, source=TOUCHSCREEN, displayId=0, action=DOWN, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, classification=NONE, edgeFlags=0x00000000, xPrecision=1.0, yPrecision=1.0, xCursorPosition=nan, yCursorPosition=nan, pointers=[0: (1748.0, 1152.0)]), policyFlags=0x6b000000).
RssHwmKb: 578736
RssKb: 442240
RssAnonKb: 248084
RssShmemKb: 9312
VmSwapKb: 102988
--- CriticalEventLog ---
capacity: 20
timestamp_ms: 1746257995372
window_ms: 300000
----- dumping pid: 17251 at 105598116
----- pid 17251 at 2025-05-03 15:39:55.140104249+0800 -----
Cmd line: com.android.systemui
Build fingerprint: 'chery/cheryidcpro/cheryidcpro:14/UQ1A.240205.002/eng.autoli.20250430.025459:userdebug/dev-keys'
ABI: 'arm64'
Build type: optimized
suspend all histogram: Sum: 92.523ms 99% C.I. 2.660us-10708.479us Avg: 249.388us Max: 22989us
DALVIK THREADS (134):
"main" prio=5 tid=1 Runnable
| group="main" sCount=0 ucsCount=0 flags=0 obj=0x722c2f08 self=0xb400006ecd557380
| sysTid=17251 nice=0 cgrp=foreground sched=0/0 handle=0x70cfae14f8
| state=R schedstat=( 473063282543 2713319119711 2741705 ) utm=33543 stm=13763 core=0 HZ=100
| stack=0x7fc4435000-0x7fc4437000 stackSize=8188KB
| held mutexes= "mutator lock"(shared held)
native: #00 pc 0055be4c /apex/com.android.art/lib64/libart.so (art::DumpNativeStack+172) (BuildId: 55b9536b61b755b7bae82e95aa498dab)
native: #01 pc 006773a8 /apex/com.android.art/lib64/libart.so (art::Thread::DumpStack const+340) (BuildId: 55b9536b61b755b7bae82e95aa498dab)
native: #02 pc 00694f38 /apex/com.android.art/lib64/libart.so (art::DumpCheckpoint::Run+916) (BuildId: 55b9536b61b755b7bae82e95aa498dab)
native: #03 pc 006782d4 /apex/com.android.art/lib64/libart.so (art::Thread::RunCheckpointFunction+184) (BuildId: 55b9536b61b755b7bae82e95aa498dab)
native: #04 pc 0072a710 /apex/com.android.art/lib64/libart.so (artTestSuspendFromCode+52) (BuildId: 55b9536b61b755b7bae82e95aa498dab)
native: #05 pc 00226e1c /apex/com.android.art/lib64/libart.so (art_quick_test_suspend+156) (BuildId: 55b9536b61b755b7bae82e95aa498dab)
at java.lang.reflect.Executable.getMethodNameInternal(Native method)
at java.lang.reflect.Method.getName(Method.java:115)
at java.lang.reflect.Method$1.compare(Method.java:72)
at java.lang.reflect.Method$1.compare(Method.java:67)
at java.util.TimSort.gallopRight(TimSort.java:617)
at java.util.TimSort.mergeHi(TimSort.java:863)
at java.util.TimSort.mergeAt(TimSort.java:520)
at java.util.TimSort.mergeForceCollapse(TimSort.java:461)
at java.util.TimSort.sort(TimSort.java:254)
at java.util.Arrays.sort(Arrays.java:1344)
at java.util.ArrayList.sort(ArrayList.java:1738)
at java.util.Collections.sort(Collections.java:210)
at libcore.util.CollectionUtils.removeDuplicates(CollectionUtils.java:86)
at java.lang.Class.getMethods(Class.java:2226)
at android.os.Handler.handleCallback(Handler.java:958)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loopOnce(Looper.java:205)
at android.os.Looper.loop(Looper.java:294)
at android.app.ActivityThread.main(ActivityThread.java:8248)
at java.lang.reflect.Method.invoke(Native method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:552)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:981)
这段日志是 SystemUI 进程(com.android.systemui) 被系统判定为 输入ANR(Input Dispatching Timed Out) 的完整现场。
核心结论一句话:
主线程并没有死锁、也没有 Binder 阻塞,而是陷在「Class.getMethods() → 反射排序」里连续耗 CPU,导致 InputDispatcher 的 MotionEvent 5 秒没人消费,被系统报 ANR。
关键信息速览
项目 | 日志原文 | 解读 |
---|---|---|
ANR 类型 | Input dispatching timed out ... NavigationBar0 ... Waited 5001ms |
输入事件(手指按下)在 5 s 内未被 SystemUI 主线程消费 |
进程 & 线程 | pid=17251 tid=1 main |
就是 SystemUI 的主线程 |
线程状态 | state=R schedstat=(473063282543 ...) |
一直处于 Runnable ,CPU 在跑(不是 Block) |
内存 | RssKb: 442240 VmSwapKb: 102988 |
物理内存 442 MB,Swap 占用 100 MB → 轻度内存压力 ,但 不是 GC 停滞 |
栈顶 | TimSort.gallopRight/mergeHi → Arrays.sort → Class.getMethods |
正在对 Method[] 做排序去重 ,反射耗时 |
为什么会卡在这里
调用链
主线程消息 → Handler.handleCallback → ...
→ ReflectUtils.findMethod() → Class.getMethods()
→ CollectionUtils.removeDuplicates() → Collections.sort() → Arrays.sort()
→ TimSort.mergeHi/gallopRight
Class.getMethods()
会把 本类 + 所有父类的所有 public 方法 一次性取出,数量可达数百个 ;
然后 CollectionUtils.removeDuplicates()
按 方法名 + 参数类型 排序去重,最坏情况下 TimSort 比较器要执行上万次反射字符串比较。
主线程跑满 CPU
schedstat
里 utm=33543 stm=13763 (单位 10 ms),
仅本次采样就已经 累计 335 s 用户态 + 137 s 内核态 ,
说明主线程 一直在跑 ,但 就是没干完,InputDispatcher 的事件队列因此得不到空档期处理。
不是 GC、也不是 Binder
-
没有
SuspendAll
几百毫秒的记录(GC 阻塞典型特征)。 -
栈里无
IPCThreadState::waitForResponse
等 Binder 阻塞符号。 -
内存 Swap 虽有 100 MB,但 anon 部分只有 248 MB,远未到频繁 swap-in/out 的程度。
根因 | 修复动作 |
---|---|
主线程做重型反射 + 排序 | ① 把 Class.getMethods() 与排序 移到工作线程 ; ② 结果 (),皮肤切换只读缓存; ③ 若框架不可改,:Application 起就异步解析完常用 View 的方法; |
缓存 key 建议 | 以 (类, 方法名, 参数类型数组) 做 key,避免每次拼接字符串比较; |
避免反射 | 长期用 接口/策略模式 替代「运行时找方法」; |
一句话总结
这不是死锁,也不是内存抖动,而是 SystemUI 主线程在「给反射方法排序」时 CPU 被吃光,输入事件 5 秒得不到处理,被系统判为输入 ANR;把反射+排序移出主线程并加缓存即可根治