Android 14 InputDispatcher无焦点窗口ANR深度剖析:从Monkey测试到真实用户场景的避坑实战
最近在分析Android 14的ANR问题时,我发现了一个特别有意思的现象:有些ANR在Monkey测试中频繁出现,但在真实用户操作场景下却很难复现。这背后往往隐藏着系统层级的协同设计问题,特别是WindowManagerService(WMS)与InputDispatcher之间的焦点窗口管理机制。
作为Android系统开发者和应用性能优化工程师,我们经常需要面对这类“测试能复现,用户难遇到”的诡异问题。今天我就结合自己最近处理的一个典型案例,深入剖析InputDispatcher无焦点窗口ANR的成因、复现机制以及优化方案。这个案例涉及Android 14系统,核心问题出现在快速启动和销毁Activity的场景下,WMS与InputDispatcher对焦点窗口的判定出现了不一致。
1. 问题现象:Monkey测试中的幽灵ANR
我们先来看一个典型的ANR日志片段:
ANR in com.android.launcher3
Reason: Input dispatching timed out (Waiting because no window has focus but there is a focused application that may eventually add a window when it finishes starting up.)
这种ANR的核心特征是:系统认为当前有焦点应用,但却找不到可以接收输入事件的焦点窗口。从日志层面看,通常会出现以下矛盾现象:
- 上层WMS视角:WMS认为Launcher是当前的焦点窗口
- 底层InputDispatcher视角:InputDispatcher侧没有有效的焦点窗口
- 特殊窗口状态:系统为
recents_animation_input_consumer请求了焦点,但这个窗口最终没有成为焦点窗口,原因可能是NO_WINDOW或NOT_VISIBLE
我在实际项目中遇到的这个案例,最初就是在Monkey压力测试中发现的。测试团队报告说Launcher频繁出现ANR,但当我们尝试手动复现时,即使用完全相同的操作序列,也很难触发同样的问题。
1.1 Monkey测试与真实用户操作的差异
为什么Monkey能复现而用户操作难复现?这涉及到Android输入事件分发机制的一个关键细节:
Monkey测试的输入事件生成方式:
# Monkey通过adb命令直接注入输入事件
adb shell monkey -p com.example.demoapp --throttle 100 --pct-touch 40 --pct-motion 40 -v 1000
# 或者直接注入特定按键事件
adb shell input keyevent KEYCODE_RECENT_APPS
用户操作的输入事件路径:
- 用户触摸屏幕 → 硬件中断 → EventHub → InputReader → InputDispatcher → 应用窗口
- 点击应用图标 → Launcher处理点击事件 → 启动目标Activity
这两种方式在窗口焦点更新时机上存在微妙差异,特别是在Activity快速启动和销毁的场景下。
注意:adb命令启动Activity时,系统不会为启动中的Activity创建SnapshotStartingWindow,而点击图标启动时,系统会尝试创建快照窗口。这个差异在某些场景下会直接影响焦点窗口的更新逻辑。
2. 深入InputDispatcher:焦点窗口管理机制解析
要理解这个ANR,我们需要先搞清楚Android输入系统的焦点窗口管理机制。InputDispatcher作为输入事件的分发中枢,它的焦点窗口判定逻辑直接影响着ANR的触发条件。
2.1 InputDispatcher的焦点窗口判定流程
InputDispatcher维护焦点窗口的核心逻辑在FocusResolver类中。当SurfaceFlinger更新窗口列表时,会调用FocusResolver.setInputWindows()来重新计算焦点窗口:
// 简化后的焦点窗口判定逻辑
Focusability FocusResolver::isTokenFocusable(
const sp<IBinder>& token,
const std::vector<WindowInfoHandle>& windows) {
// 1. 检查token对应的窗口是否还在当前窗口列表中
auto it = std::find_if(windows.begin(), windows.end(),
[&token](const WindowInfoHandle& handle) {
return handle.getToken() == token;
});
if (it == windows.end()) {
return Focusability::NO_WINDOW; // 窗口已不存在
}
// 2. 检查窗口是否可见
if (!it->getInfo()->isVisible()) {
return Focusability::NOT_VISIBLE; // 窗口不可见
}
// 3. 检查窗口是否能接收输入
if (!it->getInfo()->focusable) {
return Focusability::NOT_FOCUSABLE; // 窗口不可聚焦
}
return Focusability::OK; // 窗口可以作为焦点窗口
}
这个判定逻辑看似简单,但在实际运行中却可能因为窗口状态的不同步而产生问题。
2.2 WMS与InputDispatcher的协同问题
问题的核心在于:WMS和InputDispatcher维护焦点窗口的逻辑存在时间差和状态不一致。
| 组件 | 焦点窗口判定依据 | 更新时机 | 潜在问题 |
|---|---|---|---|
| WMS | ActivityRecord.mVisibleRequested WindowState.canReceiveKeys() |
Activity生命周期变化 窗口添加/移除 |
可能认为Launcher可见且可接收按键 |
| InputDispatcher | Layer.isVisibleForInput() WindowInfo中的标志位 |
SurfaceFlinger更新窗口列表 | 可能认为Launcher对应的Layer不可见 |
| SurfaceFlinger | Layer.isHiddenByPolicy() 父/相对Layer的可见性 |
窗口合成时更新 | 依赖Layer层级关系,可能误判 |
这种不一致在特定时序下会被放大。比如在快速启动和销毁Activity的场景中:
- Activity A 启动 → 获得焦点
- 用户按下Recent键 → 切换到Recents界面
- Activity A 快速finish → 窗口被移除
- 系统尝试将焦点切回Launcher,但...
此时如果Launcher对应的Task中所有Activity都finish了,那么Task会被标记为不可见,进而导致recents_animation_input_consumer(它的相对Layer是该Task)也被判定为不可见。
2.3 recents_animation_input_consumer的特殊角色
recents_animation_input_consumer是Android多任务系统中一个特殊的输入消费者,它在Recents动画期间临时接管输入事件。它的创建和显示逻辑如下:
// InputConsumerImpl中创建recents_animation_input_consumer
InputConsumerImpl createRecentsAnimationInputConsumer(SurfaceControl relativeLayer) {
InputConsumerImpl consumer = new InputConsumerImpl(
INPUT_CONSUMER_RECENTS_ANIMATION,
"recents_animation_input_consumer"
);
// 设置相对Layer(即被transientHide的Task)
consumer.setRelativeLayer(relativeLayer);
// 显示该输入消费者
consumer.show();
return consumer;
}
关键问题在于:WMS为recents_animation_input_consumer请求焦点时,并不检查其相对Layer的状态,而SurfaceFlinger和InputDispatcher在判定焦点窗口时,会严格检查相对Layer的可见性。
3. 复现与分析:从Monkey到真实场景的桥梁
要真正理解这个问题,我们需要能够稳定复现它。下面我分享两种复现方法,分别对应Monkey测试场景和模拟的用户操作场景。
3.1 Monkey测试场景复现(稳定但非用户操作)
这种方法通过adb命令精确控制Activity的启动和销毁时机:
# 步骤1:启动Demo应用

179

被折叠的 条评论
为什么被折叠?



