一切從android的handler說起(五)之觸摸事件模型

閱讀本文大概需要 5 分鐘。

 

 

在弄清楚了handler消息機制原理後,小張顯得異常高興,感覺這塊兒終於像一碗清水似的看到底了。

 

我無意間說了一句:別高興得太早,你只清楚了一半!

小張聽了有點發懵:一半?啥意思,不都非常清晰了嗎?

 

我笑了笑,說道:現在你只知道UI線程是事件驅動模型,有事就幹,沒事就睡覺,那有沒有想過事件都從何而來?爲何我手指觸摸UI上任何一個button都能很快做出反應呢?這個觸摸事件是如何轉爲message扔到UI queue裏去的呢?

 

小張瞬間愣住了,原來之前一直在聊的都是UI線程如何被message喚醒觸發,從未考慮過message的源頭是如何來的。

 

小張緊接着又補了一句:那肯定不是UI線程自己一直在盯着,而是另外一個東西,不然UI線程又該卡成翔了。

我繼續問道:是的。那究竟是誰在負責這個輸入事件的源頭的採集呢?

 

小張只得繳械,直說不知道。

我又問道:如果是你來設計這個模型,你能大概給個想法嗎?

 

小張想了一會兒說道:

1). 首先應該有一個線程在不斷的監聽屏幕,一旦有觸摸事件,就將事件捕獲;

2). 其次,還應該存在某種手段可以找到目標窗口,因爲可能有多個APP的多個界面爲用戶可見,必須確定這個事件究竟通知那個窗口;

3). 最後纔是目標窗口如何消費事件,也就是在這一步事件被包裝成message(包含具體的觸摸點x,y座標)扔進queue中喚醒UI線程來處理。

 

並且在紙上畫出了模型圖,大概如下:

 

我看了一下,說道:沒錯,你這個設計解耦很好,各負其職。其實Android系統也是這個設計。

第1)步裏所說的負責監聽觸摸事件的是InputManagerService。

第2)步裏所說的找到目標窗口的是WindowManagerService。它倆在Android操作系統啓動時已經在System Server進程中被創建好,並被註冊到了另一個ServiceManager進程當中。

 

小張不解的問道:ServiceManager?這個東東是幹嘛的,爲什麼要有它呢?

我說道:Android系統中各種服務大概有幾十種,AMS, PMS, WMS,等等。你想想如果各種隔離的服務之間要想相互通信的話,如果沒有一箇中間者,它們相互之間如何知道彼此的存在呢?

 

小張說道:哦...是不是ServiceManager負責註冊和查詢各種service,以便提供相互間的通信,就像網絡中的DNS一樣?

我說道:沒錯,這樣各個service的功能就解耦了。其實這是一種常見的解耦手法,當大量模塊之間要相互通信時,必然會產生一箇中間協調者,也是頂級思維中的HUB思想。你想想你之前用過的都有哪些輪子使用的這種思想的?

 

小張想了想說道:有通信總線EventBus, 還有組件化中負責各個組件間的通信的ARoute。

我哈哈道:沒錯,聊着聊着咱把話題扯遠了。我們還是回到剛纔的話題,逐個看看這2個service都是如何分別幹好自己職責的吧。

 

首先我們看看InputManagerService,其實系統在初始化InputManagerService時生成了2個線程:InputReaderThread和InputDispatcherThread,看名字就知道這2個線程一個是負責讀取各種事件源,另外一個是負責把事件源派發給別人。

 

 1InputManager::InputManager(
 2        const sp<EventHubInterface>& eventHub,
 3        const sp<InputReaderPolicyInterface>& readerPolicy,
 4        const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {
 5    <!--事件分發執行類-->
 6    mDispatcher = new InputDispatcher(dispatcherPolicy);
 7    <!--事件讀取執行類-->
 8    mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
 9    initialize();
10}
11
12void InputManager::initialize() {
13    mReaderThread = new InputReaderThread(mReader);
14    mDispatcherThread = new InputDispatcherThread(mDispatcher);
15}
16
17bool InputReaderThread::threadLoop() {
18    mReader->loopOnce();
19    return true;
20}
21
22void InputReader::loopOnce() {
23        int32_t oldGeneration;
24        int32_t timeoutMillis;
25        bool inputDevicesChanged = false;
26        Vector<InputDeviceInfo> inputDevices;
27        {  
28      ...<!--監聽事件-->
29        size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
30       ....<!--處理事件-->
31           processEventsLocked(mEventBuffer, count);
32       ...
33       <!--通知派發-->
34        mQueuedListener->flush();
35    }

 

InputReaderThread中的loop通過監聽EventHub的getEvents獲取Input事件,這樣輸入事件就可以被讀取,並由processEventsLocked初步被封裝成RawEvent,最後發通知,請求派發消息。以上就解決了事件讀取問題。

 

下面我們重點來看一下事件的分發。

 

上面代碼中的InputReader的mQueuedListener其實就是InputDispatcher對象,所以mQueuedListener->flush()就是通知InputDispatcher事件讀取完畢,可以派發事件了。

 

InputDispatcherThread是一個典型Looper線程,基於native的Looper實現了Hanlder消息處理模型,如果有Input事件到來就被喚醒處理事件,處理完畢後繼續睡眠等待,看下面的代碼有種非常熟悉的感覺,有木有:

 

 1bool InputDispatcherThread::threadLoop() {
 2    mDispatcher->dispatchOnce();
 3    return true;
 4}
 5
 6void InputDispatcher::dispatchOnce() {
 7    nsecs_t nextWakeupTime = LONG_LONG_MAX;
 8    {  
 9      <!--被喚醒 ,處理Input消息-->
10        if (!haveCommandsLocked()) {
11            dispatchOnceInnerLocked(&nextWakeupTime);
12        }
13       ...
14    } 
15    nsecs_t currentTime = now();
16    int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);
17    <!--睡眠等待input事件-->
18    mLooper->pollOnce(timeoutMillis);
19}

 

我們繼續深入看看dispatchOnceInnerLocked都做了什麼,這裏以其中的觸摸事件代碼分支爲例:

 

 1void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
 2        ...
 3    case EventEntry::TYPE_MOTION: {
 4        MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);
 5        ...
 6        done = dispatchMotionLocked(currentTime, typedEntry,
 7                &dropReason, nextWakeupTime);
 8        break;
 9    }
10
11bool InputDispatcher::dispatchMotionLocked(
12        nsecs_t currentTime, MotionEntry* entry, DropReason* dropReason, nsecs_t* nextWakeupTime) {
13    ...     
14    Vector<InputTarget> inputTargets;
15    bool conflictingPointerActions = false;
16    int32_t injectionResult;
17    if (isPointerEvent) {
18    <!--關鍵點1 找到目標Window-->
19        injectionResult = findTouchedWindowTargetsLocked(currentTime,
20                entry, inputTargets, nextWakeupTime, &conflictingPointerActions);
21    } else {
22        injectionResult = findFocusedWindowTargetsLocked(currentTime,
23                entry, inputTargets, nextWakeupTime);
24    }
25    ...
26    <!--關鍵點2  派發-->
27    dispatchEventLocked(currentTime, entry, inputTargets);
28    return true;
29}

 

從以上代碼可以看出,對於觸摸事件會首先通過findTouchedWindowTargetsLocked找到目標Window,進而通過dispatchEventLocked將消息發送到目標窗口。以上的步驟可以用如下的圖來總結:

 

 

到目前爲止第1)步的職責我們已經很清楚了,接着就需要關注事件是如何被精確的命中到Android裏某個Window窗口的,這也就是WindowManagerService乾的活。

 

在Android系統中,每塊屏幕被抽象成一個DisplayContent對象,內部維護一個WindowList列表對象,用來記錄當前屏幕中的所有窗口,因此,可以根據觸摸事件的位置及窗口的屬性來確定將事件發送到哪個窗口。

 

目標窗口也找到了,接下來的問題是如何通知用戶的App目標窗口,由於這是2個不同的進程,必然涉及到IPC通信。同學們可能下意識的會想到Binder通信,畢竟Binder在Android中是使用最多的IPC手段了,不過Input事件處理這裏採用的卻不是Binder:高版本的採用的都是Socket的通信方式,而比較舊的版本採用的是Pipe管道的方式。

 

我們就說socket這種方式,那麼這個Socket是怎麼來的呢?其實還是要牽扯到WindowManagerService,在APP端向WMS請求添加窗口的時候,會伴隨着Input通道的創建,窗口的添加而創建(具體代碼略過)。

 

而APP端的監聽消息的手段是:將socket添加到Looper線程的epoll數組中去,一有消息到來Looper線程就會被喚醒,並獲取事件內容。

 

這樣一來,整個鏈條就打通了,一步一步通過事件的讀取,派發,尋找目標窗口,IPC通知目標窗口,把觸摸事件包裝成message進入UI線程的message,激活UI線程的消息機制進行事件處理。

 

整個流程總結起來,就如下圖:

 

圖片來源於網絡

 

 

注:這篇文章絕大部分想法來源於“看書的小蝸牛”,我只是做了加工處理,並非100%原創。在此聲明和感謝!

更詳細的,請見https://www.jianshu.com/p/f05d6b05ba17

 


有熱愛Android技術的同學,歡迎加 QQ羣 726464410,或者掃描QQ羣二維碼 和 微信公衆號二維碼。用詼諧的方式學習Android硬核知識點。

                                                   

                                                                     歡迎轉發,關注公衆號

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章