[Framework] 理解 Android 中的 Input 事件

[Framework] 理解 Android 中的 Input 事件

在 Andorid 中的 Input 事件包括屏幕点击、按键点击(包括软硬键盘、三大金刚按键等等)和鼠标(没错 Android 是支持外置鼠标的,只是一般应用开发用不上),通常用户做了 Input 的操作后我们的应用作为反应会触发 UI 的重新绘制和一些其他的业务逻辑,在这个过程中对用户的体验非常重要,经常在这个过程也会出现卡顿和 ANR,通过了解 Input 事件可以更加深入的理解卡顿和 ANR

InputManagerService

我们的输入事件都是由 IMS 管理的,它和 AMS 一样工作在 system_server 进程。 IMS 在初始化的过程中会初始化以下几个对象 EventHubInputReaderInputDispatcher。 他们的工作方式在一定的程度上非常像 Handler 的工作方式,EventHub 对应 MessageQueueInputReader 对应 LooperInputDispatcher 对应 Handler,如果不熟悉 Handler 可以参考我之前写的文章:Android Handler 工作原理

EventHub

InputReader 会通过 EventHub#getEvents() 方法去获取事件,如果没有事件就会阻塞(这里和 Handler 一样,InputReader 也是一个死循环无限获取,没有就阻塞,也是通过 epoll 机制唤醒)。 EventHub 会监听 /dev/input 的文件来获取事件,获取到的事件是input_event 对象,它会把它转换成 RawEvent (其中包含 deviceId,用来标识是哪个 input 文件的事件)对象,最后把这个事件返回给 InputReader

InputReader

在前面说到 InputReader 会循环调用 Event#getEvents() 方法去获取 Input 事件,返回的对象是 RawEventInputReader 会根据 RawEvent 中的 deviceId 去检查本地是否已经有创建 InputDevice 对象,如果没有就会根据 deviceId 去创建对应的 InputDevice 对象,在 InputDevice 中会创建一个非常重要的 Mapper 对象,他们是 RawEvent 事件的翻译官,需要用他们来解释 RawEvent 中的数据,其中有 KeyboardInputMapper(键盘)、CursorInputMapper(鼠标)、MultiTouchInputMapper(屏幕多点触控) 和 SingleTouchInputMapper(屏幕单点触控)。

InputDevice 中的 Mapper 会把 RawEvent 对象转换成 NotifyArgs 对象 (其中的数据已经是被翻译过的了,例如键盘输入事件就有 keyCode了,触摸事件就有 xy 了,主要有以下几类 NotifyKeyArgs, NotifyMotionArgs, NotifyDeviceResetArgs, NotifySwitchArgs, - NotifyConfigurationChangedArgs)。翻译过的 NotifyArgs 对象,会被放在 mArgsQueue 队列里面等待处理。

处理 NotifyArgs 事件对象时,会通过 mPolicy 对象的 interceptKeyBeforeQueueing() 方法和 filterInputEvent() 方法去修改这个事件对象和拦截这个事件对象,默认他们的实现都是空,最终这个方法会调用到 IMS 对应的方法中去。NotifyArgs 对象会被转换成 EventKeyEvent, MotionEvent) 对象,Event 又会被转成 EventEntry(KeyEventEntryMotionEventEntry) 存放在 mInboundQueue 队列中,然后唤醒(也是通过 epoll 唤醒) InputDispatcher 去处理 mInboundQueue 中的数据。

InputDispatcher

InputDispatcherInputReader 一样他们工作都是一个死循环,InputDispatcher 一直在等待 InputReader 读到 EventEntry 后去通知它,收到通知后就会去读 InputReader#mInboundQueue 中的事件,在这里还会重制 ANR 信息 (后面写文章单独讲 ANR),然后判断该事件是否应该丢弃,如果丢弃流程就结束了,如果不丢弃就检查是否有一个有焦点的 Window 来处理它,如果有就会把这个 Window 添加到 inputTargets 中,这个 Window 中有一个非常重要的对象 inputChannel,它就是用来和应用层通信的对象。然后在 inputChannel 中找到一个可用的 connection,然后把 InputReader#mInboundQueue 中的事件读取到 outBoundQueue (会把 EventEntry 转换成 DispatchEntry) 中,然后通过 connection 发送至应用进程,把这个已经发送的事件还会添加到 waitQueue 中,表示还在等待应用进程回复的事件。这个 connection 的通信方式是 Socket,那么它是什么时候创建的呢?这就要看 Window 的添加流程了。

事件通信对象 InputChannel 的创建

我在 聊聊 Android Activity 中的 Token 文章中有聊到这个过程,在应用层所有的 UI 显示最终都会调用到 ViewRootImpl#setView() 方法,在这个方法中会调用由 WMS 生成的 SessionaddToDisplay() 方法去请求添加 Window

Java 复制代码
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        // ...
        if ((mWindowAttributes.inputFeatures
        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
        mInputChannel = new InputChannel(); 
    }

        res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
            getHostVisibility(), mDisplay.getDisplayId(),
            mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
            mAttachInfo.mOutsets, mInputChannel);
        // ...
        if (mInputChannel != null) {
            if (mInputQueueCallback != null) {
                mInputQueue = new InputQueue();
                mInputQueueCallback.onInputQueueCreated(mInputQueue);
            }

            mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
            Looper.myLooper());
        }
    }
}

其中 mInputChannel 就是在 InputDispatcher 中提到的 inputChanneladdToDisplay() 方法调用完成后会通过 mInputChannel 构建一个 WindowInputEventReceiver 对象用来处理 IMS 传递过来的事件。

我们在来看看 WMS 的处理:

Java 复制代码
final class Session extends IWindowSession.Stub implements IBinder.DeathRecipient {

    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, InputChannel outInputChannel) {
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId,
            outContentInsets, outStableInsets, outOutsets, outInputChannel);
    }
}
Java 复制代码
public int addWindow(Session session, IWindow client, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, Rect outStableInsets, Rect outOutsets, InputChannel outInputChannel) {
    // ...
    WindowState win = new WindowState(this, session, client, token,
        attachedWindow, appOp[0], seq, attrs, viewVisibility, displayContent);

    if (outInputChannel != null && (attrs.inputFeatures
    & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
        //根据WindowState的HashCode以及title来生成InputChannel名称
        String name = win.makeInputChannelName();

        //创建一对InputChannel
        InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
        //将socket服务端保存到WindowState的mInputChannel
        win.setInputChannel(inputChannels[0]);

        //socket客户端传递给outInputChannel
        inputChannels[1].transferTo(outInputChannel);
        //利用socket服务端作为参数
        mInputManager.registerInputChannel(win.mInputChannel, win.mInputWindowHandle);
    }
    // ...
    boolean focusChanged = false;
    if (win.canReceiveKeys()) {
        //新添加window能接收按下操作,则更新聚焦窗口。
        focusChanged = updateFocusedWindowLocked(UPDATE_FOCUS_WILL_ASSIGN_LAYERS,
            false /*updateInputWindows*/);
    }
    ...

    if (focusChanged) {
        mInputMonitor.setInputFocusLw(mCurrentFocus, false /*updateInputWindows*/);
    }
    //设置当前聚焦窗口
    mInputMonitor.updateInputWindowsLw(false /*force*/);
}

这里会创建一个 InputChannel 对,服务端通过 IMS#registerInputChannel 传递给 IMS,客户端通过 binder 传递给应用进程。

应用进程处理 Input 事件

在讲 ViewRootImpl#setView() 的时候说到 Session#addToDisplay() 方法调用完成后会用创建好的 inputChannel 构建一个 WindowInputReceiver 对象来接收 IMS 传递来的事件,其中回调的方法是 onInputEvent()

Java 复制代码
@Override
public void onInputEvent(InputEvent event, int displayId) {
    enqueueInputEvent(event, this, 0, true);
}
Java 复制代码
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
    // ...

    if (processImmediately) {
        doProcessInputEvents();
    } else {
        scheduleProcessInputEvents();
    }
}
Java 复制代码
void doProcessInputEvents() {
    // Deliver all pending input events in the queue.
    while (mPendingInputEventHead != null) {
        // ...

        deliverInputEvent(q);
    }

    // We are done processing all input events that we can process right now
    // so we can clear the pending flag immediately.
    if (mProcessInputEventsScheduled) {
        mProcessInputEventsScheduled = false;
        mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS);
    }
}
Java 复制代码
private void deliverInputEvent(QueuedInputEvent q) {
    // ...

    if (stage != null) {
        handleWindowFocusChanged();
        stage.deliver(q);
    } else {
        finishInputEvent(q);
    }
}

最终事件下发会调到 deliverInputEvent() 方法,这个 state 就是处理任务的地方,如果 state 为空就表示,已经处理完毕,调用 finishInputEvent() 方法,最终会通过 inputChannel 通知 IMS,如果超过一定的时间没有处理完成也就会出现 ANR。(后续文章再讨论这个问题)

其中上面描述的 state 有以下几种:

Java 复制代码
// ...
mSyntheticInputStage = new SyntheticInputStage();
InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
"aq:native-post-ime:" + counterSuffix);
InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
InputStage imeStage = new ImeInputStage(earlyPostImeStage,
"aq:ime:" + counterSuffix);
InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
"aq:native-pre-ime:" + counterSuffix);
// ...

其中 ViewPostImeInputState 就是下发我们 view 的触摸事件,对应的方法如下:

Java 复制代码
private int processPointerEvent(QueuedInputEvent q) {
    final MotionEvent event = (MotionEvent)q.mEvent;

    mAttachInfo.mUnbufferedDispatchRequested = false;
    mAttachInfo.mHandlingPointerEvent = true;
    boolean handled = mView.dispatchPointerEvent(event);
    maybeUpdatePointerIcon(event);
    maybeUpdateTooltip(event);
    mAttachInfo.mHandlingPointerEvent = false;
    if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
        mUnbufferedInputDispatch = true;
        if (mConsumeBatchedInputScheduled) {
            scheduleConsumeBatchedInputImmediately();
        }
    }
    return handled ? FINISH_HANDLED : FORWARD;
}

这个 mView 也就是 DecorView,然后通过 dispatchPointerEvent() 方法把,触摸事件传递到整个 ViewTree,直到有 View 处理。

参考文章

Input系统---启动篇
Input系统---InputReader线程
Input系统---InputDispatcher线程
Input系统---UI线程

相关推荐
Estar.Lee2 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
温辉_xh2 小时前
uiautomator案例
android
工业甲酰苯胺3 小时前
MySQL 主从复制之多线程复制
android·mysql·adb
少说多做3433 小时前
Android 不同情况下使用 runOnUiThread
android·java
Estar.Lee5 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
找藉口是失败者的习惯5 小时前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey6 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!8 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
天空中的野鸟9 小时前
Android音频采集
android·音视频
小白也想学C10 小时前
Android 功耗分析(底层篇)
android·功耗