Android触摸事件原理(InputManagerService)

  • Post author:
  • Post category:其他


从手指接触屏幕到MotionEvent被传送到Activity或者View,中间究竟经历了什么?Android中触摸事件到底是怎么来的呢?源头是哪呢?本文就直观的描述一个整个流程,不求甚解,只求了解。



Android触摸事件模型

触摸事件肯定要先捕获才能传给窗口,因此,首先应该有一个线程在不断的监听屏幕,一旦有触摸事件,就将事件捕获;其次,还应该存在某种手段可以找到目标窗口,因为可能有多个APP的多个界面为用户可见,必须确定这个事件究竟通知那个窗口;最后才是目标窗口如何消费事件的问题。

InputManagerService是Android为了处理各种用户操作而抽象的一个服务,自身可以看做是一个Binder服务实体,在SystemServer进程启动的时候实例化,并注册到ServiceManager中去,不过这个服务对外主要是用来提供一些输入设备的信息的作用,作为Binder服务的作用比较小:

private void startOtherServices() {
    ...
    inputManager = new InputManagerService(context);
    wm = WindowManagerService.main(context, 
        inputManager,mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL,
        !mFirstBoot, mOnlyCore);
    ServiceManager.addService(Context.WINDOW_SERVICE, wm);
    ServiceManager.addService(Context.INPUT_SERVICE, inputManager);
    ...
}

InputManagerService跟WindowManagerService几乎同时被添加,从一定程度上也能说明两者几乎是相生的关系,而触摸事件的处理也确实同时涉及两个服务,最好的证据就是WindowManagerService需要直接握着InputManagerService的引用,如果对照上面的处理模型,InputManagerService主要负责触摸事件的采集,而WindowManagerService负责找到目标窗口。接下来,先看看InputManagerService如何完成触摸事件的采集。



如何捕获触摸事件

InputManagerService会单独开一个线程专门用来读取触摸事件,

NativeInputManager::NativeInputManager(jobject contextObj,jobject serviceObj, 
    const sp<Looper>& looper) :mLooper(looper), mInteractive(true) 
{
    ...
    sp<EventHub> eventHub = new EventHub();
    mInputManager = new InputManager(eventHub, this, this);
}

这里有个EventHub,它主要是利用Linux的inotify和epoll机制,监听设备事件:包括设备插拔及各种触摸、按钮事件等,可以看做是一个不同设备的集线器,主要面向的是/dev/input目录下的设备节点,比如说/dev/input/event0上的事件就是输入事件,通过EventHub的getEvents就可以监听并获取该事件:

在new InputManager时候,会新建一个InputReader对象及InputReaderThread Loop线程,这个loop线程的主要作用就是通过EventHub的getEvents获取Input事件

InputManager::InputManager(const sp<EventHubInterface>& eventHub,
    const sp<InputReaderPolicyInterface>& readerPolicy,
    const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) 
{
    <!--事件分发执行类-->
    mDispatcher = new InputDispatcher(dispatcherPolicy);
    <!--事件读取执行类-->
    mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
    initialize();
}

void InputManager::initialize() {
    mReaderThread = new InputReaderThread(mReader);
    mDispatcherThread = new InputDispatcherThread(mDispatcher);
}

bool InputReaderThread::threadLoop() {
    mReader->loopOnce();
    return true;
}

void InputReader::loopOnce() {
    int32_t oldGeneration;
    int32_t timeoutMillis;
    bool inputDevicesChanged = false;
    Vector<InputDeviceInfo> inputDevices;
    { 
        ...
        <!--监听事件-->
        size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
        ....
        <!--处理事件-->
        processEventsLocked(mEventBuffer, count);
        ...
        <!--通知派发-->
        mQueuedListener->flush();
    }
}

通过上面流程,输入事件就可以被读取,经过processEventsLocked被初步封装成RawEvent,最后发通知,请求派发消息。以上就解决了事件读取问题,下面重点来看一下事件的分发。



事件的派发

在新建InputManager的时候,不仅仅创建了一个事件读取线程,还创建了一个事件派发线程,虽然也可以直接在读取线程中派发,但是这样肯定会增加耗时,不利于事件的及时读取,因此,事件读取完毕后,直接向派发线程发个通知,请派发线程去处理,这样读取线程就可以更加敏捷,防止事件丢失,因此InputManager的模型就是如下样式:

InputReader的mQueuedListener其实就是InputDispatcher对象,所以mQueuedListener->flush()就是通知InputDispatcher事件读取完毕,可以派发事件了, InputDispatcherThread是一个典型Looper线程,基于native的Looper实现了Hanlder消息处理模型,如果有Input事件到来就被唤醒处理事件,处理完毕后继续睡眠等待,简化代码如下:

bool InputDispatcherThread::threadLoop() {
    mDispatcher->dispatchOnce();
    return true;
}
void InputDispatcher::dispatchOnce() {
    nsecs_t nextWakeupTime = LONG_LONG_MAX;
    { 
        <!--被唤醒 ,处理Input消息-->
        if (!haveCommandsLocked()) {
            dispatchOnceInnerLocked(&nextWakeupTime);
        }
        ...
    } 
    nsecs_t currentTime = now();
    int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);
    <!--睡眠等待input事件-->
    mLooper->pollOnce(timeoutMillis);
}

以上就是派发线程的模型,dispatchOnceInnerLocked是具体的派发处理逻辑,这里看其中一个分支,触摸事件:

void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
    ...
    case EventEntry::TYPE_MOTION: {
        MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);
        ...
        done = dispatchMotionLocked(currentTime, typedEntry,&dropReason, nextWakeupTime);
        break;
    }
}

bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, MotionEntry* entry,
    DropReason* dropReason, nsecs_t* nextWakeupTime) {
    ...
    Vector<InputTarget> inputTargets;
    bool conflictingPointerActions = false;
    int32_t injectionResult;
    if (isPointerEvent) {
        <!--关键点1 找到目标Window-->
        injectionResult = findTouchedWindowTargetsLocked(currentTime,entry, inputTargets,     
            nextWakeupTime, &conflictingPointerActions);
    } 
    else {
        injectionResult = findFocusedWindowTargetsLocked(currentTime,entry, inputTargets,
            nextWakeupTime);
    }
    ...
    <!--关键点2 派发-->
    dispatchEventLocked(currentTime, entry, inputTargets);
    return true;
}

从以上代码可以看出,对于触摸事件会首先通过findTouchedWindowTargetsLocked找到目标Window,进而通过dispatchEventLocked将消息发送到目标窗口,下面看一下如何找到目标窗口,以及这个窗口列表是如何维护的。



如何为触摸事件找到目标窗口

Android系统能够同时支持多块屏幕,每块屏幕被抽象成一个DisplayContent对象,内部维护一个WindowList列表对象,用来记录当前屏幕中的所有窗口,包括状态栏、导航栏、应用窗口、子窗口等。对于触摸事件,我们比较关心可见窗口,用adb shell dumpsys SurfaceFlinger看一下可见窗口的组织形式:

那么,如何找到触摸事件对应的窗口呢,是状态栏、导航栏还是应用窗口呢,这个时候DisplayContent的WindowList就发挥作用了,DisplayContent握着所有窗口的信息,因此,可以

根据触摸事件的位置及窗口的属性来确定将事件发送到哪个窗口

,当然其中的细节比一句话复杂的多,跟窗口的状态、透明、分屏等信息都有关系,下面简单瞅一眼,达到主观理解的流程就可以了,

int32_t InputDispatcher::findTouchedWindowTargetsLocked(nsecs_t currentTime,
    const MotionEntry* entry, Vector<InputTarget>& inputTargets, 
    nsecs_t* nextWakeupTime,bool* outConflictingPointerActions) 
{
    ...
    sp<InputWindowHandle> newTouchedWindowHandle;
    bool isTouchModal = false;
    <!--遍历所有窗口-->
    size_t numWindows = mWindowHandles.size();
    for (size_t i = 0; i < numWindows; i++) {
        sp<InputWindowHandle> windowHandle = mWindowHandles.itemAt(i);
        const InputWindowInfo* windowInfo = windowHandle->getInfo();
        if (windowInfo->displayId != displayId) {
            continue; // wrong display
        }
        int32_t flags = windowInfo->layoutParamsFlags;
        if (windowInfo->visible) {
            if (! (flags & InputWindowInfo::FLAG_NOT_TOUCHABLE)) {
                isTouchModal = (flags & (InputWindowInfo::FLAG_NOT_FOCUSABLE| 
                    InputWindowInfo::FLAG_NOT_TOUCH_MODAL)) == 0;
                <!--找到目标窗口-->
                if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) {
                    newTouchedWindowHandle = windowHandle;
                    break; 
                    // found touched window, exit window loop
                }
            }
            ...

mWindowHandles代表着所有窗口,findTouchedWindowTargetsLocked的就是从mWindowHandles中找到目标窗口,规则太复杂,总之就是根据点击位置更窗口Z order之类的特性去确定,有兴趣可以自行分析。不过这里需要关心的是mWindowHandles,它就是是怎么来的,另外窗口增删的时候如何保持最新的呢?这里就牵扯到跟WindowManagerService交互的问题了,mWindowHandles的值是在InputDispatcher::setInputWindows中设置的,

void InputDispatcher::setInputWindows(
    const Vector<sp<InputWindowHandle> >& inputWindowHandles) {
        ...
        mWindowHandles = inputWindowHandles;
        ...

谁会调用这个函数呢? 真正的入口是WindowManagerService中的InputMonitor会简介调用InputDispatcher::setInputWindows,这个时机主要是跟窗口增改删除等逻辑相关,以addWindow为例:

从上面流程可以理解为什么说WindowManagerService跟InputManagerService是相辅相成的了,到这里,如何找到目标窗口已经解决了,下面就是如何将事件发送到目标窗口的问题了。



如何将事件发送到目标窗口

找到了目标窗口,同时也将事件封装好了,剩下的就是通知目标窗口,可是有个最明显的问题就是,目前所有的逻辑都是在SystemServer进程,而要通知的窗口位于APP端的用户进程,那么如何通知呢?下意识的可能会想到Binder通信,毕竟Binder在Android中是使用最多的IPC手段了,不过Input事件处理这采用的却不是Binder:

高版本的采用的都是Socket的通信方式,而比较旧的版本采用的是Pipe管道的方式

void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,EventEntry* eventEntry, 
    const Vector<InputTarget>& inputTargets) 
{
    pokeUserActivityLocked(eventEntry);
    for (size_t i = 0; i < inputTargets.size(); i++) {
        const InputTarget& inputTarget = inputTargets.itemAt(i);
        ssize_t connectionIndex = getConnectionIndexLocked(inputTarget.inputChannel);
        if (connectionIndex >= 0) {
            sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);
            prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget);
        } else {
        }
    }
}

代码逐层往下看会发现最后会调用到InputChannel的sendMessage函数,最会通过socket发送到APP端(Socket怎么来的接下来会分析),

这个Socket是怎么来的呢?或者说两端通信的一对Socket是怎么来的呢?其实还是要牵扯到WindowManagerService,在APP端向WMS请求添加窗口的时候,会伴随着Input通道的创建,窗口的添加一定会调用ViewRootImpl的setView函数:

ViewRootImpl

public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView)
{
    ...
    requestLayout();
    if ((mWindowAttributes.inputFeatures& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
        <!--创建InputChannel容器-->
        mInputChannel = new InputChannel();
    }
    try {
        mOrigWindowType = mWindowAttributes.type;
        mAttachInfo.mRecomputeGlobalAttributes = true;
        collectViewAttributes();
        <!--添加窗口,并请求开辟Socket Input通信通道-->
        res = mWindowSession.addToDisplay(mWindow, mSeq,     
            mWindowAttributes,getHostVisibility(), 
            mDisplay.getDisplayId(),mAttachInfo.mContentInsets, 
            mAttachInfo.mStableInsets,mAttachInfo.mOutsets, mInputChannel);
    }
    ...
    <!--监听,开启Input信道-->
    if (mInputChannel != null) {
        if (mInputQueueCallback != null) {
            mInputQueue = new InputQueue();
            mInputQueueCallback.onInputQueueCreated(mInputQueue);
        }
        mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,Looper.myLooper());
    }

在IWindowSession.aidl定义中 InputChannel是out类型,也就是说需要服务端进行填充,那么接着看服务端WMS如何填充的呢?

public int addWindow(Session session, IWindow client, int seq,WindowManager.LayoutParams attrs, 
    int viewVisibility,int displayId,Rect outContentInsets, Rect outStableInsets, 
    Rect outOutsets,InputChannel outInputChannel) 
{ 
    ...
    if (outInputChannel != null && 
        (attrs.inputFeatures& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) 
    {
        String name = win.makeInputChannelName();
        <!--关键点1创建通信信道 -->
        InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
        <!--本地用-->
        win.setInputChannel(inputChannels[0]);
        <!--APP端用-->
        inputChannels[1].transferTo(outInputChannel);
        <!--注册信道与窗口-->
        mInputManager.registerInputChannel(win.mInputChannel, win.mInputWindowHandle);
    }

WMS首先创建socketpair作为全双工通道,并分别填充到Client与Server的InputChannel中去;之后让InputManager将Input通信信道与当前的窗口ID绑定,这样就能知道哪个窗口用哪个信道通信了;最后通过Binder将outInputChannel回传到APP端,下面是SocketPair的创建代码:

status_t InputChannel::openInputChannelPair(const String8& name,
    sp<InputChannel>& outServerChannel, sp<InputChannel>& outClientChannel) 
{
    int sockets[2];
    if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) {
        status_t result = -errno;
        ...
        return result;
    }
    int bufferSize = SOCKET_BUFFER_SIZE;
    setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &bufferSize,
    sizeof(bufferSize));setsockopt(sockets[0], SOL_SOCKET, SO_RCVBUF, &bufferSize,      
    sizeof(bufferSize));setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF, &bufferSize, 
    sizeof(bufferSize));setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &bufferSize, 
    sizeof(bufferSize));
    <!--填充到server inputchannel-->
    String8 serverChannelName = name;
    serverChannelName.append(" (server)");
    outServerChannel = new InputChannel(serverChannelName, sockets[0]);
    <!--填充到client inputchannel-->
    String8 clientChannelName = name;
    clientChannelName.append(" (client)");
    outClientChannel = new InputChannel(clientChannelName, sockets[1]);
    return OK;
}

这里socketpair的创建与访问其实是还是借助文件描述符,

WMS需要借助Binder通信向APP端回传文件描述符fd

,这部分只是可以参考Binder知识,主要是在内核层面实现两个进程fd的转换,窗口添加成功后,socketpair被创建,被传递到了APP端,但是信道并未完全建立,因为还需要一个主动的监听,毕竟消息到来是需要通知的,先看一下信道模型


APP端的监听消息的手段是:将socket添加到Looper线程的epoll数组中去

,一有消息到来Looper线程就会被唤醒,并获取事件内容,从代码上来看,通信信道的打开是伴随WindowInputEventReceiver的创建来完成的。

信息到来,Looper根据fd找到对应的监听器:NativeInputEventReceiver,并调用handleEvent处理对应事件

int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) 
{
    ...
    if (events & ALOOPER_EVENT_INPUT) {
        JNIEnv* env = AndroidRuntime::getJNIEnv();
        status_t status = consumeEvents(env, false /*consumeBatches*/, -1, NULL);
        mMessageQueue->raiseAndClearException(env, "handleReceiveCallback");
        return status == OK || status == NO_MEMORY ? 1 : 0;
    }
    ...

之后会进一步读取事件,并封装成Java层对象,传递给Java层,进行相应的回调处理:

status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env, 
    bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) 
{ 
    ...
    for (;;) 
    { 
        uint32_t seq; 
        InputEvent* inputEvent; 
        <!--获取事件-->
        status_t status = mInputConsumer.consume(&mInputEventFactory, 
            consumeBatches, frameTime, &seq, &inputEvent); 
        ...
        <!--处理touch事件-->
        case AINPUT_EVENT_TYPE_MOTION: {
            MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);
            if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch{
                *outConsumedBatch = true;
            }
            inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
            break;
        } 
        <!--回调处理函数-->
        if (inputEventObj) {
            env->CallVoidMethod(receiverObj.get(),
                gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
            env->DeleteLocalRef(inputEventObj);
        }

所以最后就是触摸事件被封装成了inputEvent,并通过InputEventReceiver的dispatchInputEvent(WindowInputEventReceiver)进行处理,这里就返回到我们常见的Java世界了。



目标窗口中的事件处理

最后简单看一下事件的处理流程,Activity或者Dialog等是如何获得Touch事件的呢?如何处理的呢?直白的说就是将监听事件交给ViewRootImpl中的rootView,让它自己去负责完成事件的消费,究竟最后被哪个View消费了要看具体实现了,而对于Activity与Dialog中的DecorView重写了View的事件分配函数dispatchTouchEvent,将事件处理交给了CallBack对象处理,至于View及ViewGroup的消费,算View自身的逻辑了。



总结

现在把所有的流程跟模块串联起来,流程大致如下:

  • 点击屏幕
  • InputManagerService的Read线程捕获事件,预处理后发送给Dispatcher线程
  • Dispatcher找到目标窗口
  • 通过Socket将事件发送到目标窗口
  • APP端被唤醒
  • 找到目标窗口处理事件