您当前的位置:首页 > 电脑百科 > 程序开发 > 移动端 > Android

十分钟让你了解Android触摸事件原理(InputManagerService)

时间:2019-08-07 11:25:23  来源:  作者:

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

Android触摸事件模型

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

十分钟让你了解Android触摸事件原理(InputManagerService)

 

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就可以监听并获取该事件:

十分钟让你了解Android触摸事件原理(InputManagerService)

 

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

十分钟让你了解Android触摸事件原理(InputManagerService)

 

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的模型就是如下样式:

十分钟让你了解Android触摸事件原理(InputManagerService)

 

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看一下可见窗口的组织形式:

十分钟让你了解Android触摸事件原理(InputManagerService)

 

那么,如何找到触摸事件对应的窗口呢,是状态栏、导航栏还是应用窗口呢,这个时候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为例:

十分钟让你了解Android触摸事件原理(InputManagerService)

 

从上面流程可以理解为什么说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怎么来的接下来会分析),

十分钟让你了解Android触摸事件原理(InputManagerService)

 

这个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端,但是信道并未完全建立,因为还需要一个主动的监听,毕竟消息到来是需要通知的,先看一下信道模型

十分钟让你了解Android触摸事件原理(InputManagerService)

 

APP端的监听消息的手段是:将socket添加到Looper线程的epoll数组中去,一有消息到来Looper线程就会被唤醒,并获取事件内容,从代码上来看,通信信道的打开是伴随WindowInputEventReceiver的创建来完成的。

十分钟让你了解Android触摸事件原理(InputManagerService)

 

信息到来,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自身的逻辑了。

十分钟让你了解Android触摸事件原理(InputManagerService)

 

总结

现在把所有的流程跟模块串联起来,流程大致如下:

点击屏幕

InputManagerService的Read线程捕获事件,预处理后发送给Dispatcher线程

Dispatcher找到目标窗口

通过Socket将事件发送到目标窗口

APP端被唤醒

找到目标窗口处理事件

如果你看到了这,点个赞,收下我双膝。如果文章有误,帮忙指正,谢谢大佬们。



Tags:Android 触摸事件原理   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
从手指接触屏幕到MotionEvent被传送到Activity或者View,中间究竟经历了什么?Android中触摸事件到底是怎么来的呢?源头是哪呢?本文就直观的描述一个整个流程,不求甚解,只求了解。An...【详细内容】
2019-08-07  Tags: Android 触摸事件原理  点击:(263)  评论:(0)  加入收藏
▌简易百科推荐
今天面试遇到同学说做过内存优化,于是我一般都会问那 Bitmap 的像素内存存在哪?大多数同学都回答在 java heap 里面,就比较尴尬,理论上你做内存优化,如果连图片这个内存大户内存...【详细内容】
2021-12-23  像程序那样思考    Tags:Android开发   点击:(8)  评论:(0)  加入收藏
Android logcat日志封装logcat痛点在Android开发中使用logcat非常频繁,logcat能帮我们定位问题,但是在日常使用中发现每次使用都需要传递tag,并且会遇到输出频率很高的log,在多...【详细内容】
2021-12-22  YuCoding    Tags:Android   点击:(8)  评论:(0)  加入收藏
对项目的基本介绍 1.整个框架主要是给MVVM框架使用的,自己写完interface接口后,通过自定义的注解就能自动生成接口方法 2.用Kotlin的Flow去代替Rxjava,因为我发现RxJava功能很...【详细内容】
2021-12-08  网易Leo    Tags:Android开发   点击:(17)  评论:(0)  加入收藏
前言在Android开发过程中,有些时候会根据需要引用别的项目到当前项目里面,而且以Module形式引用。所以本篇博文就来分享一下怎么以Module形式引用别的项目到当前项目中,方便开...【详细内容】
2021-12-07  网易Leo    Tags:Android开发   点击:(22)  评论:(0)  加入收藏
作者:fundroid这篇文章偏阅读一些,大家可以了解下 Android 的一些最新动向。每年9/10月份 Google 都会举行约为期2天的 Android Dev Summit,在活动上 Google 的技术专家们会分...【详细内容】
2021-11-30  像程序那样思考    Tags:Android开发   点击:(15)  评论:(0)  加入收藏
一、 准备工作1、安装JDK,下载地址(可能需要一个oracle账号,大家百度一下或者自行注册一个就行。尽可能选择8或者11,这两个是长期版本)Java SE | Oracle Technology Network | Or...【详细内容】
2021-11-23  永沧    Tags:Android   点击:(28)  评论:(0)  加入收藏
使用Maven Publish Plugin插件。(官方支持)一、在Library的build.gradle中配置plugins { id &#39;com.android.library&#39; id &#39;kotlin-android&#39; id &#39;k...【详细内容】
2021-11-05  羊城小阳    Tags:Android   点击:(37)  评论:(0)  加入收藏
谷歌离推出Play Store应用程序的新数据隐私部分又近了一步。应用程序开发人员现在可以通过谷歌在Play控制台的新 "数据安全表 "填写相关细节。该公司表示,所需信息将从2022年...【详细内容】
2021-10-20    中关村在线  Tags:安卓   点击:(58)  评论:(0)  加入收藏
架构究竟是什么?如何更好的理解架构?我们知道一个APP通常是由class组成,而这些class之间如何组合,相互之间又如何产生作用,就是影响这个APP的关键点。细分的话我们可以将其分为类...【详细内容】
2021-09-17  像程序那样思考    Tags:Android架构   点击:(52)  评论:(0)  加入收藏
概述当Android应用程序需要访问设备上的敏感资源时,应用程序开发人员会使用权限模型。虽然该模型使用起来非常简单,但开发人员在使用权限时容易出错,从而导致安全漏洞。本文中,...【详细内容】
2021-09-07  SecTr安全团队    Tags:Android开发   点击:(66)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条