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

看書的小蝸牛發表於2017-12-07

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

Android觸控事件模型

觸控事件肯定要先捕獲才能傳給視窗,因此,首先應該有一個執行緒在不斷的監聽螢幕,一旦有觸控事件,就將事件捕獲;其次,還應該存在某種手段可以找到目標視窗,因為可能有多個APP的多個介面為使用者可見,必須確定這個事件究竟通知那個視窗;最後才是目標視窗如何消費事件的問題。

觸控事件模型.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如何完成觸控事件的採集。

如何捕獲觸控事件

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就可以監聽並獲取該事件:

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

事件的派發

在新建InputManager的時候,不僅僅建立了一個事件讀取執行緒,還建立了一個事件派發執行緒,雖然也可以直接在讀取執行緒中派發,但是這樣肯定會增加耗時,不利於事件的及時讀取,因此,事件讀取完畢後,直接向派發執行緒發個通知,請派發執行緒去處理,這樣讀取執行緒就可以更加敏捷,防止事件丟失,因此InputManager的模型就是如下樣式:

InputManager模型.jpg

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

更新視窗邏輯.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)

僅供參考,歡迎指正

相關文章