十分鐘瞭解Android觸摸事件原理(InputManagerService)

從手指接觸屏幕到MotionEvent被傳送到Activity或者View,中間究竟經歷了什麼?Android中觸摸事件究竟是怎麼來的呢?源頭是哪呢?本文就直觀的描述一個整個流程,不求甚解,只求瞭解。android

Android觸摸事件模型

觸摸事件確定要先捕獲才能傳給窗口,所以,首先應該有一個線程在不斷的監聽屏幕,一旦有觸摸事件,就將事件捕獲;其次,還應該存在某種手段能夠找到目標窗口,由於可能有多個APP的多個界面爲用戶可見,必須肯定這個事件究竟通知那個窗口;最後纔是目標窗口如何消費事件的問題。shell

觸摸事件模型.jpg

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如何完成觸摸事件的採集。session

如何捕獲觸摸事件

InputManagerService會單獨開一個線程專門用來讀取觸摸事件,app

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就能夠監聽並獲取該事件:socket

EventHub模型.jpg

在new InputManager時候,會新建一個InputReader對象及InputReaderThread Loop線程,這個loop線程的主要做用就是經過EventHub的getEvents獲取Input事件函數

InputRead線程啓動流程

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,最後發通知,請求派發消息。以上就解決了事件讀取問題,下面重點來看一下事件的分發。oop

事件的派發

在新建InputManager的時候,不只僅建立了一個事件讀取線程,還建立了一個事件派發線程,雖然也能夠直接在讀取線程中派發,可是這樣確定會增長耗時,不利於事件的及時讀取,所以,事件讀取完畢後,直接向派發線程發個通知,請派發線程去處理,這樣讀取線程就能夠更加敏捷,防止事件丟失,所以InputManager的模型就是以下樣式:ui

InputManager模型.jpg

InputReader的mQueuedListener其實就是InputDispatcher對象,因此mQueuedListener->flush()就是通知InputDispatcher事件讀取完畢,能夠派發事件了, InputDispatcherThread是一個典型Looper線程,基於native的Looper實現了Hanlder消息處理模型,若是有Input事件到來就被喚醒處理事件,處理完畢後繼續睡眠等待,簡化代碼以下:this

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爲例:

更新窗口邏輯.png

從上面流程能夠理解爲何說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怎麼來的接下來會分析),

send流程.png

這個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端,可是信道並未徹底創建,由於還須要一個主動的監聽,畢竟消息到來是須要通知的,先看一下信道模型

InputChannl信道.jpg

APP端的監聽消息的手段是:將socket添加到Looper線程的epoll數組中去,一有消息到來Looper線程就會被喚醒,並獲取事件內容,從代碼上來看,通訊信道的打開是伴隨WindowInputEventReceiver的建立來完成的。

fd打開通訊信道.png

信息到來,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自身的邏輯了。

APP端事件處理流程

總結

如今把全部的流程跟模塊串聯起來,流程大體以下:

  • 點擊屏幕
  • InputManagerService的Read線程捕獲事件,預處理後發送給Dispatcher線程
  • Dispatcher找到目標窗口
  • 經過Socket將事件發送到目標窗口
  • APP端被喚醒
  • 找到目標窗口處理事件

InputManager完整模型.jpg

做者:看書的小蝸牛 十分鐘瞭解Android觸摸事件原理(InputManagerService)

僅供參考,歡迎指正

相關文章
相關標籤/搜索