Android Input 系统事件分发机制深度解析
前言
在做系统定制时,遇到一个需求:需要在系统层拦截特定按键组合(音量上+电源键)触发截屏功能。这个需求看似简单,但涉及到 Android Input 系统的核心机制。通过深入研究源码,对 Input 事件从硬件到应用的完整流程有了清晰的认识。
Input 系统架构概览
Android Input 系统分为三层:
scss
┌─────────────────────────────────────┐
│ 应用层 (Application) │
│ View.onTouchEvent() │
└─────────────────────────────────────┘
↑
┌─────────────────────────────────────┐
│ Framework 层 │
│ InputManagerService │
│ WindowManagerService │
└─────────────────────────────────────┘
↑
┌─────────────────────────────────────┐
│ Native 层 │
│ InputReader (读取事件) │
│ InputDispatcher (分发事件) │
└─────────────────────────────────────┘
↑
┌─────────────────────────────────────┐
│ 驱动层 (Kernel) │
│ /dev/input/eventX │
└─────────────────────────────────────┘
事件流转全流程
1. 驱动层:事件产生
当用户按下按键或触摸屏幕时,硬件产生中断,内核驱动将事件写入 /dev/input/eventX 设备节点。
bash
# 查看输入设备
adb shell getevent -l
# 输出示例
/dev/input/event2: EV_KEY KEY_VOLUMEUP DOWN
/dev/input/event2: EV_SYN SYN_REPORT 00000000
事件格式(input_event 结构):
c
struct input_event {
struct timeval time; // 时间戳
__u16 type; // 事件类型:EV_KEY, EV_ABS 等
__u16 code; // 按键码或坐标轴
__s32 value; // 值:0=UP, 1=DOWN, 2=REPEAT
};
2. Native 层:InputReader 读取事件
InputReader 运行在 SystemServer 进程的独立线程中,通过 epoll 监听所有输入设备。
cpp
// frameworks/native/services/inputflinger/reader/InputReader.cpp
void InputReader::loopOnce() {
size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
for (size_t i = 0; i < count; i++) {
RawEvent& rawEvent = mEventBuffer[i];
processEventsLocked(rawEvent);
}
}
关键流程:
EventHub::getEvents()从/dev/input/eventX读取原始事件InputReader::processEventsLocked()处理事件- 根据设备类型(键盘、触摸屏)调用对应的
InputMapper
3. Native 层:InputDispatcher 分发事件
InputReader 将处理后的事件放入队列,InputDispatcher 负责分发到目标窗口。
cpp
// frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
void InputDispatcher::dispatchOnce() {
nsecs_t nextWakeupTime = LONG_LONG_MAX;
// 从队列取出事件
mPendingEvent = mInboundQueue.dequeueAtHead();
// 找到目标窗口
InputTarget target = findFocusedWindowTargetLocked();
// 分发事件
dispatchEventLocked(currentTime, mPendingEvent, target);
}
关键点:
- ANR 检测:如果窗口 5 秒内未消费事件,触发 ANR
- 事件注入 :通过
injectInputEvent()可以模拟输入
4. Framework 层:事件传递到应用
事件通过 Socket 发送到应用进程的 InputChannel:
java
// frameworks/base/core/java/android/view/ViewRootImpl.java
final class WindowInputEventReceiver extends InputEventReceiver {
@Override
public void onInputEvent(InputEvent event) {
enqueueInputEvent(event, this, 0, true);
}
}
然后经过 View 树的分发:
scss
Activity.dispatchTouchEvent()
→ PhoneWindow.superDispatchTouchEvent()
→ DecorView.superDispatchTouchEvent()
→ ViewGroup.dispatchTouchEvent()
→ View.onTouchEvent()
实战:拦截音量键+电源键组合
需求分析
需要在系统层拦截 VOLUME_UP + POWER 组合键,触发截屏。拦截点选择:
- ❌ 应用层:无法拦截系统级按键
- ❌ InputReader:太底层,难以判断组合键
- ✅ PhoneWindowManager:最佳位置,专门处理系统按键
源码分析
java
// frameworks/base/services/core/java/com/android/server/policy/PhoneWindowManager.java
public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
final int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
case KeyEvent.KEYCODE_VOLUME_DOWN:
// 处理音量键
break;
case KeyEvent.KEYCODE_POWER:
// 处理电源键
break;
}
}
这个方法在事件入队前调用,可以拦截或修改事件。
实现方案
java
// PhoneWindowManager.java
private boolean mVolumeUpPressed = false;
private boolean mPowerPressed = false;
private static final long COMBINATION_TIMEOUT = 150; // 150ms 内按下视为组合键
public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
final int keyCode = event.getKeyCode();
final boolean down = event.getAction() == KeyEvent.ACTION_DOWN;
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_UP:
mVolumeUpPressed = down;
if (down && mPowerPressed) {
takeScreenshot();
return 0; // 拦截事件,不再分发
}
break;
case KeyEvent.KEYCODE_POWER:
mPowerPressed = down;
if (down && mVolumeUpPressed) {
takeScreenshot();
return 0;
}
break;
}
return ACTION_PASS_TO_USER; // 继续分发
}
private void takeScreenshot() {
mHandler.post(() -> {
try {
mScreenshotHelper.takeScreenshot(SCREENSHOT_FULLSCREEN);
} catch (Exception e) {
Log.e(TAG, "Failed to take screenshot", e);
}
});
}
遇到的问题
问题 1:组合键判断不准确
用户可能先按音量键,过了 1 秒才按电源键,不应该触发截屏。
解决方案:添加时间窗口判断
java
private long mVolumeUpPressTime = 0;
case KeyEvent.KEYCODE_VOLUME_UP:
if (down) {
mVolumeUpPressed = true;
mVolumeUpPressTime = SystemClock.uptimeMillis();
} else {
mVolumeUpPressed = false;
}
break;
case KeyEvent.KEYCODE_POWER:
if (down && mVolumeUpPressed) {
long delta = SystemClock.uptimeMillis() - mVolumeUpPressTime;
if (delta < COMBINATION_TIMEOUT) {
takeScreenshot();
return 0;
}
}
break;
问题 2:与原有功能冲突
电源键长按会弹出关机菜单,组合键会误触发。
解决方案:拦截后取消后续处理
java
if (down && mVolumeUpPressed && delta < COMBINATION_TIMEOUT) {
takeScreenshot();
// 取消电源键的长按检测
cancelPendingPowerKeyAction();
// 取消音量键的重复事件
cancelPendingVolumeKeyAction();
return 0; // 完全拦截
}
调试技巧
1. 查看输入设备信息
bash
# 列出所有输入设备
adb shell dumpsys input
# 实时查看输入事件(带时间戳)
adb shell getevent -lt
# 查看按键映射配置
adb shell dumpsys input | grep -A 50 "KeyCharacterMap"
2. 注入测试事件
bash
# 模拟按键
adb shell input keyevent KEYCODE_VOLUME_UP
# 模拟触摸点击
adb shell input tap 500 1000
# 模拟滑动(起点 终点 持续时间ms)
adb shell input swipe 100 500 100 1000 300
3. 添加调试日志
java
// PhoneWindowManager.java
private static final boolean DEBUG_INPUT = true;
public int interceptKeyBeforeQueueing(KeyEvent event, int policyFlags) {
if (DEBUG_INPUT) {
Log.d(TAG, "interceptKey: code=" + event.getKeyCode()
+ " action=" + event.getAction()
+ " time=" + event.getEventTime());
}
// ...
}
Input 事件的性能考量
事件处理延迟分析
通过 systrace 分析,Input 事件从硬件到应用的典型延迟:
| 阶段 | 耗时 | 说明 |
|---|---|---|
| 硬件中断 → InputReader | 1-3ms | 内核调度延迟 |
| InputReader 处理 | 0.5-2ms | 事件解析和转换 |
| InputDispatcher 分发 | 0.5-1ms | 查找目标窗口 |
| 跨进程传输 | 1-2ms | Socket 通信 |
| 应用处理 | 变量 | 取决于 View 层级 |
总延迟:3-8ms(理想情况)
如果主线程繁忙,延迟会显著增加,导致触摸不跟手。