WeTest 導讀
當我們在寫帶有UI的程序的時候,如果想獲取輸入事件,僅僅是寫一個回調函數,比如(onKeyEvent,onTouchEvent….),輸入事件有可能來自按鍵的,來自觸摸的,也有來自鍵盤的,其實軟鍵盤也是一種獨立的輸入事件。那么為什么我能通過回調函數獲取這些輸入事件呢?系統是如何精確的讓程序獲得輸入事件並去響應的呢?為什么系統只能同一時間有一個界面去獲得觸摸事件呢? 下面我們通過Android系統輸入子系統的分析來回答這些問題。
一、輸入事件的轉發流程

二、物理設備是如何將輸入數據發送給內核的
物理設備將數據發送給內核是通過設備驅動傳輸的,在linux下的/dev/input/目錄下有幾個設備文件,event0,event1,event2……… 這些設備文件實際上是驅動創建的,他們共用一個主設備號,僅僅是次設備號不同,表示這是一類設備。比如觸摸屏對應event0,觸摸屏驅動被掛載后,驅動程序會進行初始化,主要是初始化CPU引腳,設置中斷處理程序。

當按下觸摸屏的時候觸摸屏有個引腳電平變低了,相連的CPU引腳檢查到這個連接的引腳電壓變低了,那么就會觸發中斷,這個在觸摸驅動中初始化好的,CPU有個中斷向量表,這里就到了我們驅動中寫好的中斷處理函數,中斷處理函數中就會讀取觸摸屏的數據,就是通過相連接的引腳組成的二進制數據比如(01011010),這個時候我們的內核就拿到的觸摸屏的數據。
觸摸屏芯片的時序圖
三、內核是如何把輸入數據發送給用戶空間Android framework的
內核拿到觸摸屏的數據后,經過平滑處理,濾波,數據還是在內核空間,那么Android怎么拿到觸摸數據呢? Android實際上是運行在linux內核上一組進程,這一組進程組合為用戶提供UI,應用程序的安裝等等服務。

手機開機流程是linux內核先啟動,啟動完成之后會將Android進程組啟動起來,FrameWork屬於這個進程組之中。Framewok中有個服務InputManagerService,我們看Android源碼它在哪里實例化的:
SystemServer.java----------->
startOtherServices()------>
/*構造InputManagerService*/
inputManager = new InputManagerService(context);
/*將inputManager傳遞給WindowManagerService去
wm=WindowManagerService.main(context, inputManager,
mFactoryTestMode !=FactoryTest.FACTORY_TEST_LOW_LEVE !mFirstBoot, mOnlyCore);
/*給InputManagerService設置回調*/
inputManager.setWindowManagerCallbacks(wm.getInputMonitor());
/* 全初始化好后,SystemServer調用start()函數讓InputManager中兩個線程開始運行。先看InputReaderThread,它是事件在用戶態處理過程的起點*/
inputManager.start();
所以可以看到它在SystemServer進程中實例化並且啟動,所以我們首先需要看看InputManagerService的構造函數都做了什么?
構造函數會調用到jni創建NativeInputManager的c++對象, NativeInputManager構造函數中創建
Sp<EventHub> eventHub = new EventHub()
mInputManager = new InputManager(eventhub,this,this);
eventHub對象構造函數做了下面幾件事情:
1. 創建epoll對象,之后就可以把各個輸入設備的fd添加進來多路等待輸入事件
2. 利用inotify機制監聽/dev/input目錄下的變更,如果有則意味着設備變換,需要處理,輸入設備的增減刪除操作的監聽,將代表inotify的fd添加到epoll中
3. 創建pipe,管道只能用來在具有公共祖先的兩個之間通信.讀端添加epoll中
InputManager對象構造函數做了下面幾件事:
1. 創建InputDispatcher
2. 創建InputReader(eventhub,inputdispatcher),InputDispatcher繼承InputListenerInterface
3. 創建InputReaderThread
4. 創建InputDispatcherThread
我們還記得最SystemServer.java中最后通過inputManager.start(); 來運行我們的InputManagerService,所以繼續看start方法,實際上在native層的inputManager對象中,將上面創建的兩個線程InputReaderThread和InputDispatcherThread的start方法中。
對於InputReaderThread的start方法:
1. 調用構造函數中保存的eventHub的getEvents方法獲取input事件,在getEvent方法中做的事
1)判斷是不是需要打開input設備驅動,如果需要打開設備驅動,掃描/dev/input目錄下的設備文件並打開這些設備,同時會判斷設備列表中有沒有虛擬鍵盤,沒有的話就創建一個device添加進去
2)到下一步中至少系統存在兩個輸入設備,一個是觸摸屏,一個是虛擬鍵盤,因為上面這次getEvent的調用需要打開設備,所有就將這些動作封裝成RawEvent事件,這里兩個DEVICE_ADDED事件+FINISH_DEVICE_SCAN事件,將這些事件返回,不會往下走了
3)如果第二次進入getEvents方法中就會等待讀取輸入事件,將讀取的touch事件發送返回
到這里我們就知道了內核空間的觸摸輸入數據是如何傳遞到了用戶空間的Android framework中的,實際上就是通過/dev/input目錄下,去掃描這個目錄,如果有device就打開這個device ,並添加到epoll對象中,多路等待輸入事件,在loop中獲取數據。
四、Android framework是怎樣將輸入數據發送給APP進程的
Android framework獲取了觸摸輸入的數據,但是在系統中有那么多進程,那么多進程都在獲取輸入,它是如何進一步處理,准確的分發事件的呢?
InputReaderThread的start方法中做的第二件事情:
調用processEventsLocked方法處理上面的getEvents方法返回的的RawEvent
1)根據RawEvent的類型不同,調用不同的方法處理,有 ● 普通的touch事件
.● 添加設備的事件
.● 刪除設備的事件
.● FiNISHED_DEVICE_SCAN
2)對於touch事件: 調用這個touch事件對應的輸入設備(之間創建的InputDevice)的process方法,該方法內部調用內部的InputMapper的process方法,一個輸入設備有很多個Mapper,遍歷所有的Mapper,並調用process,假定我們是一個支持多點觸摸的touch screen,它的mapper是MultiTouchInputMapper,調用它的process方法。
3)MultiTouchInputMapper的process方法內部會這樣處理:
首先每次一個touchEvent獲取Slot,在沒有收到EV_SYN之前對應的Slot都是相同的,然后依次處理x,y,pressure,touch_major,這些值初始化slot的各個變量;
當收到ev.type== EV_SYN並且ev.code = SYN_MT_REPORT那么當前的slot的index加1,給下一次觸摸事件去記錄,同時sync函數處理這次觸摸事件;
然后CurrentCookedPointerData和LastCookedPointerData進行一些列的操作,up,down還是move事件,然后對應的不同事件,調用dispatchMotion,內部調用InputDispatcher的notifyMotion
4)對於InputDispatcher的notifyMotion:
● 如果InputDispatcher設置了inputFilter,那么首先調用inputFilter來消費這些事件
● 如果沒有inputFiler,或者inputFilter對這些事件不感興趣,那么就會構造一個MotionEntry,添加到mInboundQueue,並喚醒InputDispatcher線程處理
5)對於InputDispatcher的線程處理循環:
● 優化app切換延遲,當切換超時,則搶占分發,丟棄其他所有即將要處理的事件;
● 分發事件:
首先調用findTouchedWindowTagetsLocked尋找有focus的window窗口, 並把這些創建保存在inputTargets數組中;
之前注冊的monitor的InputChannel這里也會添加到inputTargets數組中;
然后向inputTargets數組一一分發事件。
到這里我們就知道了是如何找到這個APP進程的了。
五、APP進程是如何將輸入數據發送給它對應的Activity的
Activity是一個進程的基本組件,可以認為它代表了一個界面,是一堆View的集合,每次Activity啟動的時候都做了什么呢?
1、實際上取決於它背后的ViewRootImpl做了什么,在ViewRootImpl.java中的setView方法中,實例化InputChannel,當然會判斷當前的窗口能不能接受輸入事件,接着在調用到session.java中的addToDisplay方法傳遞給WindowManagerService,實際上是調用WindowManagerService的addWindow方法,在WindowManagerService中會創建一對InputChannel[],然后InputChannel[1]轉移到這個inputChannel, 然后setView方法繼續創建一個WindowInputEventReceiver對象,然后將上面創建好的InputChannel
2、WindowManagerService中的addWindow方法:
InputChannel[] inputChannels = InputChannel.openInputChannelPair(name)
/*Channel[0]保存在server端*/
win.setInputChannel[inputChannels[0]]
/* Channel[1]返回給ViewRootImpl端*/
inputChannels[1].transferTo(outInputChannel)
/*注冊到inputManagerService中*/
mInputManager.registerInputChannel(win.mInputChannel,win.mInputWindowHandle)
到這里我們就能明白如何將時間分發給對應的Activity了,其實是給了它背后的ViewRootImpl。
六、Activity又是如何將輸入數據發送給具體的View的
最后一步就是將事件分發到Activity中具體的View了,從ViewRootImpl中將事件分發給具體的View,很好理解,因為觸摸的范圍在到這里是知道的,每個View的位置以及狀態到這里也是知道的,因為View要正確渲染的話,Android圖形框架會搞定這一切,測量每個View的大小,確定每個View的位置,ViewRootImpl會一層一層將數據分發到自己每個View中,但是每個View自己知道這個觸摸事件是不是作用在自己身上的,如果不是就丟棄了,往下面分發。
總結
觸摸事件的分發流程看起來挺復雜,但是Android實現的還是很優雅的,我們去分析它的流程,對於我們想實現一些比較的酷的功能是有幫助的。當然對於我們調試代碼也會有幫助,當發現觸摸后,系統無響應,將上面的流程分解,總是能分析出原因。
騰訊WeTest提供上千台真實手機,隨時隨地進行測試,保障應用/手游品質。節省百萬硬件費用,加速敏捷研發流程。
同時騰訊WeTest兼容性測試團隊積累了10年的手游測試經驗,旨在通過制定針對性的測試方案,精准選取目標機型,執行專業、完整的測試用例,來提前發現游戲版本的兼容性問題,針對性地做出修正和優化,來保障手游產品的質量。目前該團隊已經支持所有騰訊在研和運營的手游項目。
歡迎進入:http://wetest.qq.com/product/cloudphone 體驗安卓真機
歡迎進入:http://wetest.qq.com/product/expert-compatibility-testing 使用專家兼容測試服務。WeTest兼容性測試團隊期待與您交流!You Create,We Test!
如果對使用當中有任何疑問,歡迎聯系騰訊WeTest企業QQ:800024531