美文网首页Android开发Android开发Android技术知识
十分钟让你了解Android触摸事件原理(InputManage

十分钟让你了解Android触摸事件原理(InputManage

作者: 06fd4cf1f427 | 来源:发表于2019-07-22 21:20 被阅读16次

    从手指接触屏幕到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的模型就是如下样式:

    InputReadermQueuedListener其实就是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端被唤醒
    • 找到目标窗口处理事件

    好啦,文章写到这里就结束了,如果你觉得文章写得不错就给个赞呗?如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足。谢谢。

    希望读到这的您能转发分享和关注一下我,以后还会更新技术干货,谢谢您的支持!

    转发+点赞+关注,第一时间获取最新知识点

    Android架构师之路很漫长,一起共勉吧!


    以下墙裂推荐阅读!!!

    相关文章

      网友评论

        本文标题:十分钟让你了解Android触摸事件原理(InputManage

        本文链接:https://www.haomeiwen.com/subject/tyuglctx.html