输入事件原理
安卓输入事件整体流程
Android 系统是由事件驱动的,而 input 是最常见的事件之一,用户的点击、滑动、长按等操作,都属于 input 事件驱动,其中的核心就是 InputReader 和 InputDispatcher。
InputReader 和 InputDispatcher 是跑在 SystemServer进程中的两个 native 循环线程,负责读取和分发 Input 事件。整个处理过程大致流程如下:
InputManagerService
在SystemServer的startOtherServices函数中,会新建一个InputManagerService对象,然后会作为参数传入到WMS中去。
inputManager = new InputManagerService(context);
......
wm = WindowManagerService.main(context, inputManager, !mFirstBoot, mOnlyCore,
new PhoneWindowManager(), mActivityManagerService.mActivityTaskManager);
WindowInputEventReceiver输入事件监听
在ViewRootImpl的setView函数中,会先建立一个InputChannel对象。inputChannel是输入事件的信道,它ViewRootImpl和InputManagerService的沟通桥梁。
InputChannel inputChannel = null;
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
inputChannel = new InputChannel();
}
inputChannel会通过mWindowSession.addToDisplayAsUser方法传入到WMS侧,并且与当前新建窗口WindowState建立起关系
if (openInputChannels) {
//WindowState与InputChannel关联上了
win.openInputChannel(outInputChannel);
}
之后代码运行到ViewRootImpl侧,这个时候就开始生成监听对象
if (inputChannel != null) {
if (mInputQueueCallback != null) {
mInputQueue = new InputQueue();
mInputQueueCallback.onInputQueueCreated(mInputQueue);
}
// 输入事件的监听对象
mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
Looper.myLooper());
}
}
EventHub设备事件监听
它主要是利用Linux的inotify和epoll机制,监听设备事件:包括设备插拔及各种触摸、按钮事件等,可以看做是一个不同设备的集线器,主要面向的是/dev/input目录下的设备节点,比如说/dev/input/event0上的事件就是输入事件:
InputReader读取事件
InputReader主要是负责不断的从EventHub读取事件,通知派发给InputDispatcher。
void InputReader::loopOnce() {
int32_t oldGeneration;
int32_t timeoutMillis;
bool inputDevicesChanged = false;
std::vector<InputDeviceInfo> inputDevices;
{
......
// 读取EventHub中的事件
size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
......
// 通知派发给InputDispatcher
mQueuedListener->flush();
}
InputDispatcher读取事件
void InputDispatcher::dispatchOnce() {
nsecs_t nextWakeupTime = LONG_LONG_MAX;
{ // acquire lock
......
// 被唤醒 ,处理Input消息
if (runCommandsLockedInterruptible()) {
nextWakeupTime = LONG_LONG_MIN;
}
.....
int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);
//睡眠等到事件
mLooper->pollOnce(timeoutMillis);
}
寻找目标窗口
Android系统能够同时支持多块屏幕,每块屏幕被抽象成一个DisplayContent对象,内部维护一个WindowList列表对象,用来记录当前屏幕中的所有窗口,包括状态栏、导航栏、应用窗口、子窗口等。对于触摸事件,我们比较关心可见窗口
那么,如何找到触摸事件对应的窗口呢,是状态栏、导航栏还是应用窗口呢,这个时候DisplayContent的WindowList就发挥作用了,DisplayContent握着所有窗口的信息,因此,可以根据触摸事件的位置及窗口的属性来确定将事件发送到哪个窗口,当然其中的细节比一句话复杂的多,跟窗口的状态、透明、分屏等信息都有关系,下面简单瞅一眼,达到主观理解的流程就可以了,然后会调用到InputDispatcher#findTouchedWindowAtLocked
// 遍历所有窗口
for (const sp<InputWindowHandle>& windowHandle : windowHandles) {
const InputWindowInfo* windowInfo = windowHandle->getInfo();
if (windowInfo->displayId == displayId) {
int32_t flags = windowInfo->layoutParamsFlags;
if (windowInfo->visible) {
if (!(flags & InputWindowInfo::FLAG_NOT_TOUCHABLE)) {
bool isTouchModal = (flags &
(InputWindowInfo::FLAG_NOT_FOCUSABLE |
InputWindowInfo::FLAG_NOT_TOUCH_MODAL)) == 0;
if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) {
int32_t portalToDisplayId = windowInfo->portalToDisplayId;
......
// Found window. 找到目标窗口
return windowHandle;
}
}
}
}
mWindowHandles代表着所有窗口,找到了目标窗口,同时也将事件封装好了,剩下的就是通知目标窗口,可是有个最明显的问题就是,目前所有的逻辑都是在SystemServer进程,而要通知的窗口位于APP端的用户进程,那么如何通知呢?这里面采用的都是Socket的通信方式。