输入事件超时ANR:Touch事件分发超时、按键事件超时,典型log分析
讲完系统资源竞争引发的ANR,咱们来聊聊另一个高频场景——输入事件超时。这类ANR的特点是:用户点了屏幕或按了键盘,手机直接卡住,甚至弹出“应用无响应”。我敢说,大部分开发者都见过这类log,但很多人只看了眼“Input dispatching timed out”就慌了。
其实没那么玄乎。说白了,就是主线程在限定时间内没把事件处理完。系统给你画了一条红线:Touch事件5秒,按键事件5秒。超了,你就得面对那个对话框。
一、Input事件分发的完整链路
先看一张图,帮你把整个流程串起来。
事件从硬件到APP,经过InputReader、InputDispatcher,最终落到主线程的dispatchTouchEvent/onKeyDown。系统在分发那一刻开始计时,如果主线程卡住5秒没回,系统直接判定超时。这就是你的ANR。
二、Touch事件分发超时
Touch事件是使用频率最高的输入事件。滑动、点击、长按,每个手势都映射为一串MotionEvent。问题往往出在某个View的onTouchEvent或onClick里做了重活。
onClick里直接加载了十几个动画帧并且解码。用户一点屏幕,手指还没抬起来,ANR就弹出来了。那时候我才意识到——任何可能超过16ms的操作都不该放在UI事件回调里。
典型log长什么样?来看一段真实日志:
I/InputDispatcher: Application is not responding: AppWindowToken{...} (ActivityRecord{...})
Input event: MotionEvent { action=ACTION_DOWN, x=540.0, y=1234.0 }
Current time: 2025-04-15 14:23:45.123
Event wait time: 5.87s
Reason: Waiting because the focused window has not finished processing the input event.
Focused window: Window{...} (com.example.app/com.example.app.MainActivity)
Owner pid: 12345
…
(堆栈局部)
at com.example.app.Widget.onTouchEvent(Widget.java:128)
at android.view.View.dispatchTouchEvent(View.java:14000)
at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2800)
注意这里的关键信息:
- Input event:明确告诉你是什么事件(ACTION_DOWN)
- Event wait time:等待了多久,超过5秒就炸
- 堆栈顶部:最后卡在哪个方法。上面的例子是
Widget.onTouchEvent第128行
怎么排查?我个人习惯是:
- 先看log中的堆栈,找到卡住的具体方法。
- 如果堆栈在View的onTouchEvent里,检查是不是做了文件读写、网络请求、Bitmap解码。
- 如果堆栈在InputDispatcher本身,说明系统排队阻塞——这种情况较少见,通常和系统服务有关。
三、按键事件超时
按键事件(KeyEvent)和Touch事件本质相同,不过按键场景更窄,主要集中在物理按键或输入法事件。我遇到过一些游戏场景,玩家疯狂按返回键,结果主线程卡在某个onKeyDown里做保存操作。
log示例:
I/InputDispatcher: Application is not responding: ...
Input event: KeyEvent { action=ACTION_DOWN, keyCode=KEYCODE_BACK }
Event wait time: 6.03s
Reason: Waiting because the focused window has not finished processing the input event.
…
at com.example.app.GameActivity.onKeyDown(GameActivity.java:45)
at android.app.Activity.dispatchKeyEvent(Activity.java:3600)
按键超时的特征:keyCode字段会标出具体按键。我的建议——看到KEYCODE_BACK或KEYCODE_HOME时,优先检查onBackPressed或onKeyDown中是否有数据库写入、SP提交、网络请求等同步操作。
四、更深入的log分析技巧
很多时候,系统只会给出一段log,堆栈可能不够完整。这时候我们要从traces.txt里找更多线索。ANR发生时系统会dump所有线程的堆栈到/data/anr/traces.txt,可以用adb pull出来。
举个例子,我曾在traces.txt里看到主线程卡在:
"main" prio=5 tid=1 Blocked
at com.example.app.MainActivity.onTouchEvent(MainActivity.java:67)
- waiting to lock <0x0a1b2c3d> (a java.lang.Object)
at com.example.app.DataSync.sync(DataSync.java:20)
- locked <0x0e4f5a6b> (a java.lang.Object)
看到没?主线程在等一把锁,而锁被其他线程持有着。这种情况比单纯的耗时操作更难查。你得顺着locked的哈希值找到持锁线程。很多Touch超时ANR根本原因其实是死锁或锁竞争。
adb shell dumpsys window中的InputMethod和FocusedWindow状态,确认窗口是否真的被其他弹窗阻断。
我把常见的输入事件超时原因整理成了表格,便于快速对照:
| 原因类别 | 典型场景 | log关键词 | 我的处理思路 |
|---|---|---|---|
| 主线程耗时计算 | onTouchEvent里做Bitmap缩放、加解密、大文件IO | 堆栈指向onTouchEvent/onKeyDown | 异步化,用Handler或协程 |
| 锁等待 | 主线程等待其他线程释放锁 | “waiting to lock” | 检查死锁,避免主线程持有锁 |
| 系统服务阻塞 | WindowManager、InputMethodService卡住 | “Waiting for ... service” | 重启应用或检查系统层 |
| 窗口焦点问题 | Dialog弹出未及时处理,输入被积压 | “Focus window is not ready” | 检查window flag设置 |
五、避坑指南与预防措施
这部分全是真金白银的教训。我曾经被线上一个Touch超时ANR搞到凌晨三点,后来发现只是一个TextView的onTouch里调用了SharedPreferences.commit()——commit()是同步写盘,在主线程执行可能耗时超过1秒,连续快速点击就崩了。
所以我现在给自己定了三条铁律:
- 所有回调回调里禁止任何IO操作,包括SP、数据库、文件、网络。
- 必须使用异步线程处理,比如HandlerThread、协程的Dispatchers.IO。
- 如果非要在主线程做轻量操作,用
View.postOnAnimation()来分摊到下一帧,而不是堵在事件处理里。
嗯,如果你看到ANR log里连续出现多个输入事件的超时,说明问题很稳定,大概率是某个View的onTouchEvent或onDraw里有死循环或无限递归。我曾经见过一个自定义View,在onTouch里又调了requestLayout,然后onLayout又触发onTouch——完美死锁。遇到这种情况,直接看traces.txt里的循环调用栈。
最后提醒一下:不要忽视“等待窗口”类型的ANR。有时候你的APP根本没做什么,但系统窗口服务挂了,也会导致输入事件超时。这类log里会看到Reason: Waiting for window '...' to be focused。这种通常是系统Bug,但APP可以在onResume里加个超时重新请求焦点来缓解。