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

從手指接觸屏幕到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)

僅供參考,歡迎指正

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,224評論 6 529
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 97,916評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,014評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,466評論 1 308
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,245評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,795評論 1 320
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,869評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,010評論 0 285
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,524評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,487評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,634評論 1 366
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,173評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,884評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,282評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,541評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,236評論 3 388
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,623評論 2 370