android 性能优化—ANR

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_CHANGEDACL_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 ...) 一直处于 RunnableCPU 在跑(不是 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

schedstatutm=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;把反射+排序移出主线程并加缓存即可根治

相关推荐
翻滚丷大头鱼3 小时前
android 性能优化—内存泄漏,内存溢出OOM
android·性能优化
拜无忧3 小时前
【教程】flutter常用知识点总结-针对小白
android·flutter·android studio
拜无忧4 小时前
【教程】Flutter 高性能项目架构创建指南:从入门到高性能架构
android·flutter·android studio
用户2018792831674 小时前
故事:公司的 "私人储物柜" 系统(ThreadLocalMap)
android·java
CYRUS_STUDIO4 小时前
如何防止 so 文件被轻松逆向?精准控制符号导出 + JNI 动态注册
android·c++·安全
yinmaisoft4 小时前
当低代码遇上AI,有趣,实在有趣
android·人工智能·低代码·开发工具·rxjava
Linux运维技术栈5 小时前
域名网页加载慢怎么解决:从测速到优化的全链路性能优化实战
运维·网络·nginx·性能优化·cloudflare
如此风景5 小时前
Compose Modifier 修饰符介绍
android
纽马约5 小时前
Android BaseQuickAdapter的使用
android