11.1 为什么需要两个查找方法你想想看键盘事件和触摸事件的性质完全不同。键盘事件用户按下一个键系统得知道当前哪个窗口获得了焦点。焦点窗口只有一个所以查找逻辑相对简单。触摸事件用户手指点下去系统得知道手指落在哪个窗口上。可能有多个窗口重叠还得考虑窗口的可见区域、是否可点击等等。所以Android把这两种查找逻辑分开了。一个管焦点一个管触摸。嗯这里要注意触摸事件的分发远比键盘事件复杂我在项目中遇到过好几次因为触摸目标找错导致的诡异Bug后面会细说。11.2 findFocusedWindowTargets()焦点窗口的查找这个方法的核心任务找到当前拥有输入焦点的窗口然后把事件发过去。我们先看它的简化流程// 伪代码展示核心逻辑 void findFocusedWindowTargets() { // 1. 获取当前焦点窗口 spInputWindowHandle focusedWindow findFocusedWindow(); // 2. 检查焦点窗口是否有效 if (focusedWindow nullptr) { // 没有焦点窗口丢弃事件 return; } // 3. 检查窗口是否已死亡 if (focusedWindow-getInputChannel() nullptr) { // 窗口已销毁丢弃事件 return; } // 4. 检查窗口是否可接收事件 if (!focusedWindow-canReceiveInput()) { // 窗口不可接收输入丢弃事件 return; } // 5. 添加到目标列表 mTargets.add(focusedWindow); }看起来很简单对吧但实际源码里要考虑的细节远不止这些。我记得有一次调试一个第三方输入法的问题发现按键事件总是丢失最后定位到就是findFocusedWindow()返回了nullptr——因为焦点窗口在事件到达前刚好被销毁了。关键点焦点窗口的查找依赖于WindowManagerService维护的焦点栈。每次窗口焦点变化时WMS都会通过InputManagerService通知InputDispatcher更新焦点信息。11.3 findTouchedWindowTargets()触摸窗口的查找这个方法就复杂多了。触摸事件的目标查找说白了就是根据触摸点的坐标找到最合适的那个窗口。我们来看核心流程// 伪代码展示核心逻辑 void findTouchedWindowTargets() { // 1. 获取所有可见窗口 VectorspInputWindowHandle windows getVisibleWindows(); // 2. 按Z-order排序从上到下 sortWindowsByZOrder(windows); // 3. 遍历窗口找到第一个包含触摸点的窗口 for (auto window : windows) { // 3.1 检查窗口是否可触摸 if (!window-canReceiveTouch()) continue; // 3.2 检查触摸点是否在窗口区域内 if (!window-containsPoint(touchX, touchY)) continue; // 3.3 检查窗口是否被遮挡 if (isWindowObscured(window)) continue; // 3.4 找到了添加到目标列表 mTargets.add(window); break; } // 4. 如果没找到检查是否有壁纸窗口 if (mTargets.isEmpty()) { handleTouchOnWallpaper(); } }这里有几个坑我一个个说。11.3.1 窗口遮挡检测你想想看如果两个窗口重叠了上面的窗口是透明的那触摸事件应该发给谁Android的处理方式是默认发给最上面的窗口。但如果上面的窗口设置了FLAG_NOT_TOUCHABLE或者FLAG_NOT_TOUCH_MODAL那事件就会穿透到下面的窗口。避坑指南我曾经遇到过一个Bug弹窗挡住了底部的按钮但弹窗本身设置了透明背景且没有消费触摸事件。结果用户点击弹窗区域事件穿透到了底部的按钮上。解决方案就是给弹窗设置FLAG_NOT_TOUCH_MODAL并正确处理事件拦截。11.3.2 触摸区域的计算窗口的触摸区域不仅仅是它的布局边界。还要考虑裁剪区域父窗口可能对子窗口进行裁剪圆角裁剪某些窗口有圆角效果安全区域刘海屏、挖孔屏的避让区域我记得有一次调试一个全面屏手机的触摸问题用户点击屏幕边缘总是没反应。查了半天发现是安全区域的计算出了问题触摸点被判定为在安全区域之外导致事件被丢弃。11.4 两个方法的协作关系你可能要问了这两个方法会不会同时被调用答案是会但分场景。事件类型调用的方法说明按键事件KeyEventfindFocusedWindowTargets()发给焦点窗口触摸事件MotionEventfindTouchedWindowTargets()发给触摸点下的窗口轨迹球事件两者都可能取决于具体实现嗯这里要注意触摸事件的分发优先级高于焦点事件。什么意思呢比如用户正在触摸一个窗口这时候来了一个按键事件按键事件还是会发给焦点窗口而不是触摸窗口。两者互不干扰。11.5 源码中的关键数据结构这两个方法操作的核心数据结构是InputTarget。每个InputTarget代表一个事件的目标窗口包含struct InputTarget { // 目标窗口的输入通道 spInputChannel inputChannel; // 标志位FLAG_FOREGROUND, FLAG_WALLPAPER等 uint32_t flags; // 全局缩放比例用于多窗口模式 float globalScaleFactor; // 窗口的变换矩阵 const InputWindowHandle* windowHandle; };我个人习惯在调试时重点关注flags字段。它决定了事件是以什么方式发送给窗口的——是前台分发还是后台分发是否需要壁纸处理等等。11.6 实战经验总结最后分享几个我在项目中踩过的坑焦点窗口丢失快速切换Activity时焦点窗口可能短暂为null。解决方案是在findFocusedWindowTargets()里加一个重试机制。触摸穿透Dialog或PopupWindow没有正确设置FLAG_NOT_TOUCH_MODAL导致事件穿透到下层窗口。多指触摸多点触控时每个手指的触摸点可能落在不同窗口上。Android的处理方式是第一个手指决定目标窗口后续手指都发给同一个窗口。调试技巧如果你想观察事件分发的目标窗口可以在InputDispatcher的日志里搜索findFocusedWindow或findTouchedWindow。开启adb shell dumpsys input也能看到当前窗口的焦点状态和触摸区域信息。好了这一章我们详细拆解了findFocusedWindowTargets()和findTouchedWindowTargets()的内部逻辑。下一章我们会继续深入看看事件找到目标窗口之后是怎么通过InputChannel发送过去的。到时候我会重点讲一讲跨进程通信的细节那也是很多性能问题的根源所在。