從手指接觸屏幕到MotionEvent被傳送到Activity或者View士修,中間究竟經(jīng)歷了什么?Android中觸摸事件到底是怎么來(lái)的呢?源頭是哪呢榨乎?本文就直觀的描述一個(gè)整個(gè)流程,不求甚解瘫筐,只求了解蜜暑。
Android觸摸事件模型
觸摸事件肯定要先捕獲才能傳給窗口,因此策肝,首先應(yīng)該有一個(gè)線程在不斷的監(jiān)聽屏幕肛捍,一旦有觸摸事件,就將事件捕獲之众;其次拙毫,還應(yīng)該存在某種手段可以找到目標(biāo)窗口,因?yàn)榭赡苡卸鄠€(gè)APP的多個(gè)界面為用戶可見棺禾,必須確定這個(gè)事件究竟通知那個(gè)窗口缀蹄;最后才是目標(biāo)窗口如何消費(fèi)事件的問題。
InputManagerService是Android為了處理各種用戶操作而抽象的一個(gè)服務(wù)膘婶,自身可以看做是一個(gè)Binder服務(wù)實(shí)體缺前,在SystemServer進(jìn)程啟動(dòng)的時(shí)候?qū)嵗⒆?cè)到ServiceManager中去悬襟,不過這個(gè)服務(wù)對(duì)外主要是用來(lái)提供一些輸入設(shè)備的信息的作用衅码,作為Binder服務(wù)的作用比較小:
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幾乎同時(shí)被添加脊岳,從一定程度上也能說(shuō)明兩者幾乎是相生的關(guān)系肆良,而觸摸事件的處理也確實(shí)同時(shí)涉及兩個(gè)服務(wù),最好的證據(jù)就是WindowManagerService需要直接握著InputManagerService的引用逸绎,如果對(duì)照上面的處理模型惹恃,InputManagerService主要負(fù)責(zé)觸摸事件的采集,而WindowManagerService負(fù)責(zé)找到目標(biāo)窗口棺牧。接下來(lái)巫糙,先看看InputManagerService如何完成觸摸事件的采集。
如何捕獲觸摸事件
InputManagerService會(huì)單獨(dú)開一個(gè)線程專門用來(lái)讀取觸摸事件颊乘,
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);
}
這里有個(gè)EventHub参淹,它主要是利用Linux的inotify和epoll機(jī)制醉锄,監(jiān)聽設(shè)備事件:包括設(shè)備插拔及各種觸摸、按鈕事件等浙值,可以看做是一個(gè)不同設(shè)備的集線器恳不,主要面向的是/dev/input目錄下的設(shè)備節(jié)點(diǎn),比如說(shuō)/dev/input/event0上的事件就是輸入事件开呐,通過EventHub的getEvents就可以監(jiān)聽并獲取該事件:
在new InputManager時(shí)候烟勋,會(huì)新建一個(gè)InputReader對(duì)象及InputReaderThread Loop線程,這個(gè)loop線程的主要作用就是通過EventHub的getEvents獲取Input事件
InputManager::InputManager(
const sp<EventHubInterface>& eventHub,
const sp<InputReaderPolicyInterface>& readerPolicy,
const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {
<!--事件分發(fā)執(zhí)行類-->
mDispatcher = new InputDispatcher(dispatcherPolicy);
<!--事件讀取執(zhí)行類-->
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;
{
...<!--監(jiān)聽事件-->
size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
....<!--處理事件-->
processEventsLocked(mEventBuffer, count);
...
<!--通知派發(fā)-->
mQueuedListener->flush();
}
通過上面流程筐付,輸入事件就可以被讀取卵惦,經(jīng)過processEventsLocked被初步封裝成RawEvent,最后發(fā)通知瓦戚,請(qǐng)求派發(fā)消息沮尿。以上就解決了事件讀取問題,下面重點(diǎn)來(lái)看一下事件的分發(fā)较解。
事件的派發(fā)
在新建InputManager的時(shí)候畜疾,不僅僅創(chuàng)建了一個(gè)事件讀取線程,還創(chuàng)建了一個(gè)事件派發(fā)線程印衔,雖然也可以直接在讀取線程中派發(fā)庸疾,但是這樣肯定會(huì)增加耗時(shí),不利于事件的及時(shí)讀取当编,因此,事件讀取完畢后徒溪,直接向派發(fā)線程發(fā)個(gè)通知忿偷,請(qǐng)派發(fā)線程去處理,這樣讀取線程就可以更加敏捷臊泌,防止事件丟失鲤桥,因此InputManager的模型就是如下樣式:
InputReader的mQueuedListener其實(shí)就是InputDispatcher對(duì)象,所以mQueuedListener->flush()就是通知InputDispatcher事件讀取完畢渠概,可以派發(fā)事件了, InputDispatcherThread是一個(gè)典型Looper線程茶凳,基于native的Looper實(shí)現(xiàn)了Hanlder消息處理模型,如果有Input事件到來(lái)就被喚醒處理事件播揪,處理完畢后繼續(xù)睡眠等待贮喧,簡(jiǎn)化代碼如下:
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);
}
以上就是派發(fā)線程的模型猪狈,dispatchOnceInnerLocked是具體的派發(fā)處理邏輯箱沦,這里看其中一個(gè)分支,觸摸事件:
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) {
<!--關(guān)鍵點(diǎn)1 找到目標(biāo)Window-->
injectionResult = findTouchedWindowTargetsLocked(currentTime,
entry, inputTargets, nextWakeupTime, &conflictingPointerActions);
} else {
injectionResult = findFocusedWindowTargetsLocked(currentTime,
entry, inputTargets, nextWakeupTime);
}
...
<!--關(guān)鍵點(diǎn)2 派發(fā)-->
dispatchEventLocked(currentTime, entry, inputTargets);
return true;
}
從以上代碼可以看出雇庙,對(duì)于觸摸事件會(huì)首先通過findTouchedWindowTargetsLocked找到目標(biāo)Window谓形,進(jìn)而通過dispatchEventLocked將消息發(fā)送到目標(biāo)窗口灶伊,下面看一下如何找到目標(biāo)窗口,以及這個(gè)窗口列表是如何維護(hù)的寒跳。
如何為觸摸事件找到目標(biāo)窗口
Android系統(tǒng)能夠同時(shí)支持多塊屏幕聘萨,每塊屏幕被抽象成一個(gè)DisplayContent對(duì)象,內(nèi)部維護(hù)一個(gè)WindowList列表對(duì)象童太,用來(lái)記錄當(dāng)前屏幕中的所有窗口米辐,包括狀態(tài)欄、導(dǎo)航欄康愤、應(yīng)用窗口儡循、子窗口等。對(duì)于觸摸事件征冷,我們比較關(guān)心可見窗口择膝,用adb shell dumpsys SurfaceFlinger看一下可見窗口的組織形式:
那么,如何找到觸摸事件對(duì)應(yīng)的窗口呢检激,是狀態(tài)欄肴捉、導(dǎo)航欄還是應(yīng)用窗口呢,這個(gè)時(shí)候DisplayContent的WindowList就發(fā)揮作用了叔收,DisplayContent握著所有窗口的信息齿穗,因此,可以根據(jù)觸摸事件的位置及窗口的屬性來(lái)確定將事件發(fā)送到哪個(gè)窗口饺律,當(dāng)然其中的細(xì)節(jié)比一句話復(fù)雜的多窃页,跟窗口的狀態(tài)、透明复濒、分屏等信息都有關(guān)系脖卖,下面簡(jiǎn)單瞅一眼,達(dá)到主觀理解的流程就可以了巧颈,
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;
<!--找到目標(biāo)窗口-->
if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) {
newTouchedWindowHandle = windowHandle;
break; // found touched window, exit window loop
}
}
...
mWindowHandles代表著所有窗口畦木,findTouchedWindowTargetsLocked的就是從mWindowHandles中找到目標(biāo)窗口,規(guī)則太復(fù)雜砸泛,總之就是根據(jù)點(diǎn)擊位置更窗口Z order之類的特性去確定十籍,有興趣可以自行分析。不過這里需要關(guān)心的是mWindowHandles唇礁,它就是是怎么來(lái)的勾栗,另外窗口增刪的時(shí)候如何保持最新的呢?這里就牽扯到跟WindowManagerService交互的問題了盏筐,mWindowHandles的值是在InputDispatcher::setInputWindows中設(shè)置的械姻,
void InputDispatcher::setInputWindows(const Vector<sp<InputWindowHandle> >& inputWindowHandles) {
...
mWindowHandles = inputWindowHandles;
...
誰(shuí)會(huì)調(diào)用這個(gè)函數(shù)呢? 真正的入口是WindowManagerService中的InputMonitor會(huì)簡(jiǎn)介調(diào)用InputDispatcher::setInputWindows,這個(gè)時(shí)機(jī)主要是跟窗口增改刪除等邏輯相關(guān)楷拳,以addWindow為例:
從上面流程可以理解為什么說(shuō)WindowManagerService跟InputManagerService是相輔相成的了绣夺,到這里,如何找到目標(biāo)窗口已經(jīng)解決了欢揖,下面就是如何將事件發(fā)送到目標(biāo)窗口的問題了陶耍。
如何將事件發(fā)送到目標(biāo)窗口
找到了目標(biāo)窗口,同時(shí)也將事件封裝好了她混,剩下的就是通知目標(biāo)窗口烈钞,可是有個(gè)最明顯的問題就是,目前所有的邏輯都是在SystemServer進(jìn)程坤按,而要通知的窗口位于APP端的用戶進(jìn)程毯欣,那么如何通知呢?下意識(shí)的可能會(huì)想到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 {
}
}
}
代碼逐層往下看會(huì)發(fā)現(xiàn)最后會(huì)調(diào)用到InputChannel的sendMessage函數(shù)砚作,最會(huì)通過socket發(fā)送到APP端(Socket怎么來(lái)的接下來(lái)會(huì)分析),
這個(gè)Socket是怎么來(lái)的呢嘹锁?或者說(shuō)兩端通信的一對(duì)Socket是怎么來(lái)的呢葫录?其實(shí)還是要牽扯到WindowManagerService,在APP端向WMS請(qǐng)求添加窗口的時(shí)候领猾,會(huì)伴隨著Input通道的創(chuàng)建米同,窗口的添加一定會(huì)調(diào)用ViewRootImpl的setView函數(shù):
ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
requestLayout();
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
<!--創(chuàng)建InputChannel容器-->
mInputChannel = new InputChannel();
}
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
<!--添加窗口,并請(qǐng)求開辟Socket Input通信通道-->
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
}...
<!--監(jiān)聽摔竿,開啟Input信道-->
if (mInputChannel != null) {
if (mInputQueueCallback != null) {
mInputQueue = new InputQueue();
mInputQueueCallback.onInputQueueCreated(mInputQueue);
}
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
}
在IWindowSession.aidl定義中 InputChannel是out類型面粮,也就是說(shuō)需要服務(wù)端進(jìn)行填充,那么接著看服務(wù)端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();
<!--關(guān)鍵點(diǎn)1創(chuàng)建通信信道 -->
InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
<!--本地用-->
win.setInputChannel(inputChannels[0]);
<!--APP端用-->
inputChannels[1].transferTo(outInputChannel);
<!--注冊(cè)信道與窗口-->
mInputManager.registerInputChannel(win.mInputChannel, win.mInputWindowHandle);
}
WMS首先創(chuàng)建socketpair作為全雙工通道,并分別填充到Client與Server的InputChannel中去韭山;之后讓InputManager將Input通信信道與當(dāng)前的窗口ID綁定郁季,這樣就能知道哪個(gè)窗口用哪個(gè)信道通信了;最后通過Binder將outInputChannel回傳到APP端钱磅,下面是SocketPair的創(chuàng)建代碼:
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的創(chuàng)建與訪問其實(shí)是還是借助文件描述符梦裂,WMS需要借助Binder通信向APP端回傳文件描述符fd,這部分只是可以參考Binder知識(shí)盖淡,主要是在內(nèi)核層面實(shí)現(xiàn)兩個(gè)進(jìn)程fd的轉(zhuǎn)換年柠,窗口添加成功后,socketpair被創(chuàng)建褪迟,被傳遞到了APP端冗恨,但是信道并未完全建立答憔,因?yàn)檫€需要一個(gè)主動(dòng)的監(jiān)聽,畢竟消息到來(lái)是需要通知的掀抹,先看一下信道模型
APP端的監(jiān)聽消息的手段是:將socket添加到Looper線程的epoll數(shù)組中去虐拓,一有消息到來(lái)Looper線程就會(huì)被喚醒,并獲取事件內(nèi)容傲武,從代碼上來(lái)看蓉驹,通信信道的打開是伴隨WindowInputEventReceiver的創(chuàng)建來(lái)完成的。
信息到來(lái)揪利,Looper根據(jù)fd找到對(duì)應(yīng)的監(jiān)聽器:NativeInputEventReceiver态兴,并調(diào)用handleEvent處理對(duì)應(yīng)事件
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;
}
...
之后會(huì)進(jìn)一步讀取事件,并封裝成Java層對(duì)象疟位,傳遞給Java層瞻润,進(jìn)行相應(yīng)的回調(diào)處理:
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;
}
<!--回調(diào)處理函數(shù)-->
if (inputEventObj) {
env->CallVoidMethod(receiverObj.get(),
gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
env->DeleteLocalRef(inputEventObj);
}
所以最后就是觸摸事件被封裝成了inputEvent,并通過InputEventReceiver的dispatchInputEvent(WindowInputEventReceiver)進(jìn)行處理献汗,這里就返回到我們常見的Java世界了敢订。
目標(biāo)窗口中的事件處理
最后簡(jiǎn)單看一下事件的處理流程,Activity或者Dialog等是如何獲得Touch事件的呢罢吃?如何處理的呢楚午?直白的說(shuō)就是將監(jiān)聽事件交給ViewRootImpl中的rootView,讓它自己去負(fù)責(zé)完成事件的消費(fèi)尿招,究竟最后被哪個(gè)View消費(fèi)了要看具體實(shí)現(xiàn)了矾柜,而對(duì)于Activity與Dialog中的DecorView重寫了View的事件分配函數(shù)dispatchTouchEvent,將事件處理交給了CallBack對(duì)象處理就谜,至于View及ViewGroup的消費(fèi)怪蔑,算View自身的邏輯了。
總結(jié)
現(xiàn)在把所有的流程跟模塊串聯(lián)起來(lái)丧荐,流程大致如下:
- 點(diǎn)擊屏幕
- InputManagerService的Read線程捕獲事件缆瓣,預(yù)處理后發(fā)送給Dispatcher線程
- Dispatcher找到目標(biāo)窗口
- 通過Socket將事件發(fā)送到目標(biāo)窗口
- APP端被喚醒
- 找到目標(biāo)窗口處理事件
作者:看書的小蝸牛
十分鐘了解Android觸摸事件原理(InputManagerService)
僅供參考,歡迎指正