https://www.jianshu.com/p/f05d6b05ba17

手指触摸屏幕和运动事件传输到活动或视图之间发生了什么?Android中的触控事件是怎么来的?出处在哪里?本文直观地描述了一个完整的过程,不求解决,只求理解。

安卓触摸事件模型

触摸事件必须在传输到窗口之前被捕获。所以,首先要有一个线程在不停的监控屏幕,一旦出现触摸事件,事件就会被捕捉;其次,要有一些手段找到目标窗口,因为可能会有多个用户可见的多个应用的多个界面,需要确定这个事件通知哪个窗口;最后,问题是目标窗口如何消耗事件。

触摸事件模型

InputManagerService是Android处理各种用户操作的抽象服务。它可以看作是一个Binder服务实体,在启动SystemServer进程并在ServiceManager中注册时进行实例化。但是,该服务主要用于提供输入设备的一些信息,其作为Binder服务的作用相对较小:

privatevoidstartOtherServices{

...

input manager = new input manager service;

wm = WindowManagerService.main;

服务管理器。添加服务;

服务管理器。添加服务;

...

}

InputManagerService和WindowManagerService几乎是同时添加的,说明两者在一定程度上几乎是相互关联的,触摸事件的处理确实同时涉及到两个服务。最好的证据是WindowManagerService需要直接持有InputManagerService的引用。与上述处理模型相比,InputManagerService主要负责采集触摸事件,而WindowManagerService负责寻找目标窗口。接下来,让我们看看InputManagerService是如何完成触摸事件的收集的。

如何捕捉触摸事件

InputManagerService将打开一个单独的线程来读取触摸事件。

NativeInputManager::NativeInputManager :

mLooper,mInteractive {

...

sp<。EventHub>。eventHub = newEventHub

mInputManager = new input manager;

}

有一个EventHub,主要利用Linux的inotify和epoll机制来监控设备事件,包括设备插拔、各种触摸和按键事件等。可以看作是不同设备的枢纽,主要面向/dev/input目录下的设备节点。例如/dev/input/event0上的事件就是输入事件,可以通过EventHub的getEvents进行监控和获取:

EventHub模型

在创建新的输入管理器时,将创建一个输入管理器对象和输入线程循环线程。这个循环线程的主要功能是通过事件中心的获取事件来获取输入事件

输入线程启动进程输入管理器::输入管理器 {

& lt!-事件分发执行类->:

mDispatcher = new InputDispatcher;

& lt!-事件读取执行类->:

mrreader = new inputer;

初始化;

}

void InputManager::initialize {

mrreader thread = new InputErthread;

mdispatchethread = new inputdispatchethread;

}

bool InputArterthread::ThreadLoop {

mrreader->;loopOnce

返回true

}

void InputReader::loopOnce {

int32 _ t oldGeneration

int32 _ t timeoutMillis

bool inputDevicesChanged = false

Vector <。InputDeviceInfo >;inputDevices

{

...& lt!-监控事件->:

size _ t count = Meventhub->;getEvents;

....& lt!-处理事件->:

processeventslock;

...

& lt!-通知分发->:

mqueidlistener->;齐平;

}

通过上述过程,输入事件可以被读取,在processEventsLocked后打包成一个RawEvent,最后发送通知请求发送消息。以上解决了事件阅读的问题。让我们关注事件分布。

事件分布

创建新的输入管理器时,不仅创建了一个事件读取线程,还创建了一个事件调度线程。虽然也可以在阅读线程中直接调度,但是这样肯定会增加时间消耗,不利于事件的及时阅读。所以事件读取完成后,直接给调度线程发通知,让调度线程去处理,这样读取线程可以更加敏捷,防止事件丢失。因此,输入管理器的模型如下:

输入管理器模型

InputReader的mqueudlistener实际上是一个InputDispatcher对象,所以mqueudlistener-->:Flush是通知InputDispatcher事件在读取后可以调度。InputDispatcherThread是一个典型的Looper线程。基于本机的Looper实现了Hanlder消息处理模型。如果输入事件到达,它将被唤醒以处理该事件。处理后会继续休眠等待。简化代码如下:

bool InputDispatchethread::ThreadLoop {

mDispatcher->;dispatchOnce

返回true

}

void InputDispatcher::dispatchOnce {

nsecs _ t next WakeUptime = LONG _ LONG _ MAX;

{

& lt!-唤醒,处理输入信息->:

if {

dispatchonceinnerlock;

}

...

}

nsecs _ t currentTime = now

int time out millis = TomillissecondTimeoutelay;

& lt!-睡眠等待输入事件->:

mloop er->;poll once;

}

void inputdispatcher::dispatchonceinnerlock{

...

案例事件条目::类型_运动:{

motion entry * TypeDentry = static _ cast & lt;MotionEntry* >;

...

done = dispatchtmotionlocked;

打破;

}

boolInputDispatcher::dispatchMotionLocked{

...

Vector<。输入目标>输入目标;

boolconflictingPointerActions = false;

int 32 _ tinejectionresult;

if {

& lt!-关键点1找到目标窗口->:

injection result = FindTouchedWindowTargetsLocked;

} else{

injection result = findfocuseddwindowtargetslocked;

}

...

& lt!-关键点2的分布->:

dispatchEventLocked;

returntrue

}

从上面的代码可以看出,对于触摸事件,会先通过findTouchedWindowTargetsLocked找到目标窗口,然后通过dispatchEventLocked将消息发送到目标窗口。让我们看看如何找到目标窗口以及如何维护这个窗口列表。

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

Android系统可以同时支持多个屏幕,每个屏幕抽象为一个DisplayContent对象,内部维护一个WindowList对象,用于记录当前屏幕的所有窗口,包括状态栏、导航栏、应用窗口、子窗口等。对于触摸事件,我们更关心的是可视窗口。使用adb shell dumpsys SurfaceFlinger查看可见窗口的组织形式:

聚焦窗口

那么,如何找到触摸事件对应的窗口,无论是状态栏、导航栏还是应用窗口?此时,DisplayContent的WindowList起作用,DisplayContent保存所有窗口的信息。因此,可以根据触摸事件的位置和窗口的属性来确定发送事件的窗口。当然细节比一句话复杂多了,跟窗口的状态、透明度、分屏等信息有关。让我们简单看一下,实现主观理解的过程

int 32 _ t InputDispatcher::FindTouchedWindowTargetsLocked {

sp<。InputWindowHandle & gtwindow handle = mwindowhandles . itemat;

const InputWindowInfo * WindowInfo = WindowHandle->;getInfo

if {

继续;//错误显示

}

int 32 _ t flags = WindowInfo->;layoutParamsFlags

if{

if){

isTouchModal =)= = 0;

& lt!-找到目标窗口->:

if) {

newtuchedwindowhandle = window handle;

打破;//发现触摸窗口,退出窗口循环

}

}

...

mWindowHandles代表所有窗口,findTouchedWindowTargetsLocked的目的是从MWindowHandles中找到目标窗口。规则太复杂,根据点击位置等特点来决定改变窗口Z顺序。有兴趣可以自己分析。但是这里需要关注的是mWindowHandles,这是怎么来的,在添加或者删除窗口的时候如何保持最新?这包括与WindowManagerService的交互。mWindowHandles的值在InputDispatcher::SetInputWindows中设置。

void inputdispatcher::setInputWindows {

...

mWindowHandles = InputWindowhandles;

...

谁来调用这个函数?真正的入口是WindowManagerService中的InputMonitor会短暂调用InputDispatcher::SetInputWindows。这个定时主要和加、改、删窗口的逻辑有关。以添加窗口为例:

更新窗口逻辑

从上面的过程可以理解为什么WindowManagerService和InputManagerService是相辅相成的。到目前为止,如何找到目标窗口已经解决,下面是如何向目标窗口发送事件。

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

找到目标窗口,并封装事件。剩下的就是通知目标窗口了。但最明显的问题是,所有的逻辑都在SystemServer进程中,要通知的窗口在APP端的用户进程中。那么如何通知呢?潜意识里,你可能会想到Binder交流。毕竟Binder是Android中使用最多的IPC方法,但它不是Input事件处理中使用的Binder:较高版本使用Socket通信模式,较旧版本使用Pipe管道模式。

voidInputDispatcher::dispatchEventLocked{

pokeuseeractivitylocked;

for {

constInputTarget & ampInputTarget = InputTargets . Itemat;

ssize _ tconnectionIndex = getconnectionindexlock;

if {

sp<。连接>connection = mconnectionsbyfd . value at;

prepareDispatchCycleLocked;

} else{

}

}

}

一层一层往下看,代码会发现最后会调用InputChannel的sendMessage函数,通过socket发送到APP端。

发送过程

这个Socket是怎么来的?或者说一对Socket是怎么互相通信的?其实还是涉及到WindowManagerService的。当应用程序请求WMS添加一个窗口时,它将伴随着输入通道的创建,而窗口的添加肯定会调用ViewRootImpl的setView函数:

ViewRootImpl

public void setView {

...

requestLayout

if= = 0){

& lt!-创建输入通道容器->:

mInputChannel = new InputChannel

}

尝试{

mOrigWindowType = mwindowattributes . type;

mattachinfo . mrecomputeglobattributes = true;

collectViewAttributes

& lt!-添加窗口并请求打开套接字输入通信通道->:

RES = mwindowsession . addtodisplay;

} ...

& lt!-听,打开输入通道->:

if {

if {

mInputQueue =新输入队列;

minputqueuecallback . oninputqueuecreated;

}

mInputEventReceiver = new WindowInputEventReceiver;

}

在IWindowSession.aidl的定义中,InputChannel的类型是out,这意味着它需要由服务器来填充,所以让我们看看服务器的WMS是如何填充它的。

公共内部添加窗口 {

...

if = = 0){

string name = win . makeinputchannelname;

& lt!-关键点1创建沟通渠道->:

InputChannelinputChannels = InputChannel . OpeninputchannelAir;

& lt!-本地使用->:

win . setinputchannel;

& lt!-由- APP使用->:

输入通道。transfer to;

& lt!-注册频道和窗口->:

minputmanager . registrinputchannel;

}

WMS首先创建socketpair作为全双工通道,并将其分别填充到客户机和服务器的输入通道中。然后让InputManager将Input通信通道与当前窗口ID绑定,这样就可以知道哪个窗口使用哪个通道进行通信;最后,outInputChannel通过Binder发送回APP。以下是SocketPair的创建代码:

status _ t InputChannel::OpenInputChannelAir{

int sockets;

if) {

status _ t result =-errno;

...

返回结果;

}

int BUFFERSize = SOCKET _ BUFFER _ SIZE;

setsockopt);

setsockopt);

setsockopt);

setsockopt);

& lt!-填写服务器输入频道->:

String8 serverChannelName = name

serverChannelName.append");

outServerChannel = new Inputchannel;

& lt!-填写客户端输入通道->:

String8 clientChannelName = name

client channel name . append");

outClientChannel = new Inputchannel;

返回确定;

}

在这里,socketpair的创建和访问实际上是基于文件描述符的。WMS需要通过活页夹通信将文件描述符发送回应用程序。这部分只能参考Binder知识,主要是在内核级实现两个进程fd的转换。窗口添加成功后,创建socketpair并传递给APP,但通道没有完全建立,因为需要主动监控。毕竟要通知消息到达。先看渠道模式。

输入频道

APP端监控消息的手段是给Looper线程的epoll数组添加socket,消息一到达,Looper线程就醒来,获取事件内容。从代码来看,通信通道的打开是随着WindowInputEventReceiver的创建而完成的。

fd打开通信通道

信息来了,Looper根据fd: NativeInputEventReceiver找到对应的监听器,调用handleEvent处理对应的事件

intNativeInputEventReceiver::handleEvent {

...

if {

jnie NV * env = AnDroidRuntime::Getjniev;

status _ tstatus = consume events;

消息队列->;raiseAndClearException;

return status = = OK | | status = = NO _ MEMORY?1: 0;

}

...

之后事件会被进一步读取,封装成Java层对象,传递给Java层进行相应的回调处理:

status _ t NativeInputEventReceiver::consumeEvents{

...

for {

uint32 _ t seq

InputEvent * inputEvent

& lt!-->事件->:

status _ t status = MinPutConsumer . consume;

...

& lt!-处理触摸事件->:

case AINPUT_EVENT_TYPE_MOTION: {

motion event * motion event = static _ cast & lt;MotionEvent* >;

if& amp;& ampoutonsumedbatch){

* outConsumedBatch = true

}

inputEventObj = Android _ view _ MotionEvent _ Average Ascopy;

打破;

}

& lt!-回调处理程序->:

if {

env->;CallVoidMethod;

env->;deleteLocalRef;

}

所以最后,触摸事件被封装为inputEvent,由InputEventReceiver的DispatchInputEvent处理,我们回到我们共同的Java世界。

目标窗口中的事件处理

最后简单看一下事件的处理流程,Activity或者Dialog是怎么得到Touch事件的?怎么处理?说白了就是监控事件交给了view rootView impl中的rootView,它负责完成事件本身的消耗。要看具体实施。对于活动和对话,去视图重写了视图的事件分配函数dispatchTouchEvent,并将事件处理交给回调对象。至于View和ViewGroup的消费,是View本身的逻辑。

应用端事件处理流程总结

现在将所有流程与模块串联起来,流程大致如下:

点击屏幕InputManagerService的Read线程捕获事件,预处理后发送给Dispatcher线程Dispatcher找到目标窗口通过Socket将事件发送到目标窗口APP端被唤醒找到目标窗口处理事件

输入管理器的完整模型

结束

1.《什么可以代替手指触屏 当手指触屏,Android 都发生了什么?》援引自互联网,旨在传递更多网络信息知识,仅代表作者本人观点,与本网站无关,侵删请联系页脚下方联系方式。

2.《什么可以代替手指触屏 当手指触屏,Android 都发生了什么?》仅供读者参考,本网站未对该内容进行证实,对其原创性、真实性、完整性、及时性不作任何保证。

3.文章转载时请保留本站内容来源地址,https://www.lu-xu.com/yule/1674669.html