ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android输入子系统全解析:从内核到应用的事件分发与性能优化

Android输入子系统全解析:从内核到应用的事件分发与性能优化 Android 输入子系统这块我踩了不少坑也啃了不少源码。网上讲输入事件的文章很多但多数讲得太散要么只讲应用层分发要么只堆内核源码。这周刚好把一个触摸延迟和按键重复的疑难杂症定位到 InputReader 和 InputDispatcher 层面趁热把整个输入子系统从头到尾捋一遍。这篇文章定位是 Android Framework 层系统开发、应用层做性能优化的同学也适合准备 Framework 面试的兄弟。1. 输入子系统整体架构与核心设计思路很多初学者一提到输入子系统脑子里就是View.dispatchTouchEvent那一套其实那只是冰山一角。从物理手指按下到应用层的onTouchEvent中间隔着内核、Native 层、Framework 层、应用层四个大关卡。整条链路的核心设计思路是分层解耦 事件驱动 异步分发。1.1 从硬件到应用的四层模型第一层是内核层触摸屏驱动、按键驱动把硬件信号转成标准的 Linux input 事件通过/dev/input/eventX节点暴露给上层。这里有个很关键的点input 子系统在 Linux 内核里使用input_dev结构体管理设备通过input_event结构体汇报事件驱动只需要调用input_event()或input_report_key()上报即可内核会维护一个事件队列。第二层是 Native 层也是 Android 自己加的核心层。EventHub负责监听/dev/input/目录下的所有设备节点通过epoll机制等待事件。InputReader线程读取原始事件并进行加工解析InputDispatcher线程负责把加工后的输入事件分发到目标窗口。这两个线程都工作在system_server进程里。第三层是 Framework Java 层InputManagerService作为系统服务管理一切InputWindowManager负责管理窗口信息。应用进程通过ViewRootImpl与InputDispatcher建立InputChannel连接接收事件。第四层是应用层View体系的事件分发。WindowInputEventReceiver接收InputEvent后依次经过DecorView、ViewGroup、View的分发逻辑。这个分层设计的核心好处是每一层只需要关注自己的职责驱动不需要知道上层是谁在消费事件Framework 也不关心底层是 I2C 触摸屏还是 USB 键盘。对 FrameWork 层开发来说大多数问题只需要关注 Native 层和 Framework Java 层。1.2 输入链路中的关键线程模型整个输入子系统由两条活着的线程驱动InputReader 和 InputDispatcher。这两条线程是理解整个系统的一把钥匙。InputReader 线程是一个while(true)循环核心逻辑在loopOnce()方法里。它通过EventHub.getEvents()批量读取原始事件这些事件包括设备增删、按键按下、触摸坐标变化等。读取到事件后InputReader 根据事件类型调用不同的InputMapper进行处理比如KeyboardInputMapper处理按键、TouchInputMapper处理触摸。Mapper 的职责是把原始RawEvent解析成标准化的KeyEvent或MotionEvent比如处理按键去抖、处理触摸的坐标转换、压力值归一化等。InputDispatcher 线程则运行在独立的线程里它有两个关键队列mInboundQueue用于接收 InputReader 发过来的事件mOutboundQueue用于等待分发到应用的事件。InputDispatcher 会根据窗口的input region、touchable region、z-order等信息找到事件应该投递的窗口。它通过InputChannel向应用进程写入事件等待应用处理完并返回finished信号。这两个线程之间的连接是InputReader拿到事件后调用InputDispatcher.notifyMotion()或notifyKey()此时 InputDispatcher 会把事件插入mInboundQueue并唤醒自己。这套模型的精妙之处在于InputReader 只负责读和解析InputDispatcher 只负责派和等互不阻塞。一旦 InputReader 解析过慢会直接表现为触摸不跟手而 InputDispatcher 分发超时则表现为点击无响应或 ANR。2. EventHub输入事件的源头与读取机制前面说了模型现在深挖第一道关卡EventHub。它在 Native 层的frameworks/native/services/inputflinger/reader/EventHub.cpp里。2.1 设备节点的监听与扫描机制EventHub 的构造函数里会先建立inotify实例用来监听/dev/input/目录的变化。系统插拔输入设备时ueventd会在/dev/input/下创建设备节点inotify 会收到IN_CREATE/IN_DELETE事件。EventHub 收到这些事件后会调scanDevicesLocked()重新扫描目录动态加载新的设备。openDeviceLocked()是打开设备的入口这里做了几件非常重要的事情以只读方式open()设备节点拿到文件描述符 fd通过ioctl()获取设备的name、vendor、product、version、bus等基本信息调用ioctl(EVIOCGBIT)查询设备支持的事件类型和按键码比如EV_KEY、EV_ABS、EV_REL对 ABS 类型设备还要通过ioctl(EVIOCGABS)获取每个轴的最大值、最小值、分辨率、模糊值拿到设备能力后EventHub 会为该设备创建Device对象并把它添加到自己维护的mDevices表中。对键盘类设备还要读取/sys/class/input/eventX/device/下的 keylayout 文件用于把内核扫描码映射为 Android 的键码。这里有个常见坑某些国产触摸屏的固件上报的ABS_MT_TRACKING_ID不规范ioctl拿到的 max 值设置不对会导致InputReader解析坐标时越界表现就是触摸偶尔会飞点。2.2 getEvents() 的读取与 epoll 等待机制getEvents()是 EventHub 暴露给 InputReader 的核心方法签名大概是size_t getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize)。调用流程大致如下首先EventHub 检查mPendingEventItems队列里是否还有之前从 epoll 中取出但没处理完的事件如果有先处理这些。然后EventHub 会调用epoll_wait()等待事件。这里传入了timeoutMillis超时时间InputReader 传入的通常是 100ms 左右的间隔用于周期性的其他任务。epoll_wait()返回后EventHub 拿到了就绪的 fd 列表其中可能是设备节点可读也可能是 inotify 目录变化。之后遍历就绪的 fd对设备节点的 fd 调用read()读取struct input_event数据然后转换成RawEvent填充到 buffer 中。struct input_event的内核定义长这样struct input_event { struct timeval time; // 时间戳秒微秒 __u16 type; // 事件类型 EV_KEY/EV_ABS/EV_REL... __u16 code; // 具体的按键码/轴号 __s32 value; // 值按键按下为1抬起为0触摸坐标则是对应的数值 };时间戳在 InputReader 里会被转换成nsecs_t的eventTime这对判断事件顺序、计算触摸速度、判断长按都至关重要。2.3 设备热插拔与配置加载的坑EventHub 层有一个非常隐蔽的坑事件设备节点的打开时机和配置加载不一定是可靠的。如果应用层或者 Framework 读取设备节点过快ioctl(EVIOCGBIT)可能拿不到完整的能力位图。更常见的坑是某些 HID 设备插上后内核加载 HID 驱动是异步的openDeviceLocked执行时EVIOCGABS返回的触摸分辨率可能还是 0。此时InputReader的TouchInputMapper在后续sync时会拿 max/min 做归一化除零或者产生错误坐标keylayout 文件配置错误会导致按键 scan code 映射到错误的功能典型现象是音量键变成返回键。排查时确认/odm/usr/keylayout/和/system/usr/keylayout/下的映射表排查 EventHub 层问题推荐两个工具。一个是getevent这个命令可以实时监听内核上报的原始事件adb shell getevent -lt /dev/input/event2加-l参数会把 type 和 code 显示为字符串方便阅读。另一个是dumpsys input可以看到当前系统加载了哪些输入设备、每个设备的事件类型、键值表映射、配置信息。拿到这些信息基本能确定设备有没有被正确加载。3. InputReader原始事件的解析与标准化EventHub 读到的是内核级别的input_event这玩意对上层来说太原始了。InputReader 的职责就是把原始事件翻译成 Framework 层能理解的标准KeyEvent和MotionEvent。3.1 InputMapper 的职责与工作流程InputReader 里维护了一个mMappers列表每个设备按能力创建对应的 Mapper。比如设备支持EV_KEY创建KeyboardInputMapper设备支持EV_ABS且具有ABS_MT_POSITION_X等触摸轴创建TouchInputMapper设备支持EV_REL创建CursorInputMapper鼠标设备支持EV_SW创建SwitchInputMapper每个 Mapper 的process()方法会拿到 RawEvent然后分发给具体的处理函数。以TouchInputMapper为例触摸屏上报的事件流基于BTN_TOUCH和ABS_MT_*协议。老的 B 协议通过ABS_MT_POSITION_X等事件直接报坐标新的 MT 协议 B 则通过ABS_MT_SLOT区分多个触摸点最后通过SYN_REPORT同步上报。TouchInputMapper在sync时会做以下这些事解析 slot 中每个触摸点的坐标、压力、面积根据设备分辨率归一化坐标到屏幕坐标跟踪触摸点的trackingId判断抬手up还是新触点down把 raw 的触摸信息转换成MotionEvent包含DOWN/MOVE/UP/CANCELaction、指针坐标、压力、事件时间注意InputReader这一层不做窗口查找和分发只做标准化。3.2 按键消抖与重复事件处理KeyboardInputMapper处理按键时有个非常关键的机制消抖debounce。在物理键盘或触摸屏的按键处理中由于机械触点的物理特性按下和抬起时会产生瞬间的抖动表现为多次value1和value0交替的电平信号。内核层通常会通过input_set_poll或drivers/input/keyboard的debounce机制处理一部分但 EventHub 和 InputReader 也会做进一步过滤。KeyboardInputMapper内部维护了按键状态表。对于value1的按下事件它会先记录按下时间状态切换为PRESSED。如果在一个很短的窗口内又收到了value0它会认为这是抖动标记为 cancelled不生成 KeyEvent 的上层事件。对于长按当按下事件超过ViewConfiguration.getLongPressTimeout()默认 500ms后会在KeyEvent.getRepeatCount()从 0 变成 1这是通过repeat标志位实现的。调这个参数有个经验// 通过系统属性调整长按超时 Settings.System.putInt(getContentResolver(), Settings.System.LONG_PRESS_TIMEOUT, 400);如果设备有特殊需求比如遥控器要短按和长按做不同功能可以通过调整longPressTimeout和keyRepeatDelay默认 400ms来实现更灵敏的响应但这个参数要在系统中多次测试调太小容易造成误触。3.3 触摸坐标转换与屏幕旋转映射TouchInputMapper会把触摸坐标从设备原始坐标映射到屏幕逻辑坐标。这个映射需要考虑旋转方向。系统DisplayInfo里提供了rotation值0/90/180/270TouchInputMapper会根据设备方向和屏幕方向的组合把触点坐标旋转到正确的位置。代码里对应的是transform()方法通过矩阵变换完成坐标映射。对开发者来说最容易遇到的问题就是竖屏应用在横屏设备上触摸点位偏差 90 度。这类问题排查时先确认Surface.ROTATION_*和DisplayInfo的方向是否一致再看TouchInputMapper中mOrientation和mSurfaceOrientation的差值。实际工作中很多硬件设备的触摸屏轴线方向本来就反的需要在/vendor/usr/idc/下的.idc文件中配置touch.orientation 90 touch.delimiter 10 touch.gestureMode spots这一节的核心收获是InputReader 解决的是 事件是什么 的问题。如果上层收到的 KeyEvent 的 keyCode 不对MotionEvent 的坐标不对按压状态不对都要回到这一层排查。4. InputDispatcher事件路由与窗口分发InputReader 标准化完事件之后调用InputDispatcher::notifyKey()或notifyMotion()把事件交给 InputDispatcher。这里发生了一次线程切换从 InputReader 线程切换到 InputDispatcher 线程。4.1 findTouchedWindow 的窗口查找流程对触摸事件来说InputDispatcher 拿到事件后第一步就是找到该谁接收。它内部维护着一组窗口信息包括窗口的 frame位置和大小、touchableRegion可触摸区域、flags如FLAG_NOT_TOUCHABLE、FLAG_NOT_FOCUSABLE还有窗口所属的 displayId。每个窗口都通过InputChannel与应用进程关联。findTouchedWindowLocked()的核心是遍历所有可见窗口按 z-order 从高到低用 touchableRegion 判断事件坐标是否落在窗口内。命中后还要检查这个窗口是否是焦点窗口FLAG_WATCH_OUTSIDE_TOUCH除外决定事件是否继续分发给兄弟窗口。这里最重要的经验是如果一个窗口的 touchableRegion 和可见区域不一致就会产生“按钮能看到但怎么点都没反应”的诡异问题。这种问题通常不是 InputDispatcher 的 bug而是窗口把 touchableRegion 设置错了。排查时用adb shell dumpsys window windows | grep -A 100 Window #查看每个窗口的 frame 和 touchableRegion基本能定位。4.2 事件队列的调度逻辑InputDispatcher 的核心是两个队列mInboundQueue和mOutboundQueue。mInboundQueue存放刚从 InputReader 接收的事件InputDispatcher 主循环会尽可能快地处理这些事件完成窗口查找和目标窗口确定后将事件放到对应连接Connection的mOutboundQueue里同时向Connection里注册的InputChannel发送数据。还有一个mFrozenQueue状态需要提一下当目标窗口正在处理上一个事件还没返回finished时后续事件会被挂起或按策略丢弃对 MotionEvent 的 MOVE 事件合并处理。InputDispatcher 对MOVE事件有一个合并策略后一个 MOVE 事件到达时如果前一个 MOVE 还没处理完直接丢弃前一个只保留最新的如果前一个事件是DOWN则不能丢弃后一个 MOVE必须按严格顺序分发这个策略保证了动画场景下的流畅性但也导致了一个排查难点如果应用处理 MOVE 过慢中间坐标会丢导致手势缺失。用adb shell dumpsys input能看到PendingEvents和OutboundQueue的情况。4.3 输入 ANR 与事件超时机制InputDispatcher 的 ANR 是 Framework 开发者绕不开的大山。它和应用的 ANR 不太一样输入 ANR 是指事件分发到窗口后窗口在超时时间内没有消费完并返回finished信号。这个超时时间由InputDispatcher::mInputDispatchTimeout控制默认是 5 秒。事件分发到应用后InputDispatcher会启动一个超时定时器对应ANR回调。超时后系统会做两件事一是通过InputManagerService回调notifyANR()触发 app 的 ANR 弹窗二是如果检测到目标窗口无响应会把事件转给下一个可接收的窗口或者直接丢弃。经验上大部分输入 ANR 都不是 InputDispatcher 的问题而是应用主线程卡死导致无法消费事件。但有一种情况比较特殊Choreographer卡死或View绘制耗时过长导致应用虽然在跑但主线程的Looper无法及时执行InputEventReceiver的onInputEvent()回调。这种情况下应用知道自己没死InputDispatcher却判定 ANR需要通过主线程消息队列的排队时长来判断根因。排查输入 ANR推荐第一个手段adb shell dumpsys input看Input Dispatcher State里是否有ApplicationNotResponding信息以及对应的窗口和连接状态。然后抓ANR traceadb shell debuggerd -b $(pidof system_server)看看 system_server 里 InputDispatcher 线程当前卡在哪个方法上。如果是looperWait那基本是应用侧没消费导致超时。如果卡在MotionEvent.cpp的某个方法里可能是 Binder 通信或 SurfaceFlinger 合成出问题了。4.4 InputChannel 的建立与 Binder 通信桥这里要展开讲一下InputChannel因为很多深入的性能问题都跟它有关。InputChannel本质上是一个封装了 socketpair 的 Native 对象。当系统为窗口addWindow时ViewRootImpl.setView()会调用InputChannel.openInputChannelPair()创建一个socketpair。socketpair 的两个 fd 一个保留在系统侧InputDispatcher另一个通过 Binder 调用system_server传给应用进程。应用侧通过InputChannel的 fd 注册到Looper上用Looper的 epoll 机制监听可读事件。这个设计的精妙之处在于Linux socketpair 是全双工的系统侧可以写事件、应用侧可以读事件应用侧消费完通过同一个 socket 写回 finished 信号系统侧再读。整个链路不经过 Binder性能极高。这也是为什么输入事件能保持低延迟的关键原因之一。排查InputChannel问题时如果应用侧收不到事件很可能是 fd 传递失败或InputChannel.release()被提前调用。检查dumpsys input里的Connections信息看每个连接的inputChannel的 fd、状态、monitor和outboundQueue的具体情况。5. Framework 与应用的对接InputEventReceiver 到 View 分发事件到达应用进程后并不直接交给某个 View而是经过一套精心设计的分发机制。这一步是应用层开发者接触最频繁的部分。5.1 ViewRootImpl 与 WindowInputEventReceiver应用进程里每个Window都对应一个ViewRootImpl。ViewRootImpl在setView()过程中会创建WindowInputEventReceiver它是InputEventReceiver的子类。InputEventReceiver构造时会通过 JNI 创建 Native 层的NativeInputEventReceiver把InputChannel的 fd 注册到当前线程通常是主线程的Looper上。事件可读时Looper会回调InputEventReceiver.dispatchInputEvent()。经过 JNI 层把InputEvent对象从 Native 上抛到 Java 层。WindowInputEventReceiver.onInputEvent()会把事件交给ViewRootImpl的InputStage处理。InputStage是事件分发的流水线按顺序包含ViewPreImeInputStage进行焦点判断、预 IME 处理ImeInputStage把事件交给输入法InputMethodService消费ViewPostImeInputStage事件回到应用真正进入 View 分发SyntheticInputStage处理一些合成事件比如导航键、轨迹球每个 Stage 可以消费事件返回FINISH_HANDLED也可以不处理交给下一个 Stage。5.2 View 事件分发三大方法最终进入 View 层的事件分发核心就是dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三者的博弈。ViewGroup.dispatchTouchEvent()的流程是先判断是否需要拦截onInterceptTouchEvent()决定是否拦截事件一旦拦截后续事件不再下发给子 View不拦截时按 z-order 从顶层到底层遍历子 View用isTransformedTouchPointInView()判断触摸点是否在子 View 的范围内如果子 View 是 ViewGroup递归执行 dispatch如果子是 View执行dispatchTouchEventView 的dispatchTouchEvent会优先执行OnTouchListener然后调onTouchEvent如果所有子 View 都不消费事件回到 ViewGroup 的onTouchEvent理解这套机制的关键是事件永远先走父的 dispatch再决定是否分配给子 View一旦子 View 在 DOWN 事件中消费了后续的 MOVE/UP 就会锁定该子 View不会再重新分配除非拦截。这也是很多面试里问“事件分发机制”时最常见的深坑点。有个高频问题为什么在RecyclerView里子项的点击事件总是不响应答案往往在父容器拦截逻辑比如ScrollView的onInterceptTouchEvent在判断为滑动时拦截了事件导致子 View 的 DOWN 被取消。解决思路是重写父容器的拦截逻辑或使用requestDisallowInterceptTouchEvent(true)。5.3 键盘事件与焦点分发触摸事件走的是点击命中测试键盘事件走的是焦点分发。InputDispatcher会维护一个mFocusedWindowHandle持有焦点的窗口才会收到按键事件。应用进程里ViewRootImpl收到按键事件后最终通过ViewRootImpl.dispatchKeyEvent()分发给ViewTree中持有焦点的 View。对于 EditText 这类输入控件KeyEvent最终会通过InputConnection交给输入法编辑。应用层常见的问题是点击 EditText 后输入法不弹起来或者弹起来后按键无效果。这种问题通常要检查focusable、focusableInTouchMode和WindowManager.LayoutParams的softInputMode设置。InputMethodService通过InputConnection与应用交互本质上绕过了 View 的事件分发直接通过 Binder 修改EditorInfo和ComposingText。这解释了为什么输入法输进去的字EditText没有经过dispatchKeyEvent也能上屏。排查输入法问题时不要把注意力放在 View 层要去看InputMethodManager和 IME binding 的连接状态。6. 实战一套完整的输入问题排查工具链很多人学完原理后不知道怎么下手这里分享我实际工作中处理输入子系统问题的一套排查工具链覆盖从底层到顶层。6.1 常用调试命令汇总命令大概是这几个根据问题的层次选择排查层次命令作用内核设备层adb shell getevent -lt查看设备节点原始事件内核设备层adb shell cat /proc/bus/input/devices查看系统有哪些输入设备Native 层adb shell dumpsys input查看输入子系统整体状态Framework 层adb shell dumpsys window windows查看窗口信息和 touchableRegion应用层adb shell settings put system pointer_location 1屏幕上显示触摸轨迹应用层adb shell dumpsys activity top查看当前顶层 Activity 和 View 层级dumpsys input是排查输入问题最全面的入口建议重点看几个 sectionInput Reader State每个设备的 mappers、raw 和 cooked 状态Input Dispatcher State所有窗口、连接、焦点、悬停Queue部分inboundQueue、outboundQueue 的长度如果持续有堆积说明上游或下游存在瓶颈6.2 定位“触点偏移”问题的方法触点偏移触摸输出点和手指不一致是触摸问题里最常见的导致原因可能在内核驱动层也可能在 Framework 层。我的排查顺序先用getevent查原始坐标。比如触摸屏物理分辨率是 1080x1920getevent里ABS_MT_POSITION_X最大值如果是 32767说明是相对坐标需要做映射。用dumpsys input看Input Reader里是否配置了正确的 calibration 参数。检查.idc配置文件重点看touch.orientation、touch.scale、touch.offsetX、touch.offsetY。特别是touch.orientation配置错误会导致触摸方向和屏幕方向不一致。屏幕旋转后出现偏移优先检查DisplayInfo.rotation和InputReaderConfiguration的displayViewport.orientation是否一致。之前遇到过横竖屏切换后触点偏移 90 度最后发现是DisplayInfo更新了但InputReader的 viewport 没有同步刷新属于 Framework 的同步 bug。6.3 定位按键无效问题的方法按键无效直接查三步。第一步确认内核有没有上报。getevent里按一下按键如果没有任何输出先查驱动和硬件设备节点是否存在。第二步确认 keylayout 映射是否正确。getevent -l拿到内核扫描码然后在对应的.kl文件里查找映射。比如adb shell cat /vendor/usr/keylayout/Generic.kl如果扫描码被映射到KEYCODE_UNKNOWN按键自然无效。这时需要修改.kl文件注意 QWERTY 键盘和特殊按键音量键、电源键的映射表是不同的。第三步确认事件有没有分发到窗口。dumpsys input里看focusedWindow是不是目标窗口以及DispatcherState中事件是否被消费或丢弃。有个很隐蔽的场景某些应用在onKeyDown里消费了事件并返回 true但应用自己没做任何处理导致系统按键比如音量无效。遇到这种情况要看应用的KeyEvent日志。6.4 性能与延迟优化的关键路径输入事件的全链路延迟优化核心指标是从手指物理接触到屏幕产生反应的时间。业界一般以 100ms 作为跟手的及格线超过 150ms 用户就能主观感知到卡顿。优化的几个关键点InputReader 的读取频率取决于epoll_wait的机制触摸屏每秒上报的事件频率可能高达 120Hz-240Hz。驱动如果事件上报太多EventHub读取、InputReadermapper 解析都会产生 CPU 开销。可以通过dumpsys input看每个 mapper 的averageLatency指标。InputDispatcher分发阶段最重要的优化是减少MotionEvent的拷贝和 IPC。从系统到应用的事件传递本身很快socketpair瓶颈往往在应用侧的Choreographer消息队列。如果应用主线程堆积大量Runnable即使InputDispatcher把事件送到应用进程也没法及时被处理。排查主线程卡顿用adb shell debuggerd -b $(pidof com.xxx.app) adb shell cat /data/anr/traces.txtChoreographer的doFrame是绘制的核心。如果doFrame里执行了耗时操作网络、IO、复杂布局input 事件就会被阻塞在ViewPostImeInputStage之前。最后补充一个我自己验证过的经验触摸跟手性差不一定是代码问题有时候是触摸屏 IC 的报点率太低。dumpsys input的TouchInputMapper打印里如果maxTouchPoints和reportRate很低比如只有 60Hz要考虑让硬件方案商调高报点率。软件层怎么优化都只会在系统侧花时间不会提高硬件采样频率。7. 疑难杂症与高频踩坑实录这块我觉得比原理更有价值。直接抛出我在项目实战中遇到的几个经典问题以及排查过程。7.1 触摸偶发飞点问题现象用户快速滑动时触摸点偶尔会跳到屏幕边缘然后又跳回来。画线明显出现异常的斜线。排查过程先用getevent -lt抓原始事件发现某些 slot 的ABS_MT_POSITION_X/Y突变为 0 或最大值。对比驱动源码发现触摸屏固件在报点时会周期性上报一个TRACKING_ID-1的无效点然后又上报正常点。TouchInputMapper把TRACKING_ID-1解析成 pointer up然后下一个点又解析为 down导致坐标跳变。解决方案有两层驱动层让方案商修复固件不发出无效点Framework 层做冗余过滤在TouchInputMapper中如果新点的 down 时间距离上一个 up 时间太短 10ms且坐标距离很近则合并处理为单点滑动。典型的补丁点是在TouchInputMapper::processRawTouches()里增加一个isDriftWithinThreshold()方法做位置和时间的联合判断。7.2 耳机线控按键偶发失灵现象插上耳机按线控的接听键有时候一次按下的动作被识别成两次双击或者干脆没反应。原因耳机的线控按键用的是EV_KEYKEY_MEDIA但三根线控的阻抗匹配在某些耳机上不完美导致按键的输出不是一个干净的方波信号内核的gpio-keys驱动在 debounce 窗口内检测到了多次上升/下降沿。InputReader这层的KeyboardInputMapper已经做了消抖但当内核上报的抖动超出了keyRepeatTimeout窗口时还是会多出来一个额外的KEY_UP和KEY_DOWN。解决方向在EventHub或KeyboardInputMapper中增加时间窗口去抖逻辑。比如如果相邻两个DOWN事件间隔小于 50ms后一个直接丢弃。这个思路对红外遥控器的按键乱报同样有效。7.3 应用收不到事件但系统日志正常现象dumpsys input显示事件已经放入连接队列但应用侧 View 就是没反应甚至onTouchEvent里加了日志也没输出。还伴随一个特点问题只出现在快速点击中慢速点击正常。排查过程查看应用主线程的状态发现主线程的MessageQueue里堆积了大量其他消息InputEventReceiver的 dispatch 排在很后面。参考前面讲的InputChannel机制——它注册在Looper上如果主线程阻塞在 Binder 调用或Thread.sleep即使 fd 可读也无法被执行。最终发现是应用在onTouchEvent里做了同步网络请求导致主线程阻塞后一个事件的 dispatch 全部延期。而且InputDispatcher因为没收到finished信号会对后续 MOVE 做合并与丢弃表现为触摸丢点。这类问题的标配解决方案是任何耗时操作都不能放在主线程触摸事件的消费尤其要注意onTouchEvent里禁止做 IO、反射、大数据结构遍历。7.4 分屏多窗口下触摸坐标错乱现象开启分屏后应用 A 在上半屏但是触摸下半屏的区域也能触发 A 的点击坐标错乱到让人怀疑人生。原因这是 WindowManager 的touchableRegion和frame没有正确同步导致的。分屏时DisplayArea的分割布局更新了appBounds但某些窗口的InputWindowHandle还保留了旧的touchableRegion。旧区域没有裁剪到新 bounds 内所以事件照样被分发。排查命令adb shell dumpsys window windows | grep -B 2 -A 15 mTouchableRegion确认每个窗口的touchableRegion是否和可见区域吻合。如果是系统 bug通常需要WindowManagerService在relayoutWindow后强制刷新InputDispatcher的窗口句柄。如果是应用问题检查应用是否有自定义touchableRegion或强制设置了FLAG_NOT_TOUCHABLE等 flag。8. 关于输入子系统的一些学习路径建议很多人问怎么深入学 Android 输入子系统我建议一个由浅入深、由外而内的路线。先从应用层出发搞懂View分发机制这是基础也是绝大多数面试题的高频考点。重点理解dispatchTouchEvent、onInterceptTouchEvent、onTouchEvent三者的关系建议手画调用图分 DOWN/MOVE/UP 三个动作模拟一遍事件流向。强烈建议用两个自定义 View 嵌套打印每一步的调用栈日志自己验证一遍。接着进入 Framework Java 层读InputManagerService、InputMonitor、WindowInputEventReceiver的源码。重点看ViewRootImpl和InputStage的衔接理解系统窗口如状态栏、输入法窗口和应用窗口的差异。然后进入 Native 层读EventHub、InputReader、InputDispatcher的源码。我把这几个文件的阅读顺序建议排一下先读InputDispatcher的外层方法notifyKey、notifyMotion再看InputReader的loopOnce最后才是EventHub的getEvents。这三个线程和它们的队列模型是整个子系统的骨架先把骨架看了再补齐细节。最后可以根据需要追内核drivers/input/touchscreen/和drivers/input/keyboard/的驱动代码。内核层主要关注input_dev的注册、input_report_*的上报、input_register_handler的事件处理。理论学习之外实际设备是最好的实验场。找一台有 root 权限的设备或模拟器反复使用getevent、dumpsys input、sendevent注入事件观察注入后系统行为的改变。我之前就是在调试一个 3D 触控项目时用sendevent模拟大压力触摸才真正理解了压力值在整个输入链路中的传递方式。有条件的话建议研究一下InputManagerService和WindowManagerService的联动逻辑比如输入法窗口切换时mFocusedWindow如何变化多窗口模式下touchableRegion的刷新时机。从输入视角看窗口管理会把两大系统服务串起来理解收获很大。输入子系统这个方向面试中很吃香因为这需要你同时掌握 Linux 内核知识、Native 层 C 代码、Framework Java 代码、应用层 View 机制。四层知识任何一层有短板排查问题就会失控。能在这个领域深入下去对系统稳定性和性能优化都会有很强的全局把控能力。
返回列表