Android 输入事件分发:一场穿越系统的 "快递之旅"

63 阅读6分钟

一、物流公司架构:从总部到快递员

想象 Android 输入系统是一家 "触摸物流公司",专门处理用户的触摸、键盘等输入 "包裹":

  • InputManagerService (IMS)  是物流公司总部,坐在顶层办公室(SystemServer 进程)负责全局调度

  • InputReader 是前线快递员,蹲守在硬件门口(/dev/input)收集包裹

  • InputDispatcher 是调度中心,负责将包裹按地址分发给正确的收件人

  • 应用程序 是最终收件人,每个窗口都是一个收货地址

总部与快递员之间通过一套复杂的物流系统协作,而每个应用窗口都有专属的 "收货地址"(InputChannel)。

二、物流公司开业:输入系统启动流程

2.1 总部成立:InputManagerService 的启动

当 Android 系统启动时,SystemServer 作为总公司老板,会下令成立 "触摸物流公司":

java

// SystemServer.java中启动输入服务
InputManagerService inputManager = new InputManagerService(context, handler);
inputManager.start(); // 物流公司正式开业

InputManagerService 的启动就像总部大楼的奠基,它会创建两个核心部门:

  • InputReader 部门:负责从硬件读取输入事件,像快递员在仓库门口收包裹
  • InputDispatcher 部门:负责调度分发事件,像调度中心安排配送路线

2.2 快递员上岗:InputReader 的工作准备

InputReader 快递员上岗后,会在仓库门口(/dev/input)安装一个监控摄像头(epoll),实时监控硬件设备:

cpp

运行

// InputReader.loopOnce 快递员巡检流程
void InputReader::loopOnce() {
    // 检查是否有新包裹需要处理
    size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
    if (count > 0) {
        processEventsLocked(mEventBuffer, count); // 处理收到的包裹
    }
}

EventHub 就像包裹分拣中心,负责扫描所有输入设备(/dev/input 下的文件),当有触摸或键盘事件时,就像新包裹到达,EventHub 会通知 InputReader:

cpp

运行

// EventHub.scanDevicesLocked 扫描可用设备
void EventHub::scanDevicesLocked() {
    DIR *dir = opendir("/dev/input"); // 打开仓库大门
    while ((de = readdir(dir)) != NULL) {
        if (de->d_name[0] == '.') continue;
        openDeviceLocked(devname); // 打开设备文件,注册监控
    }
}

2.3 调度中心运作:InputDispatcher 的调度逻辑

InputDispatcher 调度中心拿到包裹后,会查看收件地址簿(mWindowHandles),确定该发给哪个应用窗口:

cpp

运行

// InputDispatcher.dispatchOnceInnerLocked 调度核心逻辑
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    if (mInboundQueue.isEmpty()) {
        // 没有包裹,休息一下
    } else {
        mPendingEvent = mInboundQueue.dequeueAtHead(); // 取出包裹
        switch (mPendingEvent->type) {
            case EventEntry::TYPE_KEY:
                dispatchKeyLocked(...); // 分发键盘包裹
                break;
            case EventEntry::TYPE_MOTION:
                dispatchMotionLocked(...); // 分发触摸包裹
                break;
        }
    }
}

三、建立物流通道:InputChannel 的注册过程

每个应用窗口需要先申请一个 "收货地址"(InputChannel),才能收到输入包裹。这个过程就像用户在物流公司登记收货地址:

3.1 申请收货地址:ViewRootImpl 的注册

当 Activity 创建窗口时,会通过 ViewRootImpl 向 WMS 申请一个 InputChannel:

java

// ViewRootImpl.setView 申请收货地址
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    mWindowSession.addToDisplay(... , mInputChannel); // 申请输入通道
    if (mInputChannel != null) {
        mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper());
    }
}

WMS 收到申请后,会创建一对 "双向快递通道"(socket pair),一个给调度中心(Server 端),一个给应用(Client 端):

cpp

运行

// InputChannel.openInputChannelPair 创建双向通道
status_t InputChannel::openInputChannelPair(const String8& name, 
                                           sp<InputChannel>& serverChannel,
                                           sp<InputChannel>& clientChannel) {
    int sockets[2];
    socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets); // 创建socket对
    // 配置通道参数
    serverChannel = new InputChannel(name + "(server)", sockets[0]);
    clientChannel = new InputChannel(name + "(client)", sockets[1]);
    return OK;
}

3.2 地址登记:InputDispatcher 注册通道

WMS 将 Server 端通道注册到 InputDispatcher 的地址簿中,就像物流公司登记收件地址:

cpp

运行

// InputDispatcher.registerInputChannel 登记地址
status_t InputDispatcher::registerInputChannel(const sp<InputChannel>& inputChannel, 
                                              const sp<InputWindowHandle>& inputWindowHandle,
                                              bool monitor) {
    sp<Connection> connection = new Connection(inputChannel, inputWindowHandle, monitor);
    int fd = inputChannel->getFd();
    mConnectionsByFd.add(fd, connection); // 记录地址
    mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, handleReceiveCallback, this); // 监听该地址
    return OK;
}

3.3 收件人准备:应用端通道初始化

应用端的 Client 通道会绑定到 InputEventReceiver,就像收件人告诉物流公司把包裹送到哪个门卫处:

java

// InputEventReceiver 收件人门卫
public InputEventReceiver(InputChannel inputChannel, Looper looper) {
    mInputChannel = inputChannel;
    mMessageQueue = looper.getQueue();
    mReceiverPtr = nativeInit(this, inputChannel, mMessageQueue); // 初始化门卫
}

四、包裹配送:输入事件的分发过程

4.1 快递员取件:InputReader 获取事件

当用户触摸屏幕或按下键盘,InputReader 快递员会从硬件仓库取出包裹:

cpp

运行

// EventHub.getEvents 从硬件读取事件
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
    int fd = device->fd;
    int readSize = read(fd, readBuffer, sizeof(struct input_event) * capacity);
    // 解析输入事件,放入buffer
    for (size_t i=0; i<count; i++) {
        event->type = readBuffer[i].type;
        event->code = readBuffer[i].code;
        event->value = readBuffer[i].value;
        event++;
    }
    return event - buffer; // 返回包裹数量
}

4.2 调度中心分拣:InputDispatcher 处理事件

InputDispatcher 拿到包裹后,会根据地址簿找到对应的应用窗口:

cpp

运行

// InputDispatcher.dispatchKeyLocked 分发键盘事件
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, KeyEntry* entry, 
                                       DropReason* dropReason, nsecs_t* nextWakeupTime) {
    Vector<InputTarget> inputTargets;
    int32_t injectionResult = findFocusedWindowTargetsLocked(currentTime,
                                                           entry, inputTargets, nextWakeupTime);
    dispatchEventLocked(currentTime, entry, inputTargets); // 按地址分发
    return true;
}

4.3 包裹运输:通过 InputChannel 发送事件

调度中心通过 Server 端 InputChannel 将包裹发送出去,就像物流公司用卡车运输包裹:

cpp

运行

// InputPublisher.publishKeyEvent 发送键盘事件包裹
status_t InputPublisher::publishKeyEvent(...) {
    InputMessage msg;
    msg.header.type = InputMessage::TYPE_KEY;
    // 填充包裹内容(事件参数)
    return mChannel->sendMessage(&msg); // 通过通道发送
}

// InputChannel.sendMessage 通道运输
status_t InputChannel::sendMessage(const InputMessage* msg) {
    ssize_t nWrite = send(mFd, msg, msg->size(), MSG_DONTWAIT | MSG_NOSIGNAL);
    return nWrite >= 0 ? OK : -errno;
}

4.4 收件人签收:应用处理事件

应用端的 InputEventReceiver 门卫收到包裹后,会通知屋里的人(ViewHierarchy)来处理:

java

// NativeInputEventReceiver.handleEvent 门卫通知
int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {
    consumeEvents(env, false, -1); // 消费事件包裹
    return status == OK || status == NO_MEMORY ? 1 : 0;
}

// ViewRootImpl.deliverKeyEvent 事件送达应用
private void deliverKeyEvent(QueuedInputEvent q) {
    KeyEvent event = (KeyEvent)q.mEvent;
    if (mView.dispatchKeyEventPreIme(event)) { // 先给输入法处理
        finishInputEvent(q, true);
        return;
    }
    deliverKeyEventPostIme(q); // 再给应用视图处理
}

4.5 视图处理:事件在 ViewHierarchy 中的传递

事件包裹最终在 ViewHierarchy 中传递,就像包裹在家庭内部传递:

java

// Activity.dispatchKeyEvent 活动接收事件
public boolean dispatchKeyEvent(KeyEvent event) {
    Window win = getWindow();
    if (win.superDispatchKeyEvent(event)) { // 先给窗口处理
        return true;
    }
    View decor = mDecor;
    return event.dispatch(this, decor != null ? decor.getKeyDispatcherState() : null, this);
}

// View.dispatchKeyEvent 视图处理事件
public boolean dispatchKeyEvent(KeyEvent event) {
    if (mOnKeyListener != null && mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
        return true;
    }
    return onKeyEvent(event); // 自己处理
}

五、特殊包裹:软键盘事件的分发

当用户点击输入框时,就像需要特殊处理的 "加急包裹",需要输入法参与:

  1. 申请加急处理:View 获得焦点后,会通知 InputMethodManager 需要输入法

java

// TextView请求输入法
public void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
    super.onFocusChanged(focused, direction, previouslyFocusedRect);
    if (focused) {
        requestFocusFromTouch();
        mInputMethodManager.startInput(this, mInputConnection); // 启动输入法
    }
}
  1. 输入法处理:输入法通过 InputConnection 接收用户输入的字符

cpp

运行

// InputMethodService.commitText 输入法提交文本
public void commitText(CharSequence text, int newCursorPosition) {
    mInputConnection.commitText(text, newCursorPosition); // 提交到文本框
}
  1. 特殊通道传递:输入法提交的文本会作为特殊包裹,跳过输入法处理阶段

java

// ViewRootImpl处理输入法提交的事件
private void deliverKeyEventPostIme(QueuedInputEvent q) {
    if (mView.dispatchKeyEvent(event)) { // 直接给视图处理
        finishInputEvent(q, true);
        return;
    }
    finishInputEvent(q, false);
}

六、总结:输入事件的完整旅程

  1. 用户操作:触摸屏幕或按下键盘,硬件产生输入信号

  2. 快递员取件:InputReader 从 /dev/input 读取事件,封装成包裹

  3. 调度中心分拣:InputDispatcher 根据焦点窗口确定收件地址

  4. 通道运输:通过 InputChannel socket 对发送事件到应用

  5. 门卫签收:InputEventReceiver 接收事件,通知应用

  6. 视图处理:事件在 ViewHierarchy 中传递,从 Activity 到具体 View

  7. 特殊情况:软键盘事件由输入法处理后,再提交给文本框

整个过程就像一个高效的物流公司,从收集包裹到分拣配送,再到收件人签收,每个环节都有明确的分工和流程,确保用户的每一次操作都能准确送达目标应用。InputChannel 就像物流单号,InputDispatcher 像调度系统,共同保证了输入事件的高效分发。