安卓

首页 » 常识 » 诊断 » 十分钟了解Android触摸事件原理I
TUhjnbcbe - 2024/5/18 16:13:00

从手指接触屏幕到MotionEvent被传送到Activity或者View,中间究竟经历了什么?Android中触摸事件到底是怎么来的呢?源头是哪呢?本文就直观的描述一个整个流程,不求甚解,只求了解。

Android触摸事件模型

触摸事件肯定要先捕获才能传给窗口,因此,首先应该有一个线程在不断的监听屏幕,一旦有触摸事件,就将事件捕获;其次,还应该存在某种手段可以找到目标窗口,因为可能有多个APP的多个界面为用户可见,必须确定这个事件究竟通知那个窗口;最后才是目标窗口如何消费事件的问题。

触摸事件模型.jpg

InputManagerService是Android为了处理各种用户操作而抽象的一个服务,自身可以看作是一个Binder服务实体,在SystemServer进程启动的时候实例化,并注册到ServiceManager中去,不过这个服务对外主要是用来提供一些输入设备的信息的作用,作为Binder服务的作用比较小:

privatevoidstartOtherServices(){...inputManager=newInputManagerService(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(jobjectcontextObj,jobjectserviceObj,constspLooperlooper):mLooper(looper),mInteractive(true){...spEventHubeventHub=newEventHub();mInputManager=newInputManager(eventHub,this,this);}

这里有个EventHub,它主要是利用Linux的inotify和epoll机制,监听设备事件:包括设备插拔及各种触摸、按钮事件等,可以看作是一个不同设备的集线器,主要面向的是/dev/input目录下的设备节点,比如说/dev/input/event0上的事件就是输入事件,通过EventHub的getEvents就可以监听并获取该事件:

EventHub模型.jpg

在newInputManager时候,会新建一个InputReader对象及InputReaderThreadLoop线程,这个loop线程的主要作用就是通过EventHub的getEvents获取Input事件

InputRead线程启动流程

InputManager::InputManager(constspEventHubInterfaceeventHub,constspInputReaderPolicyInterfacereaderPolicy,constspInputDispatcherPolicyInterfacedispatcherPolicy){!--事件分发执行类--mDispatcher=newInputDispatcher(dispatcherPolicy);!--事件读取执行类--mReader=newInputReader(eventHub,readerPolicy,mDispatcher);initialize();}voidInputManager::initialize(){mReaderThread=newInputReaderThread(mReader);mDispatcherThread=newInputDispatcherThread(mDispatcher);}boolInputReaderThread::threadLoop(){mReader-loopOnce();returntrue;}voidInputReader::loopOnce(){int32_toldGeneration;int32_ttimeoutMillis;boolinputDevicesChanged=false;VectorInputDeviceInfoinputDevices;{...!--监听事件--size_tcount=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事件到来就被唤醒处理事件,处理完毕后继续睡眠等待,简化代码如下:

boolInputDispatcherThread::threadLoop(){mDispatcher-dispatchOnce();}voidInputDispatcher::dispatchOnce(){nsecs_tnextWakeupTime=LONG_LONG_MAX;{!--被唤醒,处理Input消息--if(!haveCommandsLocked()){dispatchOnceInnerLocked(nextWakeupTime);}...}nsecs_tcurrentTime=now();inttimeoutMillis=toMillisecondTimeoutDelay(currentTime,nextWakeupTime);!--睡眠等待input事件--mLooper-pollOnce(timeoutMillis);}

以上就是派发线程的模型,dispatchOnceInnerLocked是具体的派发处理逻辑,这里看其中一个分支,触摸事件:

voidInputDispatcher::dispatchOnceInnerLocked(nsecs_t*nextWakeupTime){...caseEventEntry::TYPE_MOTION:{MotionEntry*typedEntry=static_castMotionEntry*(mPendingEvent);...done=dispatchMotionLocked(currentTime,typedEntry,dropReason,nextWakeupTime);break;}boolInputDispatcher::dispatchMotionLocked(nsecs_tcurrentTime,MotionEntry*entry,DropReason*dropReason,nsecs_t*nextWakeupTime){...VectorInputTargetinputTargets;boolconflictingPointerActions=false;int32_tinjectionResult;if(isPointerEvent){!--关键点1找到目标Window--injectionResult=findTouchedWindowTargetsLocked(currentTime,entry,inputTargets,nextWakeupTime,);}else{injectionResult=findFocusedWindowTargetsLocked(currentTime,entry,inputTargets,nextWakeupTime);}...!--关键点2派发--dispatchEventLocked(currentTime,entry,inputTargets);returntrue;}

从以上代码可以看出,对于触摸事件会首先通过findTouchedWindowTargetsLocked找到目标Window,进而通过dispatchEventLocked将消息发送到目标窗口,下面看一下如何找到目标窗口,以及这个窗口列表是如何维护的。

如何为触摸事件找到目标窗口

Android系统能够同时支持多块屏幕,每块屏幕被抽象成一个DisplayContent对象,内部维护一个WindowList列表对象,用来记录当前屏幕中的所有窗口,包括状态栏、导航栏、应用窗口、子窗口等。对于触摸事件,我们比较关心可见窗口,用adbshelldumpsysSurfaceFlinger看一下可见窗口的组织形式:

焦点窗口

那么,如何找到触摸事件对应的窗口呢,是状态栏、导航栏还是应用窗口呢,这个时候DisplayContent的WindowList就发挥作用了,DisplayContent握着所有窗口的信息,因此,可以根据触摸事件的位置及窗口的属性来确定将事件发送到哪个窗口,当然其中的细节比一句话复杂的多,跟窗口的状态、透明、分屏等信息都有关系,下面简单瞅一眼,达到主观理解的流程就可以了,

int32_tInputDispatcher::(nsecs_tcurrentTime,constMotionEntry*entry,VectorInputTargetinputTargets,nsecs_t*nextWakeupTime,bool*outConflictingPointerActions){...spInputWindowHandlenewTouchedWindowHandle;boolisTouchModal=false;!--遍历所有窗口--size_tnumWindows=mWindowHandles.size();for(size_ti=0;inumWindows;i++){spInputWindowHandlewindowHandle=mWindowHandles.itemAt(i);constInputWindowInfo*windowInfo=windowHandle-getInfo();if(windowInfo-displayId!=displayId){continue;//wrongdisplay}int32_tflags=windowInfo-layoutParamsFlags;if(windowInfo-visible){if(!(flagsInputWindowInfo::FLAG_NOT_TOUCHABLE)){isTouchModal=(flags(InputWindowInfo::FLAG_NOT_FOCUSABLE

InputWindowInfo::FLAG_NOT_TOUCH_MODAL))==0;!--找到目标窗口--if(isTouchModal

windowInfo-touchableRegionContainsPoint(x,y)){=windowHandle;break;//foundtouchedwindow,exitwindowloop}}...

mWindowHandles代表着所有窗口,findTouchedWindowTargetsLocked的就是从mWindowHandles中找到目标窗口,规则太复杂,总之就是根据点击位置更窗口Zorder之类的特性去确定,有兴趣可以自行分析。不过这里需要关心的是mWindowHandles,它就是是怎么来的,另外窗口增删的时候如何保持最新的呢?这里就牵扯到跟WindowManagerService交互的问题了,mWindowHandles的值是在InputDispatcher::setInputWindows中设置的,

voidInputDispatcher::setInputWindows(constVectorspInputWindowHandleinputWindowHandles){...mWindowHandles=inputWindowHandles;...

谁会调用这个函数呢?真正的入口是WindowManagerService中的InputMonitor会简介调用InputDispatcher::setInputWindows,这个时机主要是跟窗口增改删除等逻辑相关,以addWindow为例:

更新窗口逻辑.png

从上面流程可以理解为什么说WindowManagerService跟InputManagerService是相辅相成的了,到这里,如何找到目标窗口已经解决了,下面就是如何将事件发送到目标窗口的问题了。

如何将事件发送到目标窗口

找到了目标窗口,同时也将事件封装好了,剩下的就是通知目标窗口,可是有个最明显的问题就是,目前所有的逻辑都是在SystemServer进程,而要通知的窗口位于APP端的用户进程,那么如何通知呢?下意识的可能会想到Binder通信,毕竟Binder在Android中是使用最多的IPC手段了,不过Input事件处理这采用的却不是Binder:高版本的采用的都是Socket的通信方式,而比较旧的版本采用的是Pipe管道的方式。

voidInputDispatcher::dispatchEventLocked(nsecs_tcurrentTime,EventEntry*eventEntry,constVectorInputTargetinputTargets){pokeUserActivityLocked(eventEntry);for(size_ti=0;iinputTargets.size();i++){constInputTargetinputTarget=inputTargets.itemAt(i);ssize_tconnectionIndex=getConnectionIndexLocked(inputTarget.inputChannel);if(connectionIndex=0){spConnectionconnection=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

publicvoidsetView(Viewview,WindowManager.LayoutParamsattrs,ViewpanelParentView){...requestLayout();if((mWindowAttributes.inputFeaturesWindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL)==0){!--创建InputChannel容器--mInputChannel=newInputChannel();}try{mOrigWindowType=mWindowAttributes.type;mAttachInfo.mRe

1
查看完整版本: 十分钟了解Android触摸事件原理I