输入事件超时ANR:触摸事件分发与5秒超时
触摸事件超时,是日常开发中遇到最多的一类ANR。说白了,就是用户点了屏幕,系统等了5秒,应用却没把事件处理完。这类ANR的日志特征非常明显,但很多人看到“Input dispatching timed out”就慌了。别急,今天我把它的底层原理和排查方法说透。
触摸事件从屏幕到应用的完整旅程
你每次点一下屏幕,背后都走了一条固定的路径。我画了个简单的流程图,先帮你理解整体脉络:
嗯,这张图把关键环节都标出来了。下面我拆开讲每个环节,以及5秒超时到底是怎么算的。
第一步:系统层怎么收到触摸事件的?
触摸事件首先由硬件产生,通过驱动上报给Linux内核,然后由InputReader线程读取。我习惯把InputReader看作“快递员”——他把原始的屏幕触摸信号打包成系统能识别的MotionEvent。
关键点在这里:从硬件触发到InputReader读完,这一步基本不会超时,因为它发生在内核态,很快。真正的超时陷阱在后面。
第二步:InputDispatcher分发——5秒倒计时开始
InputDispatcher拿到事件后,会做几件事:
- 找到当前焦点窗口(谁在顶层)
- 把事件序列化后通过Socket发送到目标应用的UI线程
- 启动一个5秒的超时定时器
为什么是5秒?我查过源码,这个时间是在InputDispatcher.cpp里硬编码的,对应ANR_TIMEOUT_DURATION常量。注意,系统并不是等到5秒满了才处理,而是每个事件单独计时。如果5秒内应用发回“finished”信号,定时器取消;否则系统认为发生了ANR。
我个人习惯:排查这类ANR时,先看log中是否出现Input dispatching timed out。如果出现,后面会紧跟着一个Waiting because no window have focus或Window is in touch mode等信息。这些能帮你快速定位是焦点问题还是线程阻塞。
第三步:事件到达UI线程后发生了什么?
事件通过Socket到达应用进程,被InputEventReceiver接收,然后抛到ViewRootImpl的deliverInputEvent()方法里。接下来经过View的分发链条:
- Activity.dispatchTouchEvent() — 先走Activity
- PhoneWindow / DecorView — 再走Window
- ViewGroup.dispatchTouchEvent() — 递归分发
- 目标View.onTouchEvent() — 最终消费
整个链条必须在UI线程执行。一旦UI线程被某个耗时操作(比如网络请求、数据库读写、Layout布局)堵住了,事件就卡在起点,5秒倒计时走完,ANR就来了。
日志特征:如何一眼判断是输入事件超时?
日志里最常见的ANR关键词是ANR in com.xxx,但输入事件型ANR有专属特征。我整理了一个对比表:
| 日志片段 | 含义 | 常见场景 |
|---|---|---|
Input dispatching timed out |
系统等待应用处理输入事件超时 | UI线程被阻塞 |
Waiting because no window have focus |
焦点窗口丢失,事件分发不下去 | Dialog弹出或窗口切换时序问题 |
Reason: Waiting to send key event |
按键事件排队等待(通常伴随触摸超时) | 动画/布局卡死导致事件积压 |
Choreographer was sleeping longer |
主线程刷帧延迟(超时伴随现象) | 同一线程的其他耗时任务拖累 |
拿到traces.txt文件后,你可以直接搜索"main" prio=5查看UI线程堆栈。如果堆栈停在InputEventReceiver.nativeConsumeBatchedInputEvents或者View.dispatchTouchEvent附近,并且下面的业务代码在慢操作(比如Glide加载一张大图、RxJava的subscribeOn阻塞),基本就石锤了。
曾经踩过的一个坑
我曾经在一个短视频App中遇到过:用户快速滑动Feed时偶尔ANR,日志显示Input dispatching timed out (Waiting because no window have focus)。排查了好久,最后发现是RecyclerView的item动画在滑动过程中销毁了有焦点的子View,导致窗口焦点丢失。系统找不到焦点窗口,事件卡住了。
避坑指南:如果你看到“Waiting because no window have focus”,别急着怀疑性能问题,先检查窗口焦点管理。可以用adb shell dumpsys window windows查看当前焦点窗口状态。
5秒超时检测的底层原理
最后说一下系统是怎么精确计时的。InputDispatcher内部维护了一个mMonitor对象和一个定时器。具体流程:
- 每次分发事件时,记录当前时间戳到
mEventEntry->deliveryTime - 设置一个
mTimeoutDuration = 5000ms的Alarm - 如果应用在5秒内通过
finishInputEvent()通知完成,则取消Alarm - 5秒期满,Alarm回调触发
processAnrLocked(),生成ANR
注意,这个5秒不是从事件到达应用开始算的,而是从InputDispatcher开始分发瞬间就开始计时。这就意味着:如果应用进程本身卡在Binder调用上,或者系统服务端处理慢,也可能导致超时。
重要提示:超时时间的5秒是系统默认值,不同厂商可能会修改。小米和华为部分机型改成8秒甚至10秒。所以不能完全依赖5秒来判断。查看实际超时时间,可以抓取/system/framework/framework-res.apk中的config_restartWindowTimeout资源。
好了,本章的输入事件超时ANR原理就讲到这里。记住:它的根因绝大多数是UI线程被阻塞,窗口焦点问题是偶尔的“烟雾弹”。下回拿到Input dispatching timed out日志,先搜堆栈里最慢的那段代码。