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、drawfinishInputEvent(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;把反射+排序移出主線程并加緩存即可根治