【Android Framework系列】第8章 事件分发你真了解吗?

news2024/11/24 14:32:22

1 事件分发基本认知

1.1 事件分发的”事件“是指什么

在这里插入图片描述

1.2 事件处理中涉及到的点

在这里插入图片描述

1.3 Android 事件处理的三个流程

在Android中,Touch事件的分发分服务端应用端。在服务端由WindowManagerService(借助InputManagerService)负责采集和分发的,在应用端则是由ViewRootImpl(内部有一个mView变量指向View树的根,负责控制View树的UI绘制和事件消息的分发)负责分发的。
当输入设备可用时,比如触屏,Linux内核会在/dev/input中创建对应的设备节点,输入事件所产生的原始信息会被Linux内核中的输入子系统采集,原始信息由Kernel Space的驱动层一直传递到User Space的设备节点
IMS所做的工作就是监听/dev/input下的所有的设备节点,当设备节点有数据时会将数据进行加工处理并找到合适的Window,将输入事件派发给他
在这里插入图片描述

1.4 事件的本质

将点击事件(MotionEvent)向某个View进行传递并最终得到处理,即当一个点击事件发生后,系统需要将这个事件传递给一个具体的View去处理。这个事件传递的过程就是分发过程。

adb shell getevent
adb shell input event 4
adb shell getevent -t -l

1.5 Linux - Posix函数了解

ePoll

被公认为Linux2.6下性能最好的多路I/O就绪通知方法。epoll只有epoll_create,epoll_ctl,epoll_wait 3个系统调用。

epoll_create

创建epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。

epoll_ctl

epoll事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。

epoll_wait

收集在epoll监控的事件中已经发送的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时
具体参考文档

iNotify

它是一个内核用于通知用户空间程序文件系统变化的机制

// 每一个 inotify 实例对应一个独立的排序的队列
int fd = inotify_init ();
// fd 是 inotify_init() 返回的文件描述符,path 是被监视的目标的路径名(即文件名或目录名),
// mask 是事件掩码, 在头文件 linux/inotify.h 中定义了每一位代表的事件。
// 可以使用同样的方式来修改事件掩码,即改变希望被通知的inotify 事件。
// Wd 是 watch 描述符。
inotify_add_watch

具体用法参考

socketpair

socketpair()函数用于创建一对无名的、相互连接的套接子。
具体使用参考

fd

linux中, 每一个进程在内核中,都对应有一个“打开文件”数组,存放指向文件对象的指针,而 fd 是这个数组的下标

我们对文件进行操作时,系统调用,将fd传入内核,内核通过fd找到文件,对文件进行操作。
既然是数组下标,fd的类型为int, < 0 为非法值, >=0 为合法值。
在linux中,一个进程默认可以打开的文件数为1024个,fd的范围为0~1023。可以通过设置,改变最大值。
在linux中,值为0、1、2的fd,分别代表标准输入、标准输出、标准错误输出。

2 事件采集流程

2.1 从内核到IMS过程

2.1.1 流程示意

在这里插入图片描述

2.1.2 EventHub

2.1.2.1 INotify

INotify是Linux提供的一种文件系统变化通知机制,什么叫文件系统变化?创建,删除,读写通通叫做变化,使用如下代码就可以将某个目录加入到INotify中

int inotifyFd = inotify_init();
int wd = inotify_add_watch(inotifyFd, "/dev/input", IN_CREATE | IN_DELETE);

上述两行代码就将/dev/input加入到了INotify中,这样,对于外部输入设备的插拔就可以很好的被检测到了。可不幸的是,INotify发现文件系统变化后不会主动告诉别人,它需要主动通过read()函数读取inotifyFd描述符来获取具体变化信息。也就是说,你插入了鼠标,INotify马上就知道了这个信息,并且将信息更新到了inotifyFd描述符对应的对象当中

2.1.2.2 Eventhub

Epoll可以用来监听多个描述符的可读写inotifyFd状态,什么意思呢?比如说上面说到了外部输入设备插拔可以被INotify检测到,并且将相信信息写入到inotifyFd对用的对象中,但是我没法知道INotify什么时候捕获到了这些信息。Epoll可以监听inotifyFd对应的对象内容是否有变化,一旦有变化马上能进行处理,平常大部分时间没监听到变化时睡大觉
在这里插入图片描述
epoll_wait大部分时间处于阻塞状态(这点和socket等待连接很相似),一旦/dev/input/event0节点有变化(即产生了输入事件),epoll_wait会执行完毕

2.1.3 IMS

2.1.3.1 InputReaderThread

InputReader会不断的循环读取EventHub中的原始输入事件

2.1.3.2 InputDispatcherThread

InputDispatcher中保存了WMS中的所有的Window信息WMS会将窗口的信息实时的更新到InputDispatcher中),这样InputDispatcher就可以将输入事件派发给合适的Window

2.1.3.3 整体流程源码分析

2.1.3.3.1 由SystemServer启动一个IMS进程进程,然后进行各个对象的初始化

SystemServer.startOtherServices
在这里插入图片描述
InputManagerService.InputManagerService
在这里插入图片描述
在这里插入图片描述

2.1.3.3.2 同步会在native层进行初始化,开启一个InputManager对象|

开启两个线程:
一个Reader线程用来读取触摸信号
一个Dispatcher用来转发信息

EventHub负责采集底层信号
在这里插入图片描述
com_android_server_input_InputManagerService.cpp
在这里插入图片描述
com_android_server_input_InputManagerService.cpp
在这里插入图片描述
InputManager.cpp.InputManager
在这里插入图片描述
InputManager.cpp.initialize
在这里插入图片描述
InputReader.cpp.InputReader
在这里插入图片描述
InputDispatcher.cpp.InputDispatcher
在这里插入图片描述

2.1.3.3.3 通过IMS.start启动上面两个线程

SystemServer.startOtherService
在这里插入图片描述
InputManagerService.start
在这里插入图片描述
com_android_server_input_InputManagerService.cpp
在这里插入图片描述
Inputmanager.cpp
在这里插入图片描述

2.1.3.3.4 InputRerader线程处理过程

整体示意
在这里插入图片描述

2.1.3.3.4.1 线程运行

InputReader.cpp
在这里插入图片描述

2.1.3.3.4.2 处理事件,注意getEvent的阻塞问题

InputReader.cpp

void InputReader::loopOnce() {
358    int32_t oldGeneration;
359    int32_t timeoutMillis;
360    bool inputDevicesChanged = false;
361    Vector<InputDeviceInfo> inputDevices;
362    { // acquire lock
363        AutoMutex _l(mLock);
364
365        oldGeneration = mGeneration;
366        timeoutMillis = -1;
367         //查看InputReader配置是否修改
368        uint32_t changes = mConfigurationChangesToRefresh;
369        if (changes) {
370            mConfigurationChangesToRefresh = 0;
371            timeoutMillis = 0;
372            refreshConfigurationLocked(changes);
373        } else if (mNextTimeout != LLONG_MAX) {
374            nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
375            timeoutMillis = toMillisecondTimeoutDelay(now, mNextTimeout);
376        }
377    } // release lock
378      //从Eventhub中读取事件,这里注意因为之前将的EventHub.getEvent中有epoll_wait进行阻塞,若FD发生改变则执行后面代码
379    size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
380
381    { // acquire lock
382        AutoMutex _l(mLock);
383        mReaderIsAliveCondition.broadcast();
384
385        if (count) {//处理事件
386            processEventsLocked(mEventBuffer, count);
387        }
388
389        if (mNextTimeout != LLONG_MAX) {
390            nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
391            if (now >= mNextTimeout) {
392#if DEBUG_RAW_EVENTS
393                ALOGD("Timeout expired, latency=%0.3fms", (now - mNextTimeout) * 0.000001f);
394#endif
395                mNextTimeout = LLONG_MAX;
396                timeoutExpiredLocked(now);
397            }
398        }
399
400        if (oldGeneration != mGeneration) {
401            inputDevicesChanged = true;
402            getInputDevicesLocked(inputDevices);
403        }
404    } // release lock
405
406    // Send out a message that the describes the changed input devices.
407    if (inputDevicesChanged) {
408        mPolicy->notifyInputDevicesChanged(inputDevices);//发送一个消息,该消息描述已更改
409    }
410
411    // Flush queued events out to the listener.
412    // This must happen outside of the lock because the listener could potentially call
413    // back into the InputReader's methods, such as getScanCodeState, or become blocked
414    // on another thread similarly waiting to acquire the InputReader lock thereby
415    // resulting in a deadlock.  This situation is actually quite plausible because the
416    // listener is actually the input dispatcher, which calls into the window manager,
417    // which occasionally calls into the input reader.
418    mQueuedListener->flush();//将时间传递到InputDispatcher
419}
2.1.3.3.4.3 向下衍生调用,ProcessEventsForDeviceLocked是处理数据流,下面是针对于文件状态处理

processEventsLocked
在这里插入图片描述

2.1.3.3.4.4 最终调用Device->process

processEventsForDeviceLocked
在这里插入图片描述

2.1.3.3.4.5 调用

InputDevice::process
在这里插入图片描述

2.1.3.3.4.6 InputMapper::process

这里注意InputMapper有多个子类,
我们要找触摸事件的相关处理就是找到TouchInputMapper
在这里插入图片描述

2.1.3.3.4.7 TouchInputManager::sync

在这里插入图片描述

2.1.3.3.4.8 然后最终数据流

1.processRawTouches(false /timeout/);
4345
2.cookAndDispatch
在这里插入图片描述
3.dispatchTouches
在这里插入图片描述
4.dispatchMotion
在这里插入图片描述
5.当前这个getListener实际获取是mQueuedListener = new QueuedInputListener(listener);实例
在这里插入图片描述
6.InputLintener.cpp.QueuedInputListener::notifyMotion
这里我们实际可以看到他就是往一个队列当中添加数据
实际上是我们将触摸相关的事件进行包装之后,将其加入到一个ArgsQueue队列,
到此,我们已经将数据加入到参数队列中
在这里插入图片描述
7.这时我们在回到上面processEventsLocked调用之后,发现当前mQueueListener->flush的调用
在这里插入图片描述
8.InputLintener.cpp.QueuedInputListener::flush

遍历整个mArgsQueue数组, 在input架构中NotifyArgs的实现子类主要有以下几类

NotifyConfigurationChangedArgs
NotifyKeyArgs
NotifyMotionArgs 通知motion事件
NotifySwitchArgs
NotifyDeviceResetArgs

在这里插入图片描述

9.NotifyMotionArgs
这里的listener根据溯源可以看到是InputDispatcher
在这里插入图片描述
在这里插入图片描述
所以综上分析,是调用InputDispatcher中的notifyMotion,这里就完成了向InputDispatcher的信息传递
在这里插入图片描述
10.执行notifyMotion中的唤醒操作
InputDispatcher.cpp
在这里插入图片描述
在这里插入图片描述

2.1.3.3.5 InputDispatcher线程处理过程

整体流程示意
在这里插入图片描述

2.1.3.3.5.1 InputDispatcher.cpp.threadLoop()

4623

2.1.3.3.5.2 InputDispatcher.cpp.dispatchOnce()

pollOnce这里是等待被唤醒,进入epoll_wait等待状态,当发生以下任一情况则退出等待状态:
callback:通过回调方法来唤醒;
timeout:到达nextWakeupTime时间,超时唤醒;
wake: 主动调用Looperwake()方法;
在这里插入图片描述

2.1.3.3.5.3 dispatchOnceInnerLocked

294
在这里插入图片描述

2.1.3.3.5.4 dispatchMotionLocked

这里两条线,
1.findTouchedWindowTargetLocked 负责找到要分发的Window
2.dispatchEventLocked 负责具体分发目标
864
在这里插入图片描述
1.findTouchedWindowTargetLocked 负责找到能够分发的Window
1166
在这里插入图片描述
在这里插入图片描述
这是一个很长的方法
大体是判断这个事件的类型
获取能够处理这个事件的forceground window,如果这个window不能够继续处理事件,就是说这个window的主线程被某些耗时操作占据,handleTargetsNotReadyLocked这个方法主要是判定当前的window是否发生ANR如果没有则处理下面的addWindowTargetLocked
在这里插入图片描述
addWindowTargetLocked
这里主要的目的是将inputChannel添加至InputTarget
在这里插入图片描述

2.dispatchEventLocked 负责具体分发目标
1.dispatchEventLocked
958

2.prepareDispatchCycleLocked
1837

3.enqueueDispatchEntriesLocked
该方法主要功能:
根据dispatchMode来决定是否需要加入outboundQueue队列;
根据EventEntry,来生成DispatchEntry事件;
将dispatchEntry加入到connection的outbound队列.
执行到这里,其实等于由做了一次搬运的工作,将InputDispatcher中mInboundQueue中的事件取出后, 找到目标window后,封装dispatchEntry加入到connection的outbound队列.
在这里插入图片描述

4.startDispatchCycleLocked
startDispatchCycleLocked的主要功能: 从outboundQueue中取出事件,重新放入waitQueue队列

startDispatchCycleLocked触发时机:当起初connection.outboundQueue等于空, 经enqueueDispatchEntryLocked处理后, outboundQueue不等于空。

startDispatchCycleLocked主要功能: 从outboundQueue中取出事件,重新放入waitQueue队列

publishMotionEvent执行结果status不等于OK的情况下:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
5.至此调用了connection的inputPublisher的publishMotionEvent方法将事件分发消耗。
InputTransport.cpp
在这里插入图片描述
在这里插入图片描述

2.2 从IMS到WMS过程

2.2.1 InputChannel

2.2.1.1 概念

InputReaderThreadInputDispatcherThread是运行在SystemServer进程中的
用户进程是和其不在同一个进程中的,那么这中间一定也是有进程间的通信机制在里面
InputChannel,主要的核心目的是为了与用户进程进行通信,这个InputChannel是由WMS管理,触发构建生成

2.2.1.2 模型示意

在这里插入图片描述

2.2.1.3 创建InputChannel过程

1.在ViewRootImpl中的setView函数中的session.addToDispaly会触发WMSaddWindow调用
这里注意的是在进行addTodisplay之前在java层面构建了一个InputChannel对象
在这里插入图片描述
在这里插入图片描述
2.addWindow中通过WindowState去进行openInputChannel
这里其实就是到native层面去构建一个通信机制,
考虑性能问题,一般还是依赖于linux底层的通讯机制
在这里插入图片描述
3.openInputChannel
构建并且注册
在这里插入图片描述
4.InputChannel.openInputChannelPair实际上就是调用了native层去创建一组通信
在这里插入图片描述
注意一下这个函数在InputTransport.cpp
这里很明显的能看到,构建了两组socket通信

Linux实现了一个socketpair调用可以实现上述在同一个文件描述符中进行读写的功能(该调用目前也是POSIX规范的一部分 。该系统调用能创建一对已连接的(UNIX族)无名socket。在Linux中,完全可以把这一对socket当成pipe返回的文件描述符一样使用,唯一的区别就是这一对文件描述符中的任何一个都可读和可写
在这里插入图片描述
5.创建完成后,回到上面3点最后一句,这里是在将构建好的通道注册到InputDispatcher中
最终调用到InputDispatcher.cpp.registerInputChannel函数
至此,当前InputChannel对象传递至InputDispatcher中,且进行了一定封装,同时传入了窗体句柄过来
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2.2.1.4 开启输入事件的监听接收事件

1.setView中,我们创建了InputChannel之后,开启了对于InputChannel中输入事件的监听
ViewRootImpl.setview
846
这里是调用到父类的构造
在这里插入图片描述
2.在native层构建一个监听事件的监听器
android_view_InputEventReceiver.cpp
339
在这里插入图片描述
在这里插入图片描述
该方法所执行的操作就是对传递的fd添加epoll监控,Looper会循环调用pollOnce方法,而pollOnce方法的核心实现就是pollInner
大致实现内容为等待消息的到来,当有消息到来后,根据消息类型做一些判断处理,然后调用其相关的callback。
我们当前是对于开启的socket的一个监听,当有数据到来,我们便会执行相应的回调。这里对于InputChannel的回调是在调用了NativeInputEventReceiver的handleEvent方法
在这里插入图片描述
3.消息接收到后回调
android_view_InputEventReceiver.cpp::handleEvent
在这里插入图片描述
android_view_InputEventReceiver.cpp::consumeEvents
在这里插入图片描述
consume
在这里插入图片描述
android_view_InputEventReceiver.cpp::consumeEvents
上面消息接收完毕之后,执行完consume后回到consumeEvent中,这时根据读取的数据进行判断处理,然后向上call到Java函数
在这里插入图片描述

2.2.1.5 回调上层JAVA函数

2.3 从WMS转入ViewRootImpl过程

1.InputEventReceiver.dispatchInputEvent
这里调用的InputEvent是下发到子类当中,因为具体实现类为WindowInputEventReceiver
在这里插入图片描述

2.ViewRootImpl.WindowInputEventReceiver.onInputEvent
8182

3.ViewRootImpl.QueuedInputEvent.enqueueInputEvent
7992

4.ViewRootImpl.QueuedInputEvent.doProcessInputEvents
8031

5.ViewRootImpl.QueuedInputEvent.deliverInputEvent
8080

6.在ViewRootImpl中,有一系列类似于InputStage(输入事件舞台)的概念,
每种InputStage可以处理一定的事件类型,
子类有:AsyncInputStageViewPreImeInputStageViewPostImeInputStage
对于点击事件来说,ViewPostImeInputStage可以处理它,
ViewPostImeInputStage中,有一个processPointerEvent方法,
它会调用mViewdispatchPointerEvent方法,注意,这里的mView其实就是DecorView

在这里插入图片描述
在这里插入图片描述
注意这里肯定是调用子类onProcess
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
还记得这里的View是谁?? DecorView!
在这里插入图片描述
这里可以看到最终调用到ViewdispatchPointerEvent类,这里调用的是dispatchTouchEvent
但是这里考虑到实现类是DecView,所以调用的是子类的该方法
在这里插入图片描述
这里可以看到他是在对于一个windowcallback调用
在这里插入图片描述
callback是在Activity.attach()中设置的, 就是当前的activity
在这里插入图片描述

所以这里调用的是ActivitydispatchTouchEvent
在这里插入图片描述

3 事件分发过程

3.1 责任链模式

责任链模式是一种行为模式,为请求创建一个接收者的对象链.这样就避免,一个请求链接多个接收者的情况.进行外部解耦.类似于单向链表结构

3.1.1 优点

  1. 降低耦合度。它将请求的发送者和接收者解耦。
  2. 简化了对象。使得对象不需要知道链的结构。
  3. 增强给对象指派职责的灵活性。通过改变链内的成员或者调动它们的次 序,允许动态地新增或者删除责任。
  4. 增加新的请求处理类很方便

3.1.2 缺点

1.不能保证请求一定被接收
2.系统性能将受到一定影响,而且在进行代码调试时不太方便,可能会造成循环调用
3.可能不容易观察运行时的特征,有碍于除错

3.1.3 在Android事件分发机制是责任链模式最典型的应用:

dispatchTouchEvent就是责任链中的将事件交给下一级处理
onInterceptTouchEvent就是责任链中,处理自己处理事务的方法
onTouchEvent是责任链中事件上报的事件链

3.2 事件在哪些对象之间进行传递在这里插入图片描述

在这里插入图片描述

3.3 事件分发顺序

在这里插入图片描述

3.4 事件分发过程由哪些方法协作完成?

在这里插入图片描述

3.5 关于事件分发消费中的U型链与L型链的阐述

3.5.1 事件分发过程

在这里插入图片描述
事件分发机制详细流程

super:调用父类方法
true:消费事件,即事件不继续往下传递
false:不消费事件,事件也不继续往下传递 / 交由给父控件onTouchEvent()处理

3.5.1.1 Activity

Activity要做的事情是转发到下层,都没有消费的情况下,最后由Activity来收尾兜底

3.5.1.2 ViewGroup

要做的事情

  1. 是否在容器层面拦截
  2. 该时间是否要分发,取消事件不需要消费
  3. 传递子View消费之前还要看是不是有效的,是点在了哪一个View
  4. 所以需要遍历所有子View确定是否存在

源码分析

public boolean dispatchTouchEvent(MotionEvent ev) {
        //验证事件是否连续
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }
        //这个变量用于记录事件是否被处理完
        boolean handled = false;
        //过滤掉一些不合法的事件:当前的View的窗口被遮挡了。
        if (onFilterTouchEventForSecurity(ev)) {
            //如果事件发生的View在的窗口,没有被遮挡
            final int action = ev.getAction();
            //重置前面为0 ,只留下后八位,用于判断相等时候,可以提高性能。
            final int actionMasked = action & MotionEvent.ACTION_MASK;
            //判断是不是Down事件,如果是的话,就要做初始化操作
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                //如果是down事件,就要清空掉之前的状态,比如,重置手势判断什么的。
                //比如,之前正在判断是不是一个单点的滑动,但是第二个down来了,就表示,不可能是单点的滑动,要重新开始判断触摸的手势
                //清空掉mFirstTouchTarget
                // Throw away all previous state when starting a new touch gesture.
                // The framework may have dropped the up or cancel event for the previous gesture
                // due to an app switch, ANR, or some other state change.
                cancelAndClearTouchTargets(ev);
                resetTouchState();
            }
 
 
            //检查是否拦截事件
            final boolean intercepted;
            //如果当前是Down事件,或者已经有处理Touch事件的目标了
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
                //判断允不允许这个View拦截
                //使用与运算作为判断,可以让我们在flag中,存储好几个标志
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //如果说允许拦截事件
                if (!disallowIntercept) {
                    //确定是不是拦截了
                    intercepted = onInterceptTouchEvent(ev);
                    //重新恢复Action,以免action在上面的步骤被人为地改变了
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                //如果说,事件已经初始化过了,并且没有子View被分配处理,那么就说明,这个ViewGroup已经拦截了这个事件
                intercepted = true;
            }
 
            // Check for cancelation.
            //如果viewFlag被设置了PFLAG_CANCEL_NEXT_UP_EVENT ,那么就表示,下一步应该是Cancel事件
            //或者如果当前的Action为取消,那么当前事件应该就是取消了。
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;
 
            // Update list of touch targets for pointer down, if needed.
            //如果需要(不是取消,也没有被拦截)的话,那么在触摸down事件的时候更新触摸目标列表
            //split代表,当前的ViewGroup是不是支持分割MotionEvent到不同的View当中
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            //新的触摸对象,
            TouchTarget newTouchTarget = null;
            //是否把事件分配给了新的触摸
            boolean alreadyDispatchedToNewTouchTarget = false;
            //事件不是取消事件,也没有拦截那么就要判断
            if (!canceled && !intercepted) {
                //如果是个全新的Down事件
                //或者是有新的触摸点
                //或者是光标来回移动事件(不太明白什么时候发生)
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    //这个事件的索引,也就是第几个事件,如果是down事件就是0
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    //获取分配的ID的bit数量
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;
 
                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    //清理之前触摸这个指针标识,以防他们的目标变得不同步。
                    removePointersFromTouchTargets(idBitsToAssign);
 
                    final int childrenCount = mChildrenCount;
                    //如果新的触摸对象为null(这个不是铁定的吗)并且当前ViewGroup有子元素
                    if (newTouchTarget == null && childrenCount != 0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        //下面所做的工作,就是找到可以接收这个事件的子元素
                        final View[] children = mChildren;
                        //是否使用自定义的顺序来添加控件
                        final boolean customOrder = isChildrenDrawingOrderEnabled();
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            //如果是用了自定义的顺序来添加控件,那么绘制的View的顺序和mChildren的顺序是不一样的
                            //所以要根据getChildDrawingOrder取出真正的绘制的View
                            //自定义的绘制,可能第一个会画到第三个,和第四个,第二个画到第一个,这样里面的内容和Children是不一样的
                            final int childIndex = customOrder ?
                                    getChildDrawingOrder(childrenCount, i) : i;
                            final View child = children[childIndex];
                            //如果child不可以接收这个触摸的事件,或者触摸事件发生的位置不在这个View的范围内
                            if (!canViewReceivePointerEvents(child)
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                continue;
                            }
                            //获取新的触摸对象,如果当前的子View在之前的触摸目标的列表当中就返回touchTarget
                            //子View不在之前的触摸目标列表那么就返回null
                            newTouchTarget = getTouchTarget(child);
                            if (newTouchTarget != null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                //如果新的触摸目标对象不为空,那么就把这个触摸的ID赋予它,这样子,
                                //这个触摸的目标对象的id就含有了好几个pointer的ID了
 
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }
                            //如果子View不在之前的触摸目标列表中,先重置childView的标志,去除掉CACEL的标志
                            resetCancelNextUpFlag(child);
                            //调用子View的dispatchTouchEvent,并且把pointer的id 赋予进去
                            //如果说,子View接收并且处理了这个事件,那么就更新上一次触摸事件的信息,
                            //并且为创建一个新的触摸目标对象,并且绑定这个子View和Pointer的ID
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Child wants to receive touch within its bounds.
                                mLastTouchDownTime = ev.getDownTime();
                                mLastTouchDownIndex = childIndex;
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                //这里给mFirstTouchTarget赋值
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                    }
                    //如果newTouchTarget为null,就代表,这个事件没有找到子View去处理它,
                    //那么,如果之前已经有了触摸对象(比如,我点了一张图,另一个手指在外面图的外面点下去)
                    //那么就把这个之前那个触摸目标定为第一个触摸对象,并且把这个触摸(pointer)分配给最近添加的触摸目标
                    if (newTouchTarget == null && mFirstTouchTarget != null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while (newTouchTarget.next != null) {
                            newTouchTarget = newTouchTarget.next;
                        }
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                    }
                }
            }
 
            // Dispatch to touch targets.
            //如果没有触摸目标
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                //那么就表示我们要自己在这个ViewGroup处理这个触摸事件了
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                // Dispatch to touch targets, excluding the new touch target if we already
                // dispatched to it.  Cancel touch targets if necessary.
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                //遍历TouchTargt树.分发事件,如果我们已经分发给了新的TouchTarget那么我们就不再分发给newTouchTarget
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        //是否让child取消处理事件,如果为true,就会分发给child一个ACTION_CANCEL事件
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //派发事件
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        //cancelChild也就是说,派发给了当前child一个ACTION_CANCEL事件,
                        //那么就移除这个child
                        if (cancelChild) {
                            //没有父节点,也就是当前是第一个TouchTarget
                            //那么就把头去掉
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                //把下一个赋予父节点的上一个,这样当前节点就被丢弃了
                                predecessor.next = next;
                            }
                            //回收内存
                            target.recycle();
                            //把下一个赋予现在
                            target = next;
                            //下面的两行不执行了,因为我们已经做了链表的操作了。
                            //主要是我们不能执行predecessor=target,因为删除本节点的话,父节点还是父节点
                            continue;
                        }
                    }
                    //如果没有删除本节点,那么下一轮父节点就是当前节点,下一个节点也是下一轮的当前节点
                    predecessor = target;
                    target = next;
                }
            }
 
            // Update list of touch targets for pointer up or cancel, if needed.
            //遇到了取消事件、或者是单点触摸下情况下手指离开,我们就要更新触摸的状态
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                //如果是多点触摸下的手指抬起事件,就要根据idBit从TouchTarget中移除掉对应的Pointer(触摸点)
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }
 
        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }

主要做了这三件事:

  1. 检查是否拦截事件
  2. 检查子view并传递事件
  3. 自己处理事件

整体流程:

  1. DOWN事件下,通过轮询当前ViewGroup的下层View
  2. 同步进行判断,是否在这个View的矩形范围内
  3. 如果在的话,那么选中这个View进行dispatch转发,他是ViewGroup会继续转发下层,如果是View直接消费处理!
  4. MOVE类型,因为其特性的原因,量大,用轮询不合适
  5. 所以,在DOWN下来的时候直接记录当前这个View,然后默认后续直接转发这个View
  6. 如果手指画出当前View,那么事件信息传递过来的ActionID会改变,改变ID后,进行将target变更!UP\ CANCEL会重置

3.5.1.3 View

源码分析

/**
     * Pass the touch screen motion event down to the target view, or this
     * view if it is the target.
     * 将屏幕的按压事件传递给目标view,或者当前view即目标view
     * @param event The motion event to be dispatched.
     * @return True if the event was handled by the view, false otherwise.
     */
    public boolean dispatchTouchEvent(MotionEvent event) {
        //最前面这一段就是判断当前事件是否能获得焦点,
        // 如果不能获得焦点或者不存在一个View,那我们就直接返回False跳出循环
        // If the event should be handled by accessibility focus first.
        if (event.isTargetAccessibilityFocus()) {
            // We don't have focus or no virtual descendant has it, do not handle the event.
            if (!isAccessibilityFocusedViewOrHost()) {
                return false;
            }
            // We have focus and got the event, then use normal event dispatch.
            event.setTargetAccessibilityFocus(false);
        }
        //设置返回的默认值
        boolean result = false;
        //这段是系统调试方面,可以直接忽略
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        //Android用一个32位的整型值表示一次TouchEvent事件,低8位表示touch事件的具体动作,比如按下,抬起,滑动,还有多点触控时的按下,抬起,这个和单点是区分开的,下面看具体的方法:
        //1 getAction:触摸动作的原始32位信息,包括事件的动作,触控点信息
        //2 getActionMasked:触摸的动作,按下,抬起,滑动,多点按下,多点抬起
        //3 getActionIndex:触控点信息
        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            //当我们手指按到View上时,其他的依赖滑动都要先停下
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }
        //过滤掉一些不合法的事件,比如当前的View的窗口被遮挡了。
        if (onFilterTouchEventForSecurity(event)) {
            //ListenerInfo 是view的一个内部类 里面有各种各样的listener,
            // 例如OnClickListener,OnLongClickListener,OnTouchListener等等
            ListenerInfo li = mListenerInfo;

            //首先判断如果监听li对象!=null 且我们通过setOnTouchListener设置了监听,
            // 即是否有实现OnTouchListener,
            // 如果有实现就判断当前的view状态是不是ENABLED,
            // 如果实现的OnTouchListener的onTouch中返回true,并处理事件,则
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                //如果满足这些条件那么返回true,这个事件就在此处理意味着这个View需要事件分发
                result = true;
            }
            //如果上一段判断的条件没有满足(没有在代码里面setOnTouchListener的话),
            // 就判断View自身的onTouchEvent方法有没有处理,没有处理最后返回false,处理了返回true;
            //也就是前面的判断优先级更高

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        系统调试分析相关,没有影响
        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        //如果这是手势的结尾,则在嵌套滚动后清理
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
    }

先调用listeneronTouch,而onTouchEvent的调用时根据onTouch结果走,
onTouch的使用优先级高于onTouchEventonTouch通过返回值可以控制onTouchEvent的调用。

3.5.2 事件消费过程

3.5.3 U型模型

简单理解:事件从上到下传递,从下到上消费(无消费)

3.5.4 L型模型

简单理解:事件从上到下传递,后被拦截(有消费)

4 总结

最后总结一下,事件分发就是事件从linux层通过驱动采集数据,底层使用epoll和inotify传递出来,FrameWork层通过InputReaderThread读取,通过InputDispatcherThread分发到WMS,WMS将事件传递到Activity,上层的Activity、ViewGroup、View之间事件的分发和消费

具体流程:

  1. 事件信号是物理文件存储数据,位置:dev/input
  2. linux有提供相关的文件监控api,其中使用了inotify(能监控文件变化产生FD) epoll(可以监控FD,以此配合完成文件的监控与监听)
  3. Android写了两个线程来处理dev/input下面的信号(InputReaderThreadInputDispathcerThread)
  4. 专门写了一个EventHub对象,里面用inotify+epolldev/input下进行监控!
  5. 将该对象放到InputReaderThread当中去执行,轮训getEvent(),这个里面有epoll_wait,相当于wait-notify机制,唤醒的触发点是/dev/input下的文件被改变,这个文件由驱动去推送数据
  6. InputReaderThread当中将/dev/input下的数据提取,封装,然后交给InputDispathcerThread
  7. InputDispathcerThread给最终选择到对应的ViewRootImpl(Window)
  8. 中间的通信机制通过socketpair进行,两边一人一组socketpair
  9. 然后在ViewRootImpl中对于Channel的连接的文件进行监控,一次导致能够最终上层接受到touch信号!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/786490.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

高校vr元宇宙虚拟禁毒体验推动社会戒毒工作的深入开展

元宇宙是指一个虚拟的、全球性的、可交互的虚拟世界&#xff0c;深度融合了VR虚拟现实、AR增强现实和ai等技术。将元宇宙应用于戒毒安全教育平台&#xff0c;具有以下现实意义&#xff1a; 创造安全的学习环境 戒毒安全教育需要让人们了解毒品的危害和如何预防&#xff0c;但直…

水环境综合治理监测系统:筑牢城市水生态安全屏障

水是生命之源&#xff0c;是人类赖以生存的基础。然而&#xff0c;随着工业化、城市化的快速发展&#xff0c;水污染问题日益凸显&#xff0c;给居民的环境卫生以及用水安全带来了巨大的威胁。因此&#xff0c;加强水环境综合治理&#xff0c;保护水资源和维护生态平衡&#xf…

vue之ReadIdcard(身份证读取组件)

组件功能 读取二代身份证信息组件,包含无效身份证验证,过期身份证验证,是否满16周岁验证 #界面 #界面输入项 序号输入项输入形式是否必输是否可配置备注1

CentOS 安装Oracle11g

一、方式一&#xff08;亲测&#xff09; https://blog.csdn.net/zw521cx/article/details/108550215 遇到问题解决&#xff1a; 1.执行 dbca -silent -responseFile /home/oracle/response/dbca.rsp 报错 解决办法&#xff1a; a.全局查找 [rootVM-0-8-centos ~]# locate S…

Vue全局事件总线

main.js //引入Vue import Vue from vue //引入App import App from "./App"; //关闭Vue的生产提示 Vue.config.productionTip false // const Demo Vue.extend({}) // const d new Demo() // Vue.prototype.x d//创建vm new Vue({el:#app,render:h>h(App),b…

MySQL 修改时区的方法

文章目录 什么是MySQL时区&#xff1f;通过MySQL命令模式下修改首先查看MySQL当前的时间进行修改 不方便重启MySQL&#xff0c;临时解决时区问题通过修改配置文件mysql.cnf(my.ini)来进行修改总结 环境&#xff1a;Windows10系统&#xff0c;MySQL5.7版本 mysql修改时区的方法&…

音视频——压缩原理

H264视频压缩算法现在无疑是所有视频压缩技术中使用最广泛&#xff0c; 最流行的。随着 x264/openh264以及ffmpeg等开源库的推出&#xff0c;大多数使用者无需再对H264的细节做过多的研究&#xff0c;这大降低了人们使用H264的成本。 但为了用好H264&#xff0c;我们还是要对…

X - Transformer

回顾 Transformer 的发展 Transformer 最初是作为机器翻译的序列到序列模型提出的&#xff0c;而后来的研究表明&#xff0c;基于 Transformer 的预训练模型&#xff08;PTM&#xff09; 在各项任务中都有最优的表现。因此&#xff0c;Transformer 已成为 NLP 领域的首选架构&…

面试题——当实体类中的属性名和表中的字段名不一样,如何将查询的结果封装到指定 pojo?

在使用MyBatis的时候&#xff0c;应该注意实体类的属性名尽量和表的字段名尽量相同&#xff0c;如果不同将会导致MyBatis无法完成数据的封装&#xff0c;但是在软件开发过程中&#xff0c;数据库的创建和软件环境的搭建不可能是同一个人&#xff0c;实体类属性名和数据库的字段…

真正理解红黑树,真正的(Linux内核里大量用到的数据结构

作为一种数据结构&#xff0c;红黑树可谓不算朴素&#xff0c;因为各种宣传让它过于神秘&#xff0c;网上搜罗了一大堆的关于红黑树的文章&#xff0c;不外乎千篇一律&#xff0c;介绍概念&#xff0c;分析性能&#xff0c;贴上代码&#xff0c;然后给上罪恶的一句话&#xff0…

芯片制造详解.从沙子到晶圆.学习笔记(一)

刚入行半导体行业&#xff0c;很多知识需要系统的学习&#xff0c;想从入门通俗易懂的知识开始学起&#xff0c;于是在导师的帮助下&#xff0c;找到了这门课程&#xff0c;那就从这门课程开始打开我的半导体之旅吧。 我只是对视频内容的提炼&#xff0c;和自己的学习心得&…

hack the box—Lame

扫描 还是老方法nmapfscan得到开放的端口和服务 nmap -sV -sC -sT -v -T4 10.10.10.3 看到开了445&#xff0c;先来波ms17-010&#xff0c;发现失败。 这里还开个21&#xff0c;并且可以知道版本号&#xff0c;直接搜索ftp漏洞 msf正好有对应的模块 设置好参数后进行攻击&…

从iOS App启动速度看如何为基础性能保驾护航 | 京东物流技术团队

1 前言 启动是App给用户的第一印象&#xff0c;一款App的启动速度&#xff0c;不单单是用户体验的事情&#xff0c;往往还决定了它能否获取更多的用户。所以到了一定阶段App的启动优化是必须要做的事情。App启动基本分为以下两种 1.1 冷启动 App 点击启动前&#xff0c;它的…

uniapp---app端人脸识别组件(宽屏版1280*800组件,需手动截屏拍人脸识别,踩坑,成长)

一、首先记录下踩到的坑 我这边做的是一个挂在门口的门牌机&#xff0c;可以用于扫脸签到&#xff0c;扫码签到&#xff0c;扫脸实现用的是live-pusher组件&#xff08;代码很长&#xff0c;放在最下面&#xff0c;不能直接用&#xff0c;需要根据实际情况修改&#xff09;去做…

处理多维特征的输入

数据的每一列称为&#xff1a;特征/字段 x的数据变为8列&#xff0c;维数8 step one:构建数据集 x_data;y_data&#xff1a;创建两个Tensor step two:定义模型 step three:构造损失和优化器 step four:训练 else 激活函数&#xff1a; 代码更改部分&#xff1a; 转->大佬笔…

基于AutoEncoder自编码器的人脸识别matlab仿真

目录 1.算法理论概述 2.部分核心程序 3.算法运行软件版本 4.算法运行效果图预览 5.算法完整程序工程 1.算法理论概述 人脸识别是计算机视觉领域的重要研究方向&#xff0c;其目标是从图像或视频中准确地识别和识别人脸。传统的人脸识别方法通常基于特征提取和分类器&#…

Java虚拟机——后端编译与优化

编译器无论在何时、何种状态下将Class文件转换成与本地基础设施相关的二进制机器码&#xff0c;它都可以视为整个编译过程的后端。即时编译一直是绝对主流的编译形式&#xff0c;不过提前编译也逐渐被主流JDK支持。 1 即时编译器 目前两款主流的Java虚拟机&#xff08;HotSpo…

【MATLAB绘图】

MATLAB绘图函数&#xff1a;Plot函数详解 介绍 MATLAB是一种常用的科学计算和数据可视化工具&#xff0c;它提供了强大的绘图函数&#xff0c;使用户能够创建各种类型的图表和图形。 基本语法 plot函数的基本语法如下&#xff1a; plot(x, y)其中&#xff0c;x和y是长度相…

HDFS的文件块大小(重点)

HDFS 中的文件在物理上是分块存储 &#xff08;Block &#xff09; &#xff0c; 块的大小可以通过配置参数( dfs.blocksize&#xff09;来规定&#xff0c;默认大小在Hadoop2.x/3.x版本中是128M&#xff0c;1.x版本中是64M。 如果一个文件文件小于128M&#xff0c;该文件会占…

Git 命令行教程:如何在 GitLab 中恢复已删除的分支

在软件开发过程中&#xff0c;版本控制是一个至关重要的环节。Git 是最流行的分布式版本控制系统之一&#xff0c;它能够帮助团队高效地管理代码。然而&#xff0c;有时候会发生意外&#xff0c;例如代码误合、错误的删除等情况&#xff0c;导致重要的开发分支本地和远程不慎被…