输入事件超时ANR:Touch事件分发超时、按键事件超时,典型log分析

讲完系统资源竞争引发的ANR,咱们来聊聊另一个高频场景——输入事件超时。这类ANR的特点是:用户点了屏幕或按了键盘,手机直接卡住,甚至弹出“应用无响应”。我敢说,大部分开发者都见过这类log,但很多人只看了眼“Input dispatching timed out”就慌了。

其实没那么玄乎。说白了,就是主线程在限定时间内没把事件处理完。系统给你画了一条红线:Touch事件5秒,按键事件5秒。超了,你就得面对那个对话框。

一、Input事件分发的完整链路

先看一张图,帮你把整个流程串起来。

输入事件分发与ANR触发流程 InputReader 读取原始事件 InputDispatcher 分发到目标窗口 App主线程 dispatchTouchEvent Timeout检测 5秒未返回→ 弹出ANR Dialog ★ InputDispatcher会为每个待分发事件设置一个Timeout闹钟。 ★ 当主线程的Looper.loop()在5秒内没有执行完dispatchTouchEvent, 系统就会判定超时,生成ANR。

事件从硬件到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行

怎么排查?我个人习惯是:

  1. 先看log中的堆栈,找到卡住的具体方法。
  2. 如果堆栈在View的onTouchEvent里,检查是不是做了文件读写、网络请求、Bitmap解码。
  3. 如果堆栈在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根本原因其实是死锁或锁竞争。

一个小窍门:如果你看到log里只有“Input dispatching timed out”但没有详细堆栈,可以快速检查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里加个超时重新请求焦点来缓解。