Android Input的流程和原理
Android Input事件机制
Android系统是由事件驱动的,而Input是最常见的事件之一,用户的点击、滑动、长按等操作,都属于Input事件驱动,其中的核心就是InputReader和InputDispatcher。InputReader和InputDispatcher是跑在system_server进程中的两个Native循环线程,负责读取和分发Input事件。整体处理流程大致如下:
触摸屏会按照屏幕硬件的触控采样率周期,每隔几秒扫描一次,如果有触控时间就会上报到对应设备驱动;系统封装了一个叫EventHub的对象,它利用INotify和epoll机制监听/dev/input目录下的Input设备驱动节点,通过EventHug的getEvents接口就可以监听并获取到Input事件;
InputReader负责从EventHub里面把Input事件读取出来,然后交给InputDispatcher进行事件分发;
InputDispatcher在拿到InputReader获取的事件之后,对事件进行包装后,寻找并分发到目标窗口;
InboundQueue队列("iq")中放着InputDispatcher从InputReader中拿到的Input事件;
OutboundQueue队列("oq")里面放的是即将要指派给各个目标窗口App的事件;
WaitQueue队列("wq")里面记录的是已经派发给App,但是App还在处理没有返回处理成功的事件;
PendingInputEventQueue("aq")中记录的是应用需要处理的Input事件,这里可以看到Input事件已经传递到应用进程;
deliverInputEvent标识App UI Thread被Input事件唤醒;
InputResponse标识Input事件区域,这里可以看到一个Input_Down事件 + 若干个Input_Move + 一个Input_Up事件的处理阶段都被算到了这里;
App响应处理Input事件,内部会在其界面View数中逐层分发和处理。
结合Systrace分析
从上面的系统机制的分析可以看出,整个Input触控时间的分发与处理主要涉及到两个进程:一个是system_server系统进程,另一个是当前焦点窗口所属的Setting应用进程。
system_server进程的处理过程
-
当用户手指在Setting应用界面滑动时,系统system_server进程中的Native线程InputReader会从EventHub中读取,利用Linux的epoll机制监听到的屏幕驱动上报的Input触控事件,然后唤醒另一条Native线程InputDIspatcher负责进行事件的进一步分发处理。
-
InputDispatcher被唤醒后会先将事件放到InboundQueue队列("iq")中,然后找到具体处理此Input事件的应用目标窗口,并将Input事件放入对应的应用目标窗口的OutboundQueue队列("oq")中,等待进一步通过SocketPair双工信道发送Input事件到应用目标窗口中。
-
最后当事件发送给具体的应用目标窗口后,会将事件移动到WaitQueue队列("wq")中,一直等待收到目标应用处理Input事件完成后反馈后再从队列中移除,如果5秒钟没有收到目标应用窗口处理完成此次Input事件的反馈,就会报该应该ANR异常事件。以上整个过程在Android系统AOSP源码中都加有相应的Systrace tag,如下图Systrace所示:
应用进程的处理过程
当Input触控时间通过socket传递到Setting应用进程这边后,会唤醒应用的UI线程在ViewRootImpl#deliverInputEvent的流程中进行Input事件的具体分发与处理。具体的处理流程:
先交给之前在添加应用PhoneWindow窗口时的ViewRootImpl#setView流程中创建的多个不同类型的InputUsage中依次进行处理(比如对输入法处理逻辑的封装ImeInputUsage,某些key类型的Input事件会由它先交给输入法进程处理完成后再交给应用窗口的InputUsage处理),整个处理流程是按照责任链的设计模式进行;
然后会交给负责应用窗口Input事件分发处理的ViewPostImeInputUsage中具体处理,这里面会从View布局树的根节点DecorView开始遍历整个View树上的每一个子View或ViewGroup控件执行事件的分发、拦截、处理的逻辑;
最后触控时间处理完成后会调用finishInputEvent结束应用对触控事件处理逻辑,这里面会通过JNI调用到Native层InputConsumer的sendFinishedSignal函数中通过socket消息通知系统框架中的InputDIspatcher该Input事件处理完成,触发从"wq"队列中及时移除待处理事件以免报ANR异常。
一次滑动过程的触控交互的InputResponse区域中一般会包含一个Input的ACTION_DOWN事件 + 多个ACTION_MOVE事件 + 一个ACTION_UP事件,Setting应用界面中的相关View控件在收到多个ACTION_MOVE触控事件后,经过判断用户手指滑动行为,一般会调用View#invalidate等相关接口触发UI线程的绘制上帧更新画面的操作。
Android Input初始化及InputReader流程
概述
当输入设备可用时,Linux内核会在/dev/input/下创建对应名为event0~n或其他名称的设备节点。而当输入设备不可用时,则会将对应的节点删除。
Android输入系统的工作原理概括来说,就是监控/dev/input/下的所有设备节点,当某个节点有数据可读时,将数据读出并进行一系列的翻译加工,然后再所有的窗户中寻找合适的事件接收者,并派发给它。
getevent与sendevent
getevent与sendevent两个工具可以从设备节点中直接读取输入事件或写入输入事件。
Getevent
由于getevent不会对事件数据做任何加工,因此其输出的内容是由内核提供的最原始的事件,输出是十六进制的:
adb shell getevent [-选项] [device_path]。
adb shell getevent –t 查看当前按下按键的值,值0x01表示按下,0x00则表示抬起。
按下返回
[1556162527.777123] /dev/input/event6: 0001 009e 00000001
[1556162527.777123] /dev/input/event6: 0000 0000 00000000
送开返回
[1556162530.504152] /dev/input/event6: 0003 0030 00000000
[1556162530.504152] /dev/input/event6: 0003 0032 00000000
[1556162530.504152] /dev/input/event6: 0000 0002 00000000
[1556162530.504152] /dev/input/event6: 0000 0000 00000000
[1556162530.525527] /dev/input/event6: 0001 009e 00000000
[1556162530.525527] /dev/input/event6: 0000 0000 00000000
事件类型(0001),事件代码(009e)以及事件的值(00000001)。
Sendevent
实现模拟用户输入的功能,sendevent的参数为十进制。
sendevent <节点路径> <类型><代码> <值>
adb shell sendevent /dev/input/event0 1 116 1 #按下电源键
adb shell sendevent /dev/input/event0 1 116 0 #抬起电源键
adb shell input keyevent 4 # 返回按键
Android系统Input事件处理流程
内核将原始事件写入到设备节点中,InputReader不断地通过EventHub将原始事件取出来并翻译加工成Android输入事件,然后交给InputDIspatcher。InputDIspatcher根据WMS提供的窗口信息将事件交给合适的窗口。窗口的ViewRootImpl对象再沿着控件树将事件派发给感兴趣的控件。控件对其收到的事件做出响应,更新自己的画面、执行特定的动作。
Linux内核,接收输入设备的中断,并将原始事件的数据写入到设备节点中。
设备节点,作为内核的IMS的桥梁,它将原始事件的数据暴露给用户控件,以便IMS可以从中读取事件。
InputManagerService,一个Android系统服务,它分为Java层和Native层两个部分。Java层负责与WMS的通信。而Native层则是InputReader和InputDIspatcher两个输入系统关键组件的运行容器。
EventHub,直接访问所有的设备节点。并且正如其名字所描述的,它通过一个名为getEvents()函数将所有输入系统相关的待处理的底层事件返回给使用者。这些事件包括原始输入事件、设备节点的增删等。
InputReader,是IMS中的关键组件之一。它运行于一个独立的线程中,负责管理输入设备的列表与配置,以及进行输入事件的加工处理。它通过其线程循环不断地通过getEvent()函数从EventHub中将事件取出并进行处理。对于设备节点的增删事件,它会更新输入设备列表于配置。对于原始输入事件,InputReader对其进行翻译、组装、封装为包含了更多信息、更具可读性的输入事件,然后交给InputDIspatcher进行派发。
InputReaderPolicy,它为InputReader的事件加工处理提供一些策略配置,例如键盘布局信息等。
InputDispatcher,是IMS中另一个关键组件。它也运行于一个独立的线程中。InputDIspatcher中保管了来自WMS的所有窗口的信息,其收到来自InputReader的输入事件后,会在其保管的窗口中寻找合适的窗口,并将事件派发给此窗口。
InputDIspatcherPolicy,它为InputDIspatcher的派发过程提供策略控制。例如截取某些特定的输入事件来作特殊用途,或者阻止将某些事件派发给目标窗口。一个典型的例子就是HOME键被InputDIspatcherPolicy截取到PhoneWindowManager中进行处理,并阻止窗口收到HOME键按下的事件。
WMS,虽然不是输入系统中的一员,但是它却对InputDIspatcher的正常工作起到了至关重要的作用。当新建窗口时,WMS为新窗口和IMS创建了事件传递所用的通道。另外,WMS还将所有窗口的信息,包括窗口的可点击区域,焦点窗口等信息,实时地更新到IMS的InputDIspatcher中,使得InputDIspatcher可以正确的将事件派发到指定的窗口。
ViewRootImpl,对于某些窗口,如壁纸窗口、SurfaceView的窗口来说,窗口即是输入事件派发的终点。而对于其他的如Activity、对话框等使用了Android控件系统的窗口来说,输入事件的终点是控件(View)。ViewRootImpl将窗口所收到的输入事件沿着控件树将事件派发给感兴趣的控件。
初始化
InputManagerService初始化
这段代码是Android系统中InputManagerService的启动过程。首先,在SystemServer服务的
startOtherServices()
方法中,创建并启动了InputManagerService实例。然后,在InputManagerService的构造函数中,初始化了Native层和Java层的通信,并将Java层的InputManagerService对象和消息队列的Looper传递给Native层。最后,在nativeInit方法中,创建了一个NativeInputManager对象,这个对象是Java层和Native层之间的桥梁,并将这个对象的指针返回给Java层的InputManagerService,保存在mPtr成员变量中。具体来说,
startOtherServices()
方法中:
创建了InputManagerService实例。
调用了InputManagerService的
setWindowManagerCallbacks()
方法,将WindowManager的回调传递给InputManagerService。调用了InputManagerService的
start()
方法,启动了InputManagerService。在
InputManagerService
的构造函数中:
保存了上下文信息。
创建了一个名为
InputManagerHandler
的Handler,用于处理输入事件。调用了native方法
nativeInit()
,传入了上下文对象、InputManagerService对象和消息队列的Looper。将InputManagerService注册为
InputManagerInternal
服务的本地服务。在
nativeInit()
方法中:
从Java层的消息队列对象中获取了消息队列。
创建了一个
NativeInputManager
对象,这个对象是Java层和Native层之间的桥梁。将
NativeInputManager
对象的指针转换为jlong
类型,并返回给Java层的InputManagerService,保存在mPtr成员变量中。这样,Java层的InputManagerService就可以通过mPtr成员变量与Native层的
NativeInputManager
进行通信了。这个过程是Android系统启动过程中的一部分,用于初始化和管理输入事件,包括触摸、按键等输入事件的处理。
* frameworks/base/services/java/com/android/server/SystemServer.java
private void startOtherServices() {...traceBeginAndSlog("StartInputManagerService");inputManager = new InputManagerService(context);traceEnd();...traceBeginAndSlog("StartInputManager");inputManager.setWindowManagerCallbacks(wm.getInputManagerCallback());inputManager.start();traceEnd();...
}* frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
public InputManagerService(Context context) {this.mContext = context;this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper());...mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());...LocalServices.addService(InputManagerInternal.class, new LocalService());
}* frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
static jlong nativeInit(JNIEnv* env, jclass /* clazz */,jobject serviceObj, jobject contextObj, jobject messageQueueObj) {sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);.../* 新建了一个NativeInputManager对象,NativeInputManager,此对象将是Native层组件与Java层IMS进行通信的桥梁 */NativeInputManager* im = new NativeInputManager(contextObj, serviceObj, messageQueue->getLooper());im->incStrong(0);//返回了NativeInputManager对象的指针给Java层的IMS,IMS将其保存在mPtr成员变量中return reinterpret_cast<jlong>(im);
}
在nativeInit函数中,将Java层的MessageQueue转换为native层的MessageQueue,然后再取出Looper用于NativeInputManager的初始化。NativeInputManager是Java层与Native层互相通信的桥梁,它实现了InputReaderPolicyInterface与InputDispatcherPolicyInterface两个接口,通过JNI回调Java层的IMS,由它完成决策。
NativeInputManager的初始化
这个过程做了以下事情:
将Java层的InputManagerService转换为native层的InputManagerService存储在mServiceObj中 -创建InputManager。
这段代码是Android系统中InputManager的初始化过程。在InputManager的构造函数中,首先创建了InputDispatcher和InputClassifier对象,这两个对象分别负责分发输入事件和输入事件的分类。然后,创建了InputReader对象,这个对象负责从设备读取输入事件。最后,创建了InputReaderThread和InputDispatcherThread两个线程,分别用于处理输入事件的读取和分发。
具体来说:
InputManager
的构造函数中:
创建了
InputDispatcher
对象,用于分发输入事件。创建了
InputClassifier
对象,用于对输入事件进行分类。调用
createInputReader()
方法创建了InputReader
对象,这个对象负责从设备读取输入事件。调用
initialize()
方法,创建了InputReaderThread
和InputDispatcherThread
两个线程。
createInputReader()
方法中:
创建了
InputReader
对象,这个对象负责从设备读取输入事件。
initialize()
方法中:
创建了
InputReaderThread
对象,这个线程用于处理输入事件的读取。创建了
InputDispatcherThread
对象,这个线程用于处理输入事件的分发。这样,InputManager就可以通过这两个线程来处理输入事件了。
这个过程是Android系统启动过程中的一部分,用于初始化和管理输入事件,包括触摸、按键等输入事件的处理。
* frameworks/native/services/inputflinger/InputManager.cpp
InputManager::InputManager(const sp<InputReaderPolicyInterface>& readerPolicy,const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {mDispatcher = new InputDispatcher(dispatcherPolicy);mClassifier = new InputClassifier(mDispatcher);mReader = createInputReader(readerPolicy, mClassifier);initialize();
}sp<InputReaderInterface> createInputReader(const sp<InputReaderPolicyInterface>& policy,const sp<InputListenerInterface>& listener) {return new InputReader(new EventHub(), policy, listener);
}void InputManager::initialize() {mReaderThread = new InputReaderThread(mReader);mDispatcherThread = new InputDispatcherThread(mDispatcher);
}
InputManager的初始化
InputManager的构造函数也比较简洁,它创建了四个对象,分别为IMS的核心参与者InputReader与InputDispatcher,以及它们所在的线程InputReaderThread与InputDispatcherThread。注意InputManager的构造函数的参数readerPolicy与dispatcherPolicy,它们都是NativeInputManager。
InputDispatcher会创建自己线程的Looper,以及设置根据传入的dispatchPolicy设置分发规则。InputReader则会将传入的InputDispatcher封装为监听对象存起来,并创建一个EventHub。
启动
system_server执行InputManagerService.start()函数以启动IMS,在start()方法中,做了以下事情:
调用nativeStart方法,其实就是调用InputManager的start()方法。
将InputManagerService交给WatchDog监控。
注册触控点速度、显示触控的观察者,并注册广播监控它们。
主动调用updateXXX方法更新(初始化)。
InputManager的start()启动InputDispatcherThread和InputReaderThread开始监听。
当两个线程启动后,InputReader在其线程循环中不断地从EventHub中抽取原始输入事件,进行加工处理后将加工所得的事件放入InputDispatcher的派发发队列中。InputDispatcher则在其线程循环中将派发队列中的事件取出,查找合适的窗口,将事件写入到窗口的事件接收管道中。窗口事件接收线程的Looper从管道中将事件取出,交由事件处理函数进行事件响应。
这段代码是Android系统中InputManagerService的启动过程。在Java层的
start()
方法中,调用了native方法nativeStart()
,传入了mPtr成员变量,这个变量是Java层和Native层之间的桥梁。在Native层的
InputManager
的start()
方法中,首先启动了InputDispatcherThread
线程,这个线程负责分发输入事件。然后,启动了InputReaderThread
线程,这个线程负责从设备读取输入事件。具体来说:
InputManagerService
的start()
方法中:
调用了native方法
nativeStart()
,传入了mPtr成员变量,这个变量是Java层和Native层之间的桥梁。
InputManager
的start()
方法中:
调用
mDispatcherThread->run()
方法,启动了InputDispatcherThread
线程,这个线程负责分发输入事件。调用
mReaderThread->run()
方法,启动了InputReaderThread
线程,这个线程负责从设备读取输入事件。这样,InputManager就可以通过这两个线程来处理输入事件了。
这个过程是Android系统启动过程中的一部分,用于初始化和管理输入事件,包括触摸、按键等输入事件的处理。
* frameworks/base/services/core/java/com/android/server/input/InputManagerService.javapublic void start() {Slog.i(TAG, "Starting input manager");nativeStart(mPtr);...updatePointerSpeedFromSettings();updateShowTouchesFromSettings();updateAccessibilityLargePointerFromSettings();
}* frameworks/native/services/inputflinger/InputManager.cpp
status_t InputManager::start() {status_t result = mDispatcherThread->run("InputDispatcher", PRIORITY_URGENT_DISPLAY);...result = mReaderThread->run("InputReader", PRIORITY_URGENT_DISPLAY);...
}
InputReaderThread
启动后循环执行mReader->loopOnce(),loopOnce中会调用mEventHub->getEvents读取事件,读取的结果存储在参数mEventBuffer中,返回值表示事件的个数,当EventHub中无事件可以抽取时,此函数的调用将会阻塞直到事件到来或者超时。
读取到了事件就会调用processEventsLocked处理事件,对于设备事件,此函数对根据设备的可用性加载或移除设备对应的配置信息。对于原始输入事件,则在进行转译、封装与加工后将结果暂存到mQueuedListener中,处理完成后调用getInputDevicesLocked获取输入设备信息。
调用mPolicy->notifyInputDevicesChanged函数利用InputManagerService的代理通过Handler发送MSG_DELIVER_INPUT_DEVICES_CHANGED消息,通知输入设备发生了变化。
最后调用mQueuedListener->flush(),将事件队列中的所有事件交给在InputReader中注册过的InputDIspatcher。
注意 C++层的Thread类与Java层的Thread类有着一个显著的不同。C++层Thread类内建了线程循环,threadLoop()就是一次循环而已,只要返回值为true,threadLoop()将会不断地被内建的循环调用。这也是InputReader.loopOnce()函数名称的由来。而Java层Thread类的run()函数则是整个线程的全部,一旦其退出,线程也便完结。
这段代码是Android系统中InputReader的循环处理过程。在Native层的
InputReaderThread
的threadLoop()
方法中,调用了InputReader
的loopOnce()
方法,这个方法会循环读取输入事件。在
InputReader
的loopOnce()
方法中,首先从EventHub
中读取输入事件,然后处理这些事件。如果输入设备发生了变化,就通知InputReaderPolicy
。最后,调用mQueuedListener
的flush()
方法,将处理过的输入事件发送给监听者。具体来说:
InputReaderThread
的threadLoop()
方法中:
调用了
mReader
的loopOnce()
方法,这个方法会循环读取输入事件。
InputReader
的loopOnce()
方法中:
从
EventHub
中读取输入事件。处理这些事件。
如果输入设备发生了变化,就通知
InputReaderPolicy
。调用
mQueuedListener
的flush()
方法,将处理过的输入事件发送给监听者。这样,
InputReader
就可以通过这个循环来处理输入事件了。这个过程是Android系统启动过程中的一部分,用于初始化和管理输入事件,包括触摸、按键等输入事件的处理。
* frameworks/native/services/inputflinger/InputReaderBase.cpp
bool InputReaderThread::threadLoop() {mReader->loopOnce();return true;
}* frameworks/native/services/inputflinger/InputReader.cpp
void InputReader::loopOnce() {...size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);{ // acquire lockAutoMutex _l(mLock);mReaderIsAliveCondition.broadcast();if (count) {processEventsLocked(mEventBuffer, count);}...if (oldGeneration != mGeneration) {inputDevicesChanged = true;getInputDevicesLocked(inputDevices);}} // release lock// Send out a message that the describes the changed input devices.if (inputDevicesChanged) {mPolicy->notifyInputDevicesChanged(inputDevices);}mQueuedListener->flush();
}
EventHub
InputReader在其线程循环中的第一个工作便是从EventHub中读取一批未处理的事件。 EventHub的直译是事件集线器,顾名思义,它将所有的输入事件通过一个接口getEvents()将从多个输入设备节点中读取的事件交给InputReader,是输入系统最底层的一个组件。
这段代码是Android系统中EventHub的初始化过程。在EventHub的构造函数中,首先创建了一个epoll对象和inotify对象,用于监听设备节点的增删事件。然后,将设备节点的路径/dev/input作为监听对象添加到inotify对象中,当此文件夹下的设备节点发生创建与删除事件时,都可以通过inotify对象读取事件的详细信息。接着,将inotify对象和名为wakeFds的匿名管道(用于唤醒InputReader线程)作为epoll的监控对象,并注册到epoll对象中。
具体来说:
EventHub的构造函数中:
创建了一个epoll对象。
创建了一个inotify对象,用于监听设备节点的增删事件。
将设备节点的路径/dev/input作为监听对象添加到inotify对象中。
创建了一个名为wakeFds的匿名管道,并将管道读取端的描述符的可读事件注册到epoll对象中。
在epoll对象中注册了inotify对象和wakeFds管道,用于监听设备节点的增删事件和唤醒InputReader线程。
这样,EventHub就可以通过epoll和inotify来监听设备节点的变化,并及时处理这些事件了。
这个过程是Android系统启动过程中的一部分,用于初始化和管理输入事件,包括触摸、按键等输入事件的处理。
* frameworks/native/services/inputflinger/EventHub.cpp
EventHub::EventHub(void) :mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD), mNextDeviceId(1), mControllerNumbers(),mOpeningDevices(nullptr), mClosingDevices(nullptr),mNeedToSendFinishedDeviceScan(false),mNeedToReopenDevices(false), mNeedToScanDevices(true),mPendingEventCount(0), mPendingEventIndex(0), mPendingINotify(false) {acquire_wake_lock(PARTIAL_WAKE_LOCK, WAKE_LOCK_ID);//创建一个epoll对象mEpollFd = epoll_create1(EPOLL_CLOEXEC);LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance: %s", strerror(errno));//创建一个inotify对象。这个inotify对象将被用来监听设备节点的增删事件mINotifyFd = inotify_init();//将存储设备节点的路径/dev/input作为监听对象添加到inotify对象中。当此文件夹下的设备节点//发生创建与删除事件时,都可以通过mINotifyFd读取事件的详细信息mInputWd = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);...struct epoll_event eventItem;memset(&eventItem, 0, sizeof(eventItem));eventItem.events = EPOLLIN;eventItem.data.fd = mINotifyFd;//将mINotifyFd作为epoll的一个监控对象。当inotify事件到来时,epoll_wait()将//立刻返回,EventHub便可从mINotifyFd中读取设备节点的增删信息,并作相应处理int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);LOG_ALWAYS_FATAL_IF(result != 0, "Could not add INotify to epoll instance. errno=%d", errno);/*创建了一个名为wakeFds的匿名管道,并将管道读取端的描述符的可读事件注册到epoll对象中。因为InputReader在执行getEvents()时会因无事件而导致其线程阻塞在epoll_wait()的调用里,然而有时希望能够立刻唤醒InputReader线程使其处理一些请求。此时只需向wakeFds管道的写入端写入任意数据,此时读取端有数据可读,使得epoll_wait()得以返回,从而达到唤醒InputReader线程的目的*/int wakeFds[2];result = pipe(wakeFds);LOG_ALWAYS_FATAL_IF(result != 0, "Could not create wake pipe. errno=%d", errno);mWakeReadPipeFd = wakeFds[0];mWakeWritePipeFd = wakeFds[1];...eventItem.data.fd = mWakeReadPipeFd;result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);...
}
INotify
INotify是一个Linux内核所提供的一个文件系统变化通知机制。它可以为应用程序监控文件系统的变化,如文件的创建、删除、读写等。INotify机制有两个基本对象,分别为inotify对象与watch对象,都使用文件描述符表示。
-
inotify对象对应了一个队列,应用程序可以向inotify对象添加多个监听。当被监听的事件发生时,可以通过read()函数从inotify对象中将事件信息读取出来。Inotify对象可以通过inotify_init创建。
-
watch对象则用来描述文件系统的变化事件的监听。它是一个二元组,包括监听目标和事件掩码两个元素。监听目标是文件系统的一个路径,可以是文件也可以是文件夹。而事件掩码则表示了需要需要监听的事件类型,掩码中的每一位代表一种事件。可以监听的事件种类很多,其中就包括文件的创建(IN_CREATE)与删除(IN_DELETE)。以下代码即可将一个用于监听输入设备节点的创建与删除的watch对象添加到inotify对象中:
int wd = inotify_add_watch (inotifyFd, “/dev/input”,IN_CREATE | IN_DELETE);
完成上述watch对象的添加后,当/dev/input/下的设备节点发生创建与删除操作时,都会将相应的事件信息写入到inotifyFd所描述的inotify对象中,此时可以通过read()函数从inotifyFd描述符中将事件信息读取出来。
事件信息使用结构体inotify_event进行描述:
struct inotify_event {__s32 wd; /* 事件对应的Watch对象的描述符 */__u32 mask; /* 事件类型,例如文件被删除,此处值为IN_DELETE */__u32 cookie;__u32 len; /* name字段的长度 */char name[0]; /* 可变长的字段,用于存储产生此事件的文件路径*/
};
当有监听事件发生时,可以通过如下方式将一个或多个未读取的事件信息读取出来:
size_t len = read (inotifyFd, events_buf,BUF_LEN);
其中events_buf是inotify_event的数组指针,能够读取的事件数量由取决于数组的长度。成功读取事件信息后,便可根据inotify_event结构体的字段判断事件类型以及产生事件的文件路径了。
INotify机制的使用过程
通过inotify_init()创建一个inotify对象。
通过inotify_add_watch将一个或多个监听添加到inotify对象中。
通过read()函数从inotify对象中读取监听事件。当没有新事件发生时,inotify对象中无任何可读数据。
通过INotify机制避免了轮询文件系统的麻烦,但是还有一个问题,INotify机制并不是通过回调的方式通知事件,而需要使用者主动从INotify对象中进行事件读取,这里借助Linux的Epoll机制。
Epoll
Epoll可以使用一次等待监听多个描述符的可读/可写状态。等待返回时携带了可读的描述符或自定义的数据,使用者可以据此读取所需的数据后可以再次进入等待。因此不需要为每个描述符创建独立的线程进行阻塞读取,避免了资源浪费的同时又可以获得较快的响应速度。
epoll_create(int max_fds):创建一个epoll对象的描述符,之后对epoll的操作均使用这个描述符完成。max_fds参数表示了此epoll对象可以监听的描述符的最大数量。
int epoll_create1(int flag);
当flag是0时,表示和epoll_create函数完全一样,不需要size的提示了
当flag = EPOLL_CLOEXEC,创建的epfd会设置FD_CLOEXEC
当flag = EPOLL_NONBLOCK,创建的epfd会设置为非阻塞
FD_CLOEXEC是fd的一个标识说明,用来设置文件close-on-exec状态的。当close-on-exec状态为0时,调用exec时,fd不会被关闭;状态非零时则会被关闭,这样做可以防止fd泄露给执行exec后的进程
epoll_ctl (int epfd, int op,int fd, struct epoll_event *event):用于管理注册事件的函数。这个函数可以增加/删除/修改事件的注册。
op表示了何种操作,包括EPOLL_CTL_ADD/DEL/MOD三种,分别表示增加/删除/修改注册事件。
int epoll_wait(int epfd, structepoll_event * events, int maxevents, int timeout):用于等待事件的到来。当此函数返回时,events数组参数中将会包含产生事件的文件描述符。函数返回值表示获取了多少个事件。
Epoll机制的使用过程
创建epoll对象:
Int epfd = epoll_create(MAX_FDS)
。填充epoll_event结构体,以描述监控事件。
通过epoll_ctl()函数将此描述符与epoll_event结构体注册进epoll对象,重复这个步骤可以将多个文件描述符的多种事件监听注册到epoll对象中。
使用epoll_wait()函数等待事件,会使调用者陷入等待状态,直到其注册的事件之一发生之后才会返回,并且携带了刚刚发生的事件的详细信息,在如getEvents中调用。
EventHub的创建过程
- 创建mEpollFd用于监听是否有数据(有无事件)可读。
- 创建mINotifyFd将它注册到DEVICE_PATH(这里路径就是/dev/input)节点,并将它交给内核用于监听该设备节点的增删数据事件。那么只要有数据增删的事件到来,epoll_wait()就会返回,使得EventHub能收到来自系统的通知,并获取事件的详细信息。
- 调用epoll_ctl函数将mEpollFd和mINotifyFd注册到epoll中。
- 定义int wakeFd[2]作为事件传输管道的读写两端,并将读端注册到epoll中让mEpollFd监听。
EventHub
是 Android 系统中负责处理输入事件的类,它位于frameworks/native/services/inputflinger
目录下。这个类的主要职责是监听和管理/dev/input
目录下的所有输入设备,并将这些设备的事件转换为 Android 系统可以理解的事件格式。在
getEvents
方法中,它首先检查是否需要扫描设备,如果需要,它会调用scanDevicesLocked
方法来遍历/dev/input
目录下的所有设备,打开这些设备并存储到Device
结构体中,同时将这些设备的文件描述符的可读事件注册到 Epoll 中。当设备的输入事件到来时,Epoll 会在getEvents
函数的调用中产生一条 epoll 事件。然后,它遍历
mClosingDevices
链表,为每一个已卸载的设备生成DEVICE_REMOVED
事件。接着,它通过 Epoll 事件的
data
字段确定此事件表示了mINotifyFd
可读,即 Epoll 事件的处理。如果mPendingINotify
事件待处理,它会调用readNotifyLocked
函数来读取并处理存储在mINotifyFd
中的 INotify 事件。这个过程会一直重复,直到满足某个条件(例如超时或者缓冲区满)。
这个过程的关键点在于
scanDevicesLocked
和readNotifyLocked
方法,它们分别负责扫描设备和处理 INotify 事件。这两个方法的具体实现细节没有在提供的代码片段中展示,但它们应该是用来打开设备、注册 Epoll 事件、处理设备卸载和 INotify 事件的。请注意,这是一个简化的解释,实际的实现可能会更复杂,包括错误处理、线程同步、设备状态管理等。
* frameworks/native/services/inputflinger/EventHub.cpp
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {...for (;;) {...if (mNeedToScanDevices) {mNeedToScanDevices = false;//遍历/dev/input下所有可用的输入设备打开并存储到Device结构体//将设备节点的描述符的可读事件注册到Epoll中,当此设备的输入事件到来时,Epoll会在getEvents()函数的调用中产生一条epoll事件scanDevicesLocked();mNeedToSendFinishedDeviceScan = true;}//遍历mClosingDevices链表,为每一个已卸载的设备生成DEVICE_REMOVED事件while (mClosingDevices) {...}// 通过Epoll事件的data字段确定此事件表示了mINotifyFd可读,及Epoll事件的处理while (mPendingEventIndex < mPendingEventCount) {}//如果INotify事件待处理if (mPendingINotify && mPendingEventIndex >= mPendingEventCount) {mPendingINotify = false;//调用readNotifyLocked()函数读取并处理存储在mINotifyFd中的INotify事件readNotifyLocked();deviceChanged = true;}}
}
getEvents()通过Epoll事件的data.u32字段在mDevices列表中查找已加载的设备,并从设备的文件描述符中读取原始输入事件列表。从文件描述符中读取的原始输入事件存储在input_event结构体中,这个结构体的四个字段存储了事件的事件戳、类型、代码与值四个元素。然后逐一将input_event的数据转存到RawEvent中并保存至buffer以返回给调用者。
InputReader
InputReader是在InputReaderThread中启动的,InputReaderThread和InputDispatcherThread的定义是类似的,也是继承了Thread并定义了threadLoop纯虚函数。如果处理的事件为键盘输入事件,则调用时序图如下所示。
原始事件的入口函数是ProcessEventsLocked,根据事件类型区分原始输入事件还是设备增删事件,对于原始输入事件,EventHub会讲属于同意输入设备的原始输入事件放在一起,因此使用processEventsForDeviceLocked同时处理来自同意输入设备的一批事件。
InputReader
是 Android 系统中负责处理输入事件的类,它位于frameworks/native/services/inputflinger
目录下。这个类的主要职责是接收和处理来自EventHub
的原始输入事件。在
processEventsLocked
方法中,它遍历所有的原始事件。对于每一条原始事件,根据事件类型,它会调用processEventsForDeviceLocked
方法来处理。如果事件类型是原始输入事件,那么它会调用InputDevice
的process
方法来处理。如果事件类型是设备增删事件,那么它会处理设备增删事件。在
processEventsForDeviceLocked
方法中,它首先检查设备是否存在,如果设备被忽略,那么它会忽略这条事件。如果设备存在,那么它会调用InputDevice
的process
方法来处理事件。这个过程的关键点在于
InputDevice
的process
方法,它是用来处理输入事件的。这个方法的具体实现细节没有在提供的代码片段中展示,但它是用来处理输入事件的。请注意,这是一个简化的解释,实际的实现可能会更复杂,包括错误处理、设备状态管理等。
* frameworks/native/services/inputflinger/InputReader.cpp
void InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) {for (const RawEvent* rawEvent = rawEvents; count;) {int32_t type = rawEvent->type;size_t batchSize = 1;//根据事件类型区分原始输入事件还是设备增删事件if (type < EventHubInterface::FIRST_SYNTHETIC_EVENT) {int32_t deviceId = rawEvent->deviceId;...//数据事件的处理,batchSize表示属于此设备的输入事件个数processEventsForDeviceLocked(deviceId, rawEvent, batchSize);} else {//处理设备增删事件...}count -= batchSize;rawEvent += batchSize;}
}void InputReader::processEventsForDeviceLocked(int32_t deviceId,const RawEvent* rawEvents, size_t count) {//InputReader保存了mDevices字典,以设备ID为键值存储了一系列的InputDevice对象ssize_t deviceIndex = mDevices.indexOfKey(deviceId);if (deviceIndex < 0) {ALOGW("Discarding event for unknown deviceId %d.", deviceId);return;}InputDevice* device = mDevices.valueAt(deviceIndex);if (device->isIgnored()) {//ALOGD("Discarding event for ignored deviceId %d.", deviceId);return;}//调用InputDevice的process处理事件device->process(rawEvents, count);
}
processEventsForDeviceLocked中,InputReader保存了mDevices字典,在processEventsLocked的处理设备增删阶段初始化,然后调用InputDevice::process处理事件。
InputDevice
InputReader的InputDevice存储输入设备的信息,与EventHub一样,InputDevice描述了一个输入设备,并且以设备ID为键保存在mDevices中,InputReader::InputDevice和EventHub::Device结构体类似,也保存了设备的ID、厂商信息、设备所属类别。只是InputDevice多了一个InputMapper列表。
InputMapper
InputMapper是InputReader中实际进行原始输入事件加工的场所,有一系列子类,分别用于加工不同类型的原始输入事件。这样使得InputDevice不需要知道哪一个InputMapper可以处理一个原始输入事件,只须将一个事件逐一交给每一个InputMapper尝试处理,如果InputMapper可以接受这个事件则处理,否则什么都不做。
InputReader
类是 Android 系统中负责管理输入设备和处理输入事件的类,它位于frameworks/native/services/inputflinger
目录下。在
addDeviceLocked
方法中,它首先检查是否已经添加了相同的设备,如果已经添加了,那么它会忽略这条设备添加事件。然后,它获取设备标识符、设备类和设备控制器编号,并调用createDeviceLocked
方法来创建一个新的InputDevice
对象。在
createDeviceLocked
方法中,它创建一个新的InputDevice
对象,并根据设备类添加相应的InputMapper
。例如,如果设备是键盘类设备,它会添加KeyboardInputMapper
;如果设备是触摸屏设备,它会添加MultiTouchInputMapper
或SingleTouchInputMapper
。这个过程的关键点在于
InputDevice
的创建和InputMapper
的添加。InputDevice
是用来管理设备和处理事件的,而InputMapper
是用来处理特定类型的输入事件的。请注意,这是一个简化的解释,实际的实现可能会更复杂,包括错误处理、设备状态管理等。
void InputReader::addDeviceLocked(nsecs_t when, int32_t deviceId) {ssize_t deviceIndex = mDevices.indexOfKey(deviceId);if (deviceIndex >= 0) {//已添加的相同设备则不再添加ALOGW("Ignoring spurious device added event for deviceId %d.", deviceId);return;}InputDeviceIdentifier identifier = mEventHub->getDeviceIdentifier(deviceId);uint32_t classes = mEventHub->getDeviceClasses(deviceId);int32_t controllerNumber = mEventHub->getDeviceControllerNumber(deviceId);InputDevice* device = createDeviceLocked(deviceId, controllerNumber, identifier, classes);device->configure(when, &mConfig, 0);device->reset(when);
...mDevices.add(deviceId, device);...
}InputDevice* InputReader::createDeviceLocked(int32_t deviceId, int32_t controllerNumber,const InputDeviceIdentifier& identifier, uint32_t classes) {InputDevice* device = new InputDevice(&mContext, deviceId, bumpGenerationLocked(),controllerNumber, identifier, classes);...//添加键盘类设备InputMapperif (keyboardSource != 0) {device->addMapper(new KeyboardInputMapper(device, keyboardSource, keyboardType));}...//添加触摸屏设备InputMapperif (classes & INPUT_DEVICE_CLASS_TOUCH_MT) {device->addMapper(new MultiTouchInputMapper(device));} else if (classes & INPUT_DEVICE_CLASS_TOUCH) {device->addMapper(new SingleTouchInputMapper(device));}...
}
InputDevice::process首先会遍历InputDevice中的所有的事件,真正加工原始输入事件的是InputMapper对象,由于原始输入事件的类型很多,因此在InputMapper有很多子类,用于加工不同的原始输入事件,比如KeyboardInputMapper用于处理键盘输入事件,TouchInputMapper用于处理触摸输入事件。
InputDevice
类是 Android 系统中负责处理输入事件的类,它位于frameworks/native/services/inputflinger
目录下。在
process
方法中,它遍历所有的原始事件。如果mDropUntilNextSync
的值为true
,那么它会忽略当前事件。如果mDropUntilNextSync
的值为false
,那么它会遍历所有的InputMapper
,并调用它们的process
方法来处理事件。这个过程的关键点在于
InputMapper
的process
方法,它是用来处理输入事件的。这个方法的具体实现细节没有在提供的代码片段中展示,但它是用来处理输入事件的。请注意,这是一个简化的解释,实际的实现可能会更复杂,包括错误处理、设备状态管理等。
void InputDevice::process(const RawEvent* rawEvents, size_t count) {for (const RawEvent* rawEvent = rawEvents; count != 0; rawEvent++) {...//mDropUntilNextSync的值默认为false,如果设备的输入事件缓冲区溢出,这个值会置为true。if (mDropUntilNextSync) {}else{for (InputMapper* mapper : mMappers) {mapper->process(rawEvent);}}...--count;}
}
KeyboardInputMapper
是 Android 系统中负责处理键盘输入事件的类,它位于frameworks/native/services/inputflinger
目录下。在
process
方法中,它根据事件类型来处理键盘事件。当事件类型为EV_KEY
时,它会调用processKey
方法来处理。在processKey
方法中,它会根据扫描码和使用码来获取按键码、元状态和策略标志。如果按键被按下,它会根据屏幕的方向对按键的虚拟键值进行旋转转换,并检查是否应该丢弃虚拟键。如果应该丢弃,它会取消触摸并返回。然后,它会生成一个
KeyDown
结构体并将其添加到集合中。如果按键被抬起,它会从集合中移除对应的
KeyDown
对象。最后,它会生成一个
NotifyKeyArgs
对象,并调用getListener
方法的notifyKey
方法来通知按键事件。在
mapKey
方法中,它首先获取设备,然后检查键字符映射。如果键字符映射存在,它会尝试根据扫描码和使用码来映射按键码。这个过程的关键点在于键字符映射和按键码的映射,以及按键事件的处理。
请注意,这是一个简化的解释,实际的实现可能会更复杂,包括错误处理、设备状态管理等。
void KeyboardInputMapper::process(const RawEvent* rawEvent) {switch (rawEvent->type) {case EV_KEY: {//表示为Keyboard事件int32_t scanCode = rawEvent->code;int32_t usageCode = mCurrentHidUsage;mCurrentHidUsage = 0;//排除对鼠标按键的处理if (isKeyboardOrGamepadKey(scanCode)) {processKey(rawEvent->when, rawEvent->value != 0, scanCode, usageCode);}break;}...}
}void KeyboardInputMapper::processKey(nsecs_t when, bool down, int32_t scanCode,int32_t usageCode) {int32_t keyCode;int32_t keyMetaState;uint32_t policyFlags;if (getEventHub()->mapKey(getDeviceId(), scanCode, usageCode, mMetaState,&keyCode, &keyMetaState, &policyFlags)) {keyCode = AKEYCODE_UNKNOWN;keyMetaState = mMetaState;policyFlags = 0;}if (down) {// 当按下时,首先需要根据屏幕的方向对按键的虚拟键值进行旋转转换if (mParameters.orientationAware) {keyCode = rotateKeyCode(keyCode, getOrientation());}// KeyboardInputMapper维护了一个mKeyDowns集合ssize_t keyDownIndex = findKeyDown(scanCode);if (keyDownIndex >= 0) {//对于重复按下的按键,需要确保后续的处理过程中的虚拟键值与第一次按下时的一致,以免重复过程中屏幕的方向的变化导致虚拟键值的变化,使得后续InputDispatch无法正常识别重复按键的动作keyCode = mKeyDowns[keyDownIndex].keyCode;} else {//生成keydown结构体并添加到集合中if ((policyFlags & POLICY_FLAG_VIRTUAL)&& mContext->shouldDropVirtualKey(when,getDevice(), keyCode, scanCode)) {return;}if (policyFlags & POLICY_FLAG_GESTURE) {mDevice->cancelTouch(when);}KeyDown keyDown;keyDown.keyCode = keyCode;keyDown.scanCode = scanCode;mKeyDowns.push_back(keyDown);}mDownTime = when;} else {ssize_t keyDownIndex = findKeyDown(scanCode);//对于抬起的按键,则将对应的keydown对象从集合中移除if (keyDownIndex >= 0) {keyCode = mKeyDowns[keyDownIndex].keyCode;mKeyDowns.erase(mKeyDowns.begin() + (size_t)keyDownIndex);} else {return;}}...NotifyKeyArgs args(mContext->getNextSequenceNum(), when, getDeviceId(), mSource,getDisplayId(), policyFlags, down ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP,AKEY_EVENT_FLAG_FROM_SYSTEM, keyCode, scanCode, keyMetaState, downTime);getListener()->notifyKey(&args);
}status_t EventHub::mapKey(int32_t deviceId,int32_t scanCode, int32_t usageCode, int32_t metaState,int32_t* outKeycode, int32_t* outMetaState, uint32_t* outFlags) const {AutoMutex _l(mLock);Device* device = getDeviceLocked(deviceId);status_t status = NAME_NOT_FOUND;if (device) {// Check the key character map first.sp<KeyCharacterMap> kcm = device->getKeyCharacterMap();if (kcm != nullptr) {if (!kcm->mapKey(scanCode, usageCode, outKeycode)) {*outFlags = 0;status = NO_ERROR;}}
}
EventHub::openDeviceLocked会为此设备加载键盘布局配置文件,键盘布局配置文件的路径位于/system/usr/keylayout下的.lk文件。
processKey函数会将加工后的键盘输入事件封装为NotifyKeyArgs,将NotifyKeyArgs通知给InputListenerInterface。
mArgsQueue的数据类型为Vector<NotifyArgs*>,将该key事件压人该vector中。
InputReader::loopOnce的最后调用mQueuedListener->flush()遍历整个mArgsQueue数组。
InputDispatcher继承了InputDispatcherInterface,而InputDispatcherInterface继承了InputListenerInterface,因此实际上是调用了InputDispatcher的notifyKey函数,将NotifyKeyArgs交给InputDispatcher处理。
QueuedInputListener
类是 Android 系统中负责接收和处理输入事件的类,它位于frameworks/native/services/inputflinger
目录下。在
notifyKey
方法中,它将接收到的按键事件参数添加到队列中。在
flush
方法中,它遍历队列中的所有事件,调用每个事件的notify
方法来处理事件,然后删除这些事件。最后,它清空队列。这个过程的关键点在于事件的接收和处理,以及事件队列的管理。
请注意,这是一个简化的解释,实际的实现可能会更复杂,包括错误处理、事件状态管理等。
* frameworks/native/services/inputflinger/InputListener.cpp
void QueuedInputListener::notifyKey(const NotifyKeyArgs* args) {mArgsQueue.push_back(new NotifyKeyArgs(*args));
}void QueuedInputListener::flush() {size_t count = mArgsQueue.size();for (size_t i = 0; i < count; i++) {NotifyArgs* args = mArgsQueue[i];args->notify(mInnerListener);delete args;}mArgsQueue.clear();
}
Android Input初始化及InputDIspatcher流程
唤醒InputDIspatcher
以Motion事件的分化过来进行举例。
InputDispatcher
类是 Android 系统中负责分发输入事件的类,它位于frameworks/native/services/inputflinger
目录下。在
notifyMotion
方法中,它首先检查 Motion 事件的参数是否有效,然后通过interceptMotionBeforeQueueing
方法来校验 Motion 事件的触点数量和 ID 是否在合理范围内。如果 Motion 事件需要通过输入过滤器,它会创建一个MotionEntry
对象并将其添加到入队列中。如果事件被过滤器消费,它会返回而不进行分发。在
enqueueInboundEventLocked
方法中,它将事件添加到入队列中,并根据事件类型进行优化处理。如果当前应用无响应且用户希望切换到其他应用窗口,它会查找触摸窗口。在
findTouchedWindowAtLocked
方法中,它遍历所有的窗口,检查坐标是否落在窗口之上。如果窗口是可触摸的,它会返回该窗口。这个过程的关键点在于事件的分发、过滤和窗口的查找。
请注意,这是一个简化的解释,实际的实现可能会更复杂,包括错误处理、事件状态管理等。
void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) {//检查Motion事件的参数是否有效,对于motion事件,主要时校验触控点的数量与ID是否在合理范围if (!validateMotionEvent(args->action, args->actionButton,args->pointerCount, args->pointerProperties)) {return;}uint32_t policyFlags = args->policyFlags;policyFlags |= POLICY_FLAG_TRUSTED;android::base::Timer t;//mPolicy是指NativeInputManager对象,这里policyFlags为引用,会修改policyFlags的值//如果处理时间大于50ms则,输出警告mPolicy->interceptMotionBeforeQueueing(args->displayId, args->eventTime, /*byref*/ policyFlags);if (t.duration() > SLOW_INTERCEPTION_THRESHOLD) {ALOGW("Excessive delay in interceptMotionBeforeQueueing; took %s ms",std::to_string(t.duration().count()).c_str());}bool needWake;{ // acquire lockmLock.lock();//Motion事件是否需要交由InputFilter过滤if (shouldSendMotionToInputFilterLocked(args)) {mLock.unlock();//初始化MotionEvent,将NotifyMotionArgs中的参数信息赋值给MotionEvent中的参数MotionEvent event;event.initialize(args->deviceId, args->source, args->displayId,args->action, args->actionButton,args->flags, args->edgeFlags, args->metaState, args->buttonState,args->classification, 0, 0, args->xPrecision, args->yPrecision,args->downTime, args->eventTime,args->pointerCount, args->pointerProperties, args->pointerCoords);policyFlags |= POLICY_FLAG_FILTERED;//开始过滤,如果返回值为false,就会直接return,这次事件不再进行分发,直接忽略if (!mPolicy->filterInputEvent(&event, policyFlags)) {return; // event was consumed by the filter}mLock.lock();}//创建KeyEntry对象MotionEntry* newEntry = new MotionEntry(args->sequenceNum, args->eventTime,args->deviceId, args->source, args->displayId, policyFlags,args->action, args->actionButton, args->flags,args->metaState, args->buttonState, args->classification,args->edgeFlags, args->xPrecision, args->yPrecision, args->downTime,args->pointerCount, args->pointerProperties, args->pointerCoords, 0, 0);//将KeyEntry放入队列,返回true,则派发线程处于休眠,需要唤醒needWake = enqueueInboundEventLocked(newEntry);mLock.unlock();} // release lockif (needWake) {mLooper->wake();}
}bool InputDispatcher::enqueueInboundEventLocked(EventEntry* entry) {//mInboundQueue为空,则表示派发线程处于休眠bool needWake = mInboundQueue.isEmpty();//将事件放入到队列尾部mInboundQueue.enqueueAtTail(entry);//事件响应速度优化处理switch (entry->type) {case EventEntry::TYPE_KEY: {...break;}case EventEntry::TYPE_MOTION: {//当前App无响应且用户希望切换到其他应用窗口,则drop该窗口事件,并处理其他窗口事件MotionEntry* motionEntry = static_cast<MotionEntry*>(entry);if (motionEntry->action == AMOTION_EVENT_ACTION_DOWN&& (motionEntry->source & AINPUT_SOURCE_CLASS_POINTER)&& mInputTargetWaitCause == INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY&& mInputTargetWaitApplicationToken != nullptr) {...//查询可触摸的窗口sp<InputWindowHandle> touchedWindowHandle = findTouchedWindowAtLocked(displayId, x, y);...}break;}}return needWake;
}sp<InputWindowHandle> InputDispatcher::findTouchedWindowAtLocked(int32_t displayId,int32_t x, int32_t y, bool addOutsideTargets, bool addPortalWindows) {//从前到后浏览窗口,找到触摸窗口和外部目标。// mWindowHandles中保存的InputWindowHandler类保存了窗口的InputChannel以及InputWindowInfo结构体// InputWindowInfo结构体保存了窗口的各种布局信息,包括可见性、位置、尺寸、flag等。const std::vector<sp<InputWindowHandle>> windowHandles = getWindowHandlesLocked(displayId);// 递归遍历mWindowHandles中的所有WindowHandle,检查时间坐标是否落在其上for (const sp<InputWindowHandle>& windowHandle : windowHandles) {const InputWindowInfo* windowInfo = windowHandle->getInfo();if (windowInfo->displayId == displayId) {int32_t flags = windowInfo->layoutParamsFlags;if (windowInfo->visible) {if (!(flags & InputWindowInfo::FLAG_NOT_TOUCHABLE)) {// 如果窗口是focusable或者flag不为FLAG_NOT_FOCUSABLE,则说明该窗口是”可触摸模式“bool isTouchModal = (flags & (InputWindowInfo::FLAG_NOT_FOCUSABLE | InputWindowInfo::FLAG_NOT_TOUCH_MODAL)) == 0;//如果窗口是”可触摸模式或者坐标点落在窗口之上 if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) {int32_t portalToDisplayId = windowInfo->portalToDisplayId;if (portalToDisplayId != ADISPLAY_ID_NONE&& portalToDisplayId != displayId) {if (addPortalWindows) {mTempTouchState.addPortalWindow(windowHandle);}return findTouchedWindowAtLocked(portalToDisplayId, x, y, addOutsideTargets, addPortalWindows);}// Found window.return windowHandle;}}if (addOutsideTargets && (flags & InputWindowInfo::FLAG_WATCH_OUTSIDE_TOUCH)) {//将符合条件的窗口放入TempTouchState中,以便后续处理。mTempTouchState.addOrUpdateWindow(windowHandle, InputTarget::FLAG_DISPATCH_AS_OUTSIDE, BitSet32(0));}}}}return nullptr;
}
notifyMotion流程
检查Motion事件的参数是否有效,其内部会检查触控点的数量pointerCount是否在合理范围内(小于1或者大于16都是不合理的),以及触控点的ID是否在合理范围内(小于0或者大于31都是不合理的)。
调用NativeInputManager.interceptMotionBeforeQueueing,加入队列前执行拦截动作,但并不改变流程,调用链:
IMS.interceptMotionBeforeQueueing InputMonitor.interceptMotionBeforeQueueing (继承IMS.WindowManagerCallbacks) PhoneWindowManager.interceptMotionBeforeQueueing,主要是PowerManger.wakeUp
调用NativeInputManager.filterInputEvent过滤输入事件;当返回值为false则过滤该事件,不再往下分发。这里会反射到java层IMS.filterInputEvent,然后设置了InputFilter,则返回false,拦截事件,然后调用InputFilter.filterInputEvent,异步调用sendInputEvent, 最终会再调用InputDispatcher.injectInputEvent()。injectInputEvent函数和notifyKey函数很像,notifyKey函数是正常走按键流程在dispatchReader中调用的函数。这里也会想notifyKey一样,先调用PhoneWindowManager的interceptKeyBeforeQueueing函数,然后根据不同类型的Event,然后创建EventEntry,最后调用了enqueueInboundEventLocked函数。
adb shell input keyevent
就是调用的InputManager.getInstance()。injectInputEvent,然后调用到InputDispatcher.injectInputEvent进行事件模拟的。生成MotionEntry,并调用enqueueInboundEventLocked,将该事件加入到InputDispatcherd的成员变量mInboundQueue。针对Motion当前App无响应且用户希望切换到其他应用窗口,则drop该窗口事件,并处理其他窗口事件,调用findTouchedWindowAtLocked查询可触摸的窗口。
findTouchedWindowAtLocked中的mWindowHandles的赋值过程是由Java层的InputMonitor.setInputWindows(),经过JNI调用后进入InputDispatcher::setInputWindows()方法完成。 进一步说, 就是WMS执行addWindow()过程或许UI改变等场景,都会触发该方法的修改。
notifyMotion由InputReader在其线程循环中调用,因此interceptMotionBeforeQueueing和filterInputEvent都在InputReader的线程中执行。
分发过程
这段代码是Android系统中用于处理输入事件的分发逻辑。
InputDispatcherThread
类中的threadLoop
方法会调用InputDispatcher
类的dispatchOnce
方法进行一次输入事件的分发。在
dispatchOnce
方法中,首先会获取一个锁,然后调用dispatchOnceInnerLocked
方法进行输入事件的分发,这个方法的返回值会决定下次分发循环的时间点。如果命令队列为空,那么会立即进行分发。如果命令队列中有命令,那么会立即开始执行下次线程循环。然后,计算需要休眠的时间
timeoutMillis
,并通过mLooper->pollOnce
方法进入休眠,直到有事件发生或者时间到。这个方法的主要作用是处理输入事件,保证用户输入的及时响应。
注意:这里的
LONG_LONG_MAX
和LONG_LONG_MIN
是C++中的最大和最小值,nsecs_t
是一个64位的整数,用于表示时间戳。haveCommandsLocked
和runCommandsLockedInterruptible
是两个私有方法,用于检查是否有待处理的命令和执行命令,具体实现并未给出。这个代码片段没有错误处理和异常处理,实际使用时需要添加。
bool InputDispatcherThread::threadLoop() {mDispatcher->dispatchOnce();return true; }void InputDispatcher::dispatchOnce() {nsecs_t nextWakeupTime = LONG_LONG_MAX;{ // 获取锁std::scoped_lock _l(mLock);mDispatcherIsAlive.notify_all();// 通过dispatchOnceInnerLocked进行输入事件分发,传出参数nextWakeupTime决定下次派发循环的时间点if (!haveCommandsLocked()) {dispatchOnceInnerLocked(&nextWakeupTime);}// 执行命令队列中的命令if (runCommandsLockedInterruptible()) {nextWakeupTime = LONG_LONG_MIN;// 设置nextWakeupTime为立即开始执行下次线程循环}} // 释放锁// 计算需要休眠的时间timeoutMillis,并通过pollOnce进入epoll_waitnsecs_t currentTime = now();int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);mLooper->pollOnce(timeoutMillis); }
这段代码中,
dispatchOnce
方法的主要逻辑是:
获取锁,检查是否有待处理的命令,如果没有,则进行分发;如果有,则立即开始执行下次线程循环。
计算需要休眠的时间,并通过
pollOnce
方法进入休眠,直到有事件发生或者时间到。释放锁。
这个过程会一直重复,直到线程被终止。
bool InputDispatcherThread::threadLoop() {mDispatcher->dispatchOnce();return true;
}void InputDispatcher::dispatchOnce() {nsecs_t nextWakeupTime = LONG_LONG_MAX;{ // acquire lockstd::scoped_lock _l(mLock);mDispatcherIsAlive.notify_all();//通过dispatchOnceInnerLocked进行输入事件分发,传出参数nextWakeupTime决定下次派发循环的时间点if (!haveCommandsLocked()) {dispatchOnceInnerLocked(&nextWakeupTime);}//执行命令队列中的命令if (runCommandsLockedInterruptible()) {nextWakeupTime = LONG_LONG_MIN;//设置nextWakeupTime为立即开始执行下次线程循环}} // release lock// 计算需要休眠的时间timeoutMillis,并通过pollOnce进入epoll_waitnsecs_t currentTime = now();int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);mLooper->pollOnce(timeoutMillis);
}
通过dispatchOnceInnerLocked进行输入事件分发,传出参数nextWakeupTime决定下次派发循环的时间点。
执行命令队列中的命令,命令是一个符合Command签名的回调函数,可以通过InputDispatcher::postCommandLocked()创建并添加到命令队列mCommandQueue中,InputDispatcher执行命令的过程类似于Handler的工作方式。
计算需要休眠的时间timeoutMillis,并通过pollOnce进入epoll_wait。
线程执行Looper->pollOnce,进入epoll_wait等待状态,派发线程的休眠在三种情况下可能被唤醒:
callback:epoll_wait监听的fd由epoll_event发生时唤醒;
timeout:到达nextWakeupTime时间,超时唤醒;
wake:主动调用Looper::wake函数唤醒(由输入事件注入派发队列中);
这段代码是Android系统中用于处理输入事件的分发逻辑的实现。
InputDispatcher::dispatchOnceInnerLocked
方法是在dispatchOnce
方法中被调用的,用于处理一个输入事件。在
dispatchOnceInnerLocked
方法中,首先检查是否需要冻结输入分发。如果需要,则直接返回,不再处理任何事件。然后检查是否需要进行应用切换,如果需要,则更新nextWakeupTime
为应用切换的时间。如果当前没有待分发的事件,则从
mInboundQueue
队列中取出头部的事件,并重置ANR(Application Not Responding)超时。接着,检查事件是否需要被丢弃,丢弃的原因可能是因为事件的派发策略不允许,或者因为InputDispatcher被禁用。
对于不同类型的输入事件,有不同的处理逻辑。对于按键事件,如果事件因为应用切换、过期或者被其他窗口阻碍而丢弃,则进行相应的处理。对于触摸事件,如果事件因为阻碍其他窗口获得事件而丢弃,则进行相应的处理。
如果事件被丢弃,为了保证窗口收到的事件仍能保持
down/up enter/exit
的配对状态,需要对事件进行补发。然后释放mPendingEvent
对象,使得InputDispatcher能够快速处理下一个分发事件。这个过程会一直重复,直到线程被终止。
注意,如果
mPendingEvent
对象为空,则可以处理分发队列中的下一条事件。这个方法的主要逻辑是:
检查是否需要冻结输入分发,如果需要,则直接返回。
检查是否需要进行应用切换,如果需要,则更新
nextWakeupTime
。如果当前没有待分发的事件,则从
mInboundQueue
队列中取出头部的事件。检查事件是否需要丢弃,如果需要,则进行相应的处理。
根据事件类型进行相应的处理。
如果事件被丢弃,则进行相应的处理,并释放
mPendingEvent
对象,使得InputDispatcher能够快速处理下一个分发事件。这个过程会一直重复,直到线程被终止。
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {...//当分发被冻结,则不再处理超时和分发事件的工作,//setInputDispatchMode可以使InputDispatcher在禁用、冻结、正常状态切换if (mDispatchFrozen) {return;}//优化app切换延迟,当切换超时,则抢占分发,丢弃其他所有即将要处理的事件//如果isAppSwitchDue为true,说明没有及时响应HOME键等操作bool isAppSwitchDue = mAppSwitchDueTime <= currentTime;if (mAppSwitchDueTime < *nextWakeupTime) {*nextWakeupTime = mAppSwitchDueTime;}//如果还没有待分发的事件,去mInboundQueue中取出一个事件if (! mPendingEvent) {//如果mInboundQueue为空,并且没有待分发的事件,就returnif (mInboundQueue.isEmpty()) {...} else {//如果mInboundQueue不为空,取队列头部的EventEntry赋值给mPendingEvent//之所以用成员变量而不是局部变量保存,是由于此次线程循环有可能不能完成此事件派发 mPendingEvent = mInboundQueue.dequeueAtHead();traceInboundQueueLengthLocked();}//重置ANR信息.resetANRTimeoutsLocked();}//检查事件是否需要丢弃,dropReason描述了是否需要被丢弃bool done = false;DropReason dropReason = DROP_REASON_NOT_DROPPED;if (!(mPendingEvent->policyFlags & POLICY_FLAG_PASS_TO_USER)) {//在事件注入派发时调用interceptMotionBeforeQueueing询问派发测量,倘若派发策略不允许此事件被派发给用户,则丢弃dropReason = DROP_REASON_POLICY;} else if (!mDispatchEnabled) {//如果InputDispatcher被禁用,通过setInputDispatchMode设置,则此事件也会被丢弃//注意如果被冻结时时不会丢弃事件,而是等解冻后继续派发dropReason = DROP_REASON_DISABLED;}...switch (mPendingEvent->type) {...case EventEntry::TYPE_KEY: {KeyEntry* typedEntry = static_cast<KeyEntry*>(mPendingEvent);...done = dispatchKeyLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);break;}case EventEntry::TYPE_MOTION: {MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);//事件因为home键没有能被及时响应丢弃if (dropReason == DROP_REASON_NOT_DROPPED && isAppSwitchDue) {dropReason = DROP_REASON_APP_SWITCH;}//事件因为过期丢弃if (dropReason == DROP_REASON_NOT_DROPPED&& isStaleEvent(currentTime, typedEntry)) {dropReason = DROP_REASON_STALE;}//事件因为阻碍了其他窗口获得事件丢弃if (dropReason == DROP_REASON_NOT_DROPPED && mNextUnblockedEvent) {dropReason = DROP_REASON_BLOCKED;}//执行dispatchMotionLocked进行Motion事件的派发,如果派发完成,无论成功派发还是事件被丢弃,都会返回true//失败则返回false,在下次循环时再次尝试此事件派发done = dispatchMotionLocked(currentTime, typedEntry,&dropReason, nextWakeupTime);break;}...if (done) {//如果事件丢弃,为了保证窗口收到的事件仍能保持down/up enter/exit的配对状态,还需要对事件进行补发if (dropReason != DROP_REASON_NOT_DROPPED) {dropInboundEventLocked(mPendingEvent, dropReason);}mLastDropReason = dropReason;//设置mPendingEvent对象为null,使在下次循环时可以处理派发队列中的下一条事件releasePendingEventLocked();//使得InputDispatcher能够快速处理下一个分发事件//因为当派发队列为空时,派发线程可能需要在下次循环中生成重复按键事件,因此不能直接进入休眠*nextWakeupTime = LONG_LONG_MIN; // force next poll to wake up immediately}
}
如果派发队列为空,则回事派发线程陷入无期限休眠状态。
InputDispatcher的冻结处理:如果当前InputDispatcher被冻结,则不进行派发操作,InputDispatcher有三种状态,分别是正常状态、冻结状态和禁用状态,可以通过InputDispatcher的setInputDispatchMode函数来设置。
- 窗口切换操作处理:mAppSwitchDueTime ,代表了App最近发生窗口切换操作时(比如按下Home键、挂断电话),该操作事件最迟的分发时间,当事件分发的时间点距离该事件加入mInboundQueue的时间超过500ms,则认为app切换过期,即isAppSwitchDue=true。如果mAppSwitchDueTime小于nextWakeupTime(下一次InputDispatcherThread醒来的时间),就将mAppSwitchDueTime赋值给nextWakeupTime,这样当InputDispatcher处理完分发事件后,会第一时间处理窗口切换操作。
取出事件:如果没有待分发的事件,就从mInboundQueue中取出一个事件,如果mInboundQueue为空,并且没有待分发的事件,就return,如果mInboundQueue不为空,取队列头部的EventEntry赋值给mPendingEvent,mPendingEvent的类型为EventEntry对象指针。
事件丢弃:dropReason代表了事件丢弃的原因,它的默认值为DROP_REASON_NOT_DROPPED,代表事件不被丢弃。根据mPendingEvent的type做区分处理,这里主要截取了对Motion类型的处理。经过过滤,会调用dispatchMotionLocked函数为这个事件寻找合适的窗口。
后续处理:执行dispatchMotionLocked进行Motion事件的派发,如果派发完成,无论成功派发还是事件被丢弃,都会返回true,否则返回false,在下次循环时再次尝试此事件派发。如果dispatchMotionLocked事件分发成功,则会调用releasePendingEventLocked函数,其内部会将mPendingEvent的值设置为Null,并将mPendingEvent指向的对象内存释放掉。将nextWakeupTime的值设置为LONG_LONG_MIN,这是为了让InputDispatcher能够快速处理下一个分发事件。
派发一个事件至少需要一次线程循环才能完成,因为事件的目标窗口有可能正在处理前一个输入事件,在窗口完成之前的事件的处理病给予反馈之前,InputDIspatcher不会在向此窗口派发新事件。事件的派发是串行的,在队首的事件完成派发或者丢弃之前,不会对后续的事件进行派发
事件丢弃DropReason
这段代码定义了一个枚举类型
DropReason
,用于表示输入事件在分发过程中被丢弃的原因。
DROP_REASON_NOT_DROPPED
:表示事件没有被丢弃。
DROP_REASON_POLICY
:表示事件因为输入策略而被丢弃,通常是因为事件类型不被允许被分发给用户。
DROP_REASON_APP_SWITCH
:表示事件因为应用切换而被丢弃,通常是因为用户快速点击HOME键等操作,系统需要立即切换到桌面应用。
DROP_REASON_DISABLED
:表示事件因为InputDispatcher被禁用而被丢弃。
DROP_REASON_BLOCKED
:表示事件因为阻碍了其他窗口获得事件而被丢弃,可能是由于其他窗口的事件优先级更高。
DROP_REASON_STALE
:表示事件因为过期而被丢弃,可能是由于事件太旧,不再需要处理。这个枚举类型用于在输入事件分发过程中记录和处理事件被丢弃的原因,以便于后续进行相应的处理和优化。
* frameworks/native/services/inputflinger/InputDispatcher.h
enum DropReason {DROP_REASON_NOT_DROPPED = 0,DROP_REASON_POLICY = 1,DROP_REASON_APP_SWITCH = 2,DROP_REASON_DISABLED = 3,DROP_REASON_BLOCKED = 4,DROP_REASON_STALE = 5,
};
DROP_REASON_NOT_DROPPED
当InputDispatcher在将输入事件放入派发队列前想DispatcherPolicy询问此事件派发策略时,DispatcherPolicy会将DROP_REASON_NOT_DROPPED策略去掉,没有这个派发策略的对象会被丢弃。
DROP_REASON_POLICY
某些输入事件具有系统级功能,例如HOME键、电源键、电话接听/挂断等被系统处理,因此DispatcherPolicy不希望这些事件被窗口捕获。
DROP_REASON_APP_SWITCH
dispatchOnceInnerLocked()函数说明了InputDispatcher的事件派发是串行的。因此在前一个事件派发成功并得到目标窗口的反馈之前,后续事件都会被其阻塞。当某个窗口因程序缺陷而无法响应输入事件时,用户可能会尝试使用HOME键退出这个程序。而由于派发的串行性,用户所操作的HOME键在其之前的输入事件成功派发给无响应的应用窗口之前无法获得派发机会,因此在ANR对话框弹出之前的5秒里,用户不得不面对无响应程序。为了解决这个问题,InputDispatcher为HOME键设置了限时派发的要求。当InputDispatcher的enqueueInboundEventLocked()函数发现HOME键被加入派发队列后,便要求HOME键之前的所有输入事件在0.5秒(APP_SWITCH_TIMEOUT常量定义)之前派发完毕,否则这些事件将被丢弃,是的HOME键至少能在0.5秒内得到响应。
DROP_REASON_DISABLED
因为InputDispatcher被禁用而使得事件被丢弃。InputDispatcher::setInputDispatchMode()函数可以是的InputDispatcher在禁用、冻结、正常三种状态之间进行切换。禁用状态会使得所有事件被丢弃,冻结将会使的dispatchOnceInnerLocked()函数直接返回从而停止派发工作。InputDispatcher的这三种状态的切换有Java层的IMS提供接口,由AMS和WMS根据需要进行设置。例如:当手机进入休眠状态时,InputDispatcher会被禁用,而屏幕旋转过程中,InputDispatcher会被暂时冻结。
DROP_REASON_BLOCKED
和APP_SWITCH原因类似,如果是因为一个窗口无法响应输入事件,可用户可能希望在其他窗口上进行点击,以尝试是否能得到响应。因为派发的串行性,这次尝试会以失败而高中。为此,当enqueueInboundEventLocked()发现有窗口正在阻塞派发的进行,并且新入队的触摸事件的目标是另外一个窗口,则将这个新事件保存到mNextUnblockedEvent中。随后的dispatchOnceInnerLocked()会将此事件之前的输入事件全部丢弃,使得用户在其他窗口上进行点击的尝试可以立即得到响应。
DROP_REASON_STALE
在dispatchOnceInnerLocked()函数准备对事件进行派发时,会先检查一下事件所携带的时间戳和当前时间的差距。如果事件过于陈旧(10秒以上,由常量STALE_EVENT_TIMEOUT所指定),则此事件需要被抛弃。
Motion事件目标窗口确定
当事件幸运的避开了所有上述丢弃原因之后,才能由InputDispatcher尝试派发。对Motion时间来说,下一步是dispatchMotionLocked(),在这个函数中,InputDispatcher将为事件寻找合适的窗口目标。
这段代码是Android系统中用于处理触摸事件的分发逻辑的实现。
InputDispatcher::dispatchMotionLocked
方法是在dispatchOnceInnerLocked
方法中被调用的,用于处理一个触摸事件。在
dispatchMotionLocked
方法中,首先标记事件已经正式进入派发流程。然后,如果事件是需要丢弃的,则直接返回true
,不会去为该事件寻找合适的窗口。接着,根据事件的类型,调用不同的方法来寻找合适的窗口。对于触摸事件,调用
findTouchedWindowTargetsLocked
方法,对于非触摸事件(如轨迹球事件),调用findFocusedWindowTargetsLocked
方法。如果输入事件被挂起,说明找到了窗口并且窗口无响应,则返回
false
。否则,将事件派发给inputTargets
列表中的目标。这个过程会一直重复,直到线程被终止。
这个方法的主要逻辑是:
标记事件已经正式进入派发流程。
如果事件是需要丢弃的,则直接返回。
根据事件的类型,调用不同的方法来寻找合适的窗口。
如果找到了合适的窗口,则将事件派发给这些窗口。
如果事件被挂起,说明找到了窗口并且窗口无响应,则返回
false
。这个过程会一直重复,直到线程被终止。
注意,这个方法中,
injectionResult
的值决定了事件是否被成功派发,如果injectionResult
不是INPUT_EVENT_INJECTION_SUCCEEDED
,则表示无法找到合适窗口,事件将被直接丢弃。
bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, MotionEntry* entry, DropReason* dropReason, nsecs_t* nextWakeupTime) {//标记事件已经正式进入派发流程if (! entry->dispatchInProgress) {entry->dispatchInProgress = true;}//如果事件是需要丢弃的,则返回true,不会去为该事件寻找合适的窗口if (*dropReason != DROP_REASON_NOT_DROPPED) {setInjectionResult(entry, *dropReason == DROP_REASON_POLICY? INPUT_EVENT_INJECTION_SUCCEEDED : INPUT_EVENT_INJECTION_FAILED);return true;}bool isPointerEvent = entry->source & AINPUT_SOURCE_CLASS_POINTER;//目标窗口信息列表会存储在inputTargets中std::vector<InputTarget> inputTargets;bool conflictingPointerActions = false;int32_t injectionResult;if (isPointerEvent) {//处理点击形式的事件,比如触摸屏幕injectionResult = findTouchedWindowTargetsLocked(currentTime,entry, inputTargets, nextWakeupTime, &conflictingPointerActions);} else {//处理非触摸形式的事件,比如轨迹球injectionResult = findFocusedWindowTargetsLocked(currentTime,entry, inputTargets, nextWakeupTime);}//输入事件被挂起,说明找到了窗口并且窗口无响应if (injectionResult == INPUT_EVENT_INJECTION_PENDING) {return false;}setInjectionResult(entry, injectionResult);//返回值不为SUCCEEDED,表示无法找到合适窗口,例如窗口未获取焦点,或点击位置没落在任何一个窗口内,此事件将被直接丢弃if (injectionResult != INPUT_EVENT_INJECTION_SUCCEEDED) {...return true;}//分发目标添加到inputTargets列表中addGlobalMonitoringTargetsLocked(inputTargets, getTargetDisplayId(entry));...//将事件派发给inputTargets列表中的目标dispatchEventLocked(currentTime, entry, inputTargets);return true;
}
对于被丢弃的事件直接返回true。
给事件寻找合适的目标窗口,目标窗口分为普通窗口和监听窗口(monitoring),普通窗口通过按点查找与按焦点查找两种方式获得,而监听窗口则无条件监听所有输入事件。普通窗口的查找结果决定了此次线程循环是否可以完成事件派发。
如果成功找到可以接收事件的目标窗口,则调用dispatchEventLocked()函数进行派发。
查找到的派发目标存储在InputTarget结构体中。
这段代码是Android系统中InputDispatcher的一部分,主要功能是处理触摸事件的分发。以下是对这段代码的中文解释:
首先,如果当前是一个新的手势或者是一个分割动作并且动作是POINTER_DOWN,那么从MotionEntry中获取事件坐标点。
然后,使用这些坐标点在当前显示区域中找到被触摸的窗口。
找到的窗口会被添加到临时触摸状态(mTempTouchState)中,以便后续处理。
接着,检查临时触摸状态中的所有目标窗口是否准备好接受新的输入事件。
如果窗口不能接收新事件,则记录不能接收的原因,并设置下次唤醒时间为5秒后,如果5秒后线程仍未将此事件派发成功,则会开始向Java层通报ANR。
如果窗口的查找过程一切顺利,那么设置注入结果为SUCCEEDED,并为每一个临时触摸状态中的窗口生成InputTargets。
最后,在下一次迭代中,删除外部窗口或悬停触摸窗口。
注意,这段代码中的
InputDispatcher
类是一个处理触摸事件的分发器,MotionEntry
是一个存储触摸事件的结构体,InputTarget
是一个存储目标窗口的结构体,InputWindowHandle
是一个存储窗口信息的结构体。这段代码的主要功能是找到被触摸的窗口,并检查这些窗口是否准备好接受新的输入事件。如果窗口不能接收新事件,那么会设置下次唤醒时间为5秒后,如果5秒后线程仍未将此事件派发成功,则会开始向Java层通报ANR。如果窗口的查找过程一切顺利,那么会设置注入结果为SUCCEEDED,并为每一个临时触摸状态中的窗口生成InputTargets。
这段代码的主要逻辑是:
如果当前是一个新的手势或者是一个分割动作并且动作是POINTER_DOWN,那么获取事件坐标点。
使用这些坐标点在当前显示区域中找到被触摸的窗口。
找到的窗口会被添加到临时触摸状态中,以便后续处理。
检查临时触摸状态中的所有目标窗口是否准备好接受新的输入事件。
如果窗口不能接收新事件,则记录不能接收的原因,并设置下次唤醒时间为5秒后。
如果窗口的查找过程一切顺利,那么设置注入结果为SUCCEEDED,并为每一个临时触摸状态中的窗口生成InputTargets。
最后,在下一次迭代中,删除外部窗口或悬停触摸窗口。
注意,这段代码中的
InputDispatcher
类是一个处理触摸事件的分发器,MotionEntry
是一个存储触摸事件的结构体,InputTarget
是一个存储目标窗口的结构体,InputWindowHandle
是一个存储窗口信息的结构体。
int32_t InputDispatcher::findTouchedWindowTargetsLocked(nsecs_t currentTime,const MotionEntry* entry, std::vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime,bool* outConflictingPointerActions) {...if (newGesture || (isSplit && maskedAction == AMOTION_EVENT_ACTION_POINTER_DOWN)) {// 从MotionEntry中获取事件坐标点int32_t pointerIndex = getMotionEventActionPointerIndex(action);int32_t x = int32_t(entry->pointerCoords[pointerIndex].getAxisValue(AMOTION_EVENT_AXIS_X));int32_t y = int32_t(entry->pointerCoords[pointerIndex].getAxisValue(AMOTION_EVENT_AXIS_Y));bool isDown = maskedAction == AMOTION_EVENT_ACTION_DOWN;sp<InputWindowHandle> newTouchedWindowHandle = findTouchedWindowAtLocked(displayId, x, y, isDown /*addOutsideTargets*/, true /*addPortalWindows*/);...//把选中的窗口保存到TempTouchState中,以便后续处理mTempTouchState.addOrUpdateWindow(newTouchedWindowHandle, targetFlags, pointerIds);...}...//检查mTempTouchState中所有目标窗口是否准备好接受新的输入事件for (const TouchedWindow& touchedWindow : mTempTouchState.windows) {if (touchedWindow.targetFlags & InputTarget::FLAG_FOREGROUND) {//检查窗口是否准备好接收更多的输入std::string reason = checkWindowReadyForMoreInputLocked(currentTime,touchedWindow.windowHandle, entry, "touched");if (!reason.empty()) {// 如果窗口不能接收新事件,则记录不能接收的原因,并设置nextWakeupTime为5s后,如果5s后// 线程仍未将次事件派发成功而进入这个分支,则会开始向Java层通报ANR。// 在这里injectionResult被设置为INPUT_EVENT_INJECTION_PENDINGinjectionResult = handleTargetsNotReadyLocked(currentTime, entry,nullptr, touchedWindow.windowHandle, nextWakeupTime, reason.c_str());//因此次查找到的窗口不能接收事件,所以调过后续产生InputTarget的过程goto Unresponsive;}}}...//如果执行到这里,说明窗口的查找过程一切顺利,设置injectionResult为SUCCEEDED,并将injectionResult放入参数inputTargets中injectionResult = INPUT_EVENT_INJECTION_SUCCEEDED;//为每一个mTempTouchState中的窗口生成InputTargetsfor (const TouchedWindow& touchedWindow : mTempTouchState.windows) {addWindowTargetLocked(touchedWindow.windowHandle, touchedWindow.targetFlags,touchedWindow.pointerIds, inputTargets);}for (const TouchedMonitor& touchedMonitor : mTempTouchState.gestureMonitors) {addMonitoringTargetLocked(touchedMonitor.monitor, touchedMonitor.xOffset,touchedMonitor.yOffset, inputTargets);}//在下一次迭代中,删除外部窗口或悬停触摸窗口mTempTouchState.filterNonAsIsTouchWindows();...
}
此函数中出现的mWindowHandles保存了所有窗口的信息,为查找输入窗口提供依据,因此每当窗口形态发生变化是,WMS会通过IMS将所有窗口提交到InputDispatcher,完成mWindowHandles的更新。mWindowHandles中的窗口顺序,索引越小,ZOrder越大,这与WMS中的窗口列表相反。
此函数做了三项工作:
根据窗口点击坐标和事件发生坐标选择合适的目标窗口(ZOrder由上至下遍历,ZOrder越靠上,优先权越高),另外如果窗口没有在其LayoutParams.flag中指明FLAG_NOT_TOUCH_MODAL选项,说明是模式窗口,模式窗口将会阻止点击事件派发给其子窗口。
调用checkWindowReadyForMoreInputLocked()函数检查窗口是否可接收新的点击事件,如果无法接收,说明此窗口有可能发生ANR。handleTargetsNotReadyLocked()做下记录,并将injectionResult设置为PENDING,要求下次派发循环中重试。
如果找到可以接收新事件的窗口,则调用addWindowTargetLocked()生成InputTarget,并添加到inputTargets列表中。(InputTarget中几乎所有字段可在InputWindowHandle中找到)。
事件的发送
这段代码是Android系统中InputDispatcher的一部分,主要功能是分发事件到相应的输入连接。以下是对这段代码的中文解释:
首先,向
mCommandQueue
队列添加doPokeUserActivityLockedInterruptible
命令。然后,遍历
inputTargets
,对于每一个inputTarget
,通过inputTarget
内部的inputChannel
获取Connection
的索引。如果获取到的索引大于等于0,那么就从
mConnectionsByFd
容器中获取对应的Connection
。接着,根据
inputTarget
开始事件发送循环。注意,这段代码中的
InputDispatcher
类是一个处理触摸事件的分发器,EventEntry
是一个存储事件的结构体,InputTarget
是一个存储目标连接的结构体。这段代码的主要逻辑是:
向
mCommandQueue
队列添加doPokeUserActivityLockedInterruptible
命令。遍历
inputTargets
,对于每一个inputTarget
,通过inputTarget
内部的inputChannel
获取Connection
的索引。如果获取到的索引大于等于0,那么就从
mConnectionsByFd
容器中获取对应的Connection
。接着,根据
inputTarget
开始事件发送循环。注意,这段代码中的
InputDispatcher
类是一个处理触摸事件的分发器,EventEntry
是一个存储事件的结构体,InputTarget
是一个存储目标连接的结构体。
void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,EventEntry* eventEntry, const std::vector<InputTarget>& inputTargets) {//向mCommandQueue队列添加doPokeUserActivityLockedInterruptible命令pokeUserActivityLocked(eventEntry);for (const InputTarget& inputTarget : inputTargets) {//根据inputTarget内部的inputChannel来获取Connection的索引ssize_t connectionIndex = getConnectionIndexLocked(inputTarget.inputChannel);if (connectionIndex >= 0) {//获取保存在mConnectionsByFd容器中的Connectionsp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);//根据inputTarget,开始事件发送循环prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget);}}
}
遍历inputTargets列表,获取每一个inputTarget。
根据inputTarget内部的inputChannel来获取Connection的索引,再根据这个索引作为Key值来获取mConnectionsByFd容器中的Connection。Connection可以理解为InputDispatcher和目标窗口的连接,其内部包含了连接的状态、InputChannel、InputWindowHandle和事件队列等等。
调用prepareDispatchCycleLocked函数根据当前的inputTarget,开始事件发送循环。最终会通过inputTarget中的inputChannel来和窗口进行进程间通信,最终将Motion事件发送给目标窗口。调用流程:
prepareDispatchCycleLocked->enqueueDispatchEntryLocked
。将InputDispatcher中mInboundQueue中的事件取出后, 找到目标window后,封装dispatchEntry加入到connection的outbound队列。DispatchEntry附加了一个seq字段,这个字段是事件的序号,窗口对事件的反馈将携带这个序号。其实这个函数使用不同的选项多次调用了enqueueDispatchEntryLocked()。FLAG_DISPATCH_AS_IS表示不修改事件的action类型,而其他的选项则会使得action类型发生变化
startDispatchCycleLocked
。从outboundQueue
中取出事件,重新放入waitQueue队列。
inputPublisher.publishKeyEvent->mChannel->sendMessage
。InputChannel通过socket向远端的socket发送消息。其中pokeUserActivityLocked(eventEntry)方法调用NativeInputManager::pokeUserActivity,最终会调用到Java层的PowerManagerService.java中的userActivityFromNative()方法. 这也是PMS中唯一的native call方法
这段代码是Android系统中InputDispatcher的一部分,主要功能是处理用户活动并分发事件到相应的连接。以下是对这段代码的中文解释:
InputDispatcher::pokeUserActivityLocked
方法中,首先获取当前显示区域的焦点窗口,然后创建一个命令并将其添加到命令队列中。
InputDispatcher::postCommandLocked
方法中,创建一个新的命令条目并将其添加到命令队列中。
InputDispatcher::startDispatchCycleLocked
方法中,当连接状态正常且出站队列不为空时,开始事件分发循环。对于每一个事件,根据事件类型进行不同的处理。对于按键事件,直接发布事件;对于触摸事件,调用
publishMotionEvent
方法发布事件。如果发布事件失败,根据失败的原因进行不同的处理。如果是因为管道已满但等待队列为空,则调用Java层的
IMS.notifyInputChannelBroken()
方法;如果处于阻塞状态,则将连接的inputPublisherBlocked
标志设置为true。如果发布事件失败,将事件从出站队列中移除,并将其重新添加到等待队列中。
注意,这段代码中的
InputDispatcher
类是一个处理触摸事件的分发器,CommandEntry
是一个存储命令的结构体,Connection
是一个存储连接信息的结构体。这段代码的主要逻辑是:
在
InputDispatcher::pokeUserActivityLocked
方法中,获取当前显示区域的焦点窗口,然后创建一个命令并将其添加到命令队列中。在
InputDispatcher::postCommandLocked
方法中,创建一个新的命令条目并将其添加到命令队列中。在
InputDispatcher::startDispatchCycleLocked
方法中,当连接状态正常且出站队列不为空时,开始事件分发循环。对于每一个事件,根据事件类型进行不同的处理。对于按键事件,直接发布事件;对于触摸事件,调用
publishMotionEvent
方法发布事件。如果发布事件失败,根据失败的原因进行不同的处理。如果是因为管道已满但等待队列为空,则调用Java层的
IMS.notifyInputChannelBroken()
方法;如果处于阻塞状态,则将连接的inputPublisherBlocked
标志设置为true。如果发布事件失败,将事件从出站队列中移除,并将其重新添加到等待队列中。
注意,这段代码中的
InputDispatcher
类是一个处理触摸事件的分发器,CommandEntry
是一个存储命令的结构体,Connection
是一个存储连接信息的结构体。
void InputDispatcher::pokeUserActivityLocked(const EventEntry* eventEntry) {int32_t displayId = getTargetDisplayId(eventEntry);sp<InputWindowHandle> focusedWindowHandle =getValueByKey(mFocusedWindowHandlesByDisplay, displayId);...CommandEntry* commandEntry = postCommandLocked(& InputDispatcher::doPokeUserActivityLockedInterruptible);commandEntry->eventTime = eventEntry->eventTime;commandEntry->userActivityEventType = eventType;
}
InputDispatcher::CommandEntry* InputDispatcher::postCommandLocked(Command command) {CommandEntry* commandEntry = new CommandEntry(command);//将命令加入mCommandQueue队尾mCommandQueue.enqueueAtTail(commandEntry);return commandEntry;
}void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,const sp<Connection>& connection) {
...//当Connection状态正常,且outboundQueue不为空while (connection->status == Connection::STATUS_NORMAL&& !connection->outboundQueue.isEmpty()) {DispatchEntry* dispatchEntry = connection->outboundQueue.head;dispatchEntry->deliveryTime = currentTime;//设置deliveryTime时间// Publish the event.status_t status;EventEntry* eventEntry = dispatchEntry->eventEntry;switch (eventEntry->type) {case EventEntry::TYPE_KEY: {...break;}case EventEntry::TYPE_MOTION: {MotionEntry* motionEntry = static_cast<MotionEntry*>(eventEntry);...// Publish the motion event.status = connection->inputPublisher.publishMotionEvent(...);break;}...}// publishKeyEvent失败情况if (status) {if (status == WOULD_BLOCK) {if (connection->waitQueue.isEmpty()) {//pipe已满,但waitQueue为空. 不正常的行为//该方法最终会调用到Java层的IMS.notifyInputChannelBroken().abortBrokenDispatchCycleLocked(currentTime, connection, true /*notify*/);} else {/// 处于阻塞状态connection->inputPublisherBlocked = true;}} else {abortBrokenDispatchCycleLocked(currentTime, connection, true /*notify*/);}return;}// Re-enqueue the event on the wait queue.connection->outboundQueue.dequeue(dispatchEntry);traceOutboundQueueLength(connection);connection->waitQueue.enqueueAtTail(dispatchEntry);traceWaitQueueLength(connection);}
}
Motion事件在InputReaderThread线程中的InputReader进行加工,加工完毕后会判断是否要唤醒InputDispatcherThread,如果需要唤醒,会在InputDispatcherThread的线程循环中不断的用InputDispatcher来分发 Motion事件。
将Motion事件交由InputFilter过滤,如果返回值为false,这次Motion事件就会被忽略掉。
InputReader对Motion事件加工后的数据结构为NotifyMotionArgs,在InputDispatcher的notifyMotion函数中,用NotifyMotionArgs中的事件参数信息构造一个MotionEntry对象。这个MotionEntry对象会被添加到InputDispatcher的mInboundQueue队列的末尾。
如果mInboundQueue不为空,取出mInboundQueue队列头部的EventEntry赋值给mPendingEvent。根据mPendingEvent的值,进行事件丢弃处理。
调用InputDispatcher的findTouchedWindowTargetsLocked函数,在mWindowHandles窗口列表中为Motion事件找到目标窗口,并为该窗口生成inputTarget。
enqueueDispatchEntryLocked():生成事件DispatchEntry并加入connection的outbound队列。
startDispatchCycleLocked():从outboundQueue中取出事件DispatchEntry, 重新放入connection的waitQueue队列。
InputChannel.sendMessage通过socket方式将消息发送给远程进程目标窗口。
InputChannel
InputChannel就是SocketPair描述符及其操作的封装,SocketPair用来实现在本机内进行进程间的通信,一对SocketPair 通过socketpair()函数创建, 其使用者可以因此而得到两个相互连接的文件描述符。这两个描述符可以通过套接字接口 send()和recv()进行写入和读取,并且向其中一个文件描述符写人的数据,可以从另一个描述符中读取。
配对的两个InputChannel分别保有一个SocketPair的描述符,并分别分配给InputDispatcher与window。因此InputDispatcher向其保有的InputChannel中写入的输入事件,可以由window从自己的InputChannel中读取。并且window可以将事件处理完毕的反馈写入ImputChannel中,InputDispatcher再将反馈进行读取。
ActivityThread.handleResumeActivity->WindowManagerImpl.addView->WindowManagerGlobal.addView() ->ViewRootImpl.setView
这段代码是Android系统中窗口管理的一部分,主要涉及到ViewRootImpl和WindowManagerService两个类。
在ViewRootImpl的setView方法中,如果当前窗口的输入特性没有设置INPUT_FEATURE_NO_INPUT_CHANNEL,那么会创建一个InputChannel对象,并通过Binder调用进入system进程的Session。如果mInputChannel不为空,并且mInputQueueCallback不为空,则会创建一个InputQueue对象,并调用mInputQueueCallback的onInputQueueCreated方法。
在WindowManagerService的addWindow方法中,如果新添加的窗口能接收按键操作,会更新聚焦窗口。然后通过InputMonitor对象更新所有的窗口信息。
具体来说,InputChannel是Android系统中用于进程间通信的通道,InputQueue是用于处理输入事件的队列,WindowState代表一个窗口的状态,WindowManagerService负责管理所有的窗口。
在ViewRootImpl的setView方法中,如果当前窗口的输入特性没有设置INPUT_FEATURE_NO_INPUT_CHANNEL,那么会创建一个InputChannel对象。然后通过Binder调用进入system进程的Session,如果mInputChannel不为空,并且mInputQueueCallback不为空,则会创建一个InputQueue对象,并调用mInputQueueCallback的onInputQueueCreated方法。
在WindowManagerService的addWindow方法中,如果新添加的窗口能接收按键操作,会更新聚焦窗口。然后通过InputMonitor对象更新所有的窗口信息。
总的来说,这段代码的主要功能是创建和管理窗口的输入通道和输入队列,以及更新窗口的焦点状态。
* frameworks/base/core/java/android/view/ViewRootImpl.java
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {...if ((mWindowAttributes.inputFeatures& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {mInputChannel = new InputChannel();}...//通过Binder调用,进入system进程的Sessionres = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,mTempInsets);...if (mInputChannel != null) {if (mInputQueueCallback != null) {mInputQueue = new InputQueue();mInputQueueCallback.onInputQueueCreated(mInputQueue);}mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,Looper.myLooper());}...
}* frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
public int addWindow(Session session, IWindow client, int seq,LayoutParams attrs, int viewVisibility, int displayId, Rect outFrame,Rect outContentInsets, Rect outStableInsets, Rect outOutsets,DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,InsetsState outInsetsState) {...final WindowState win = new WindowState(this, session, client, token, parentWindow,appOp[0], seq, attrs, viewVisibility, session.mUid,session.mCanAddInternalSystemWindow);...final boolean openInputChannels = (outInputChannel != null&& (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);if (openInputChannels) {win.openInputChannel(outInputChannel);}...if (win.canReceiveKeys()) {//新添加window能接收按下操作,则更新聚焦窗口。focusChanged = updateFocusedWindowLocked(UPDATE_FOCUS_WILL_ASSIGN_LAYERS,false /*updateInputWindows*/);if (focusChanged) {imMayMove = false;}}...if (focusChanged) {displayContent.getInputMonitor().setInputFocusLw(displayContent.mCurrentFocus,false /*updateInputWindows*/);}//将所有的窗口信息更新到InputManagerservicedisplayContent.getInputMonitor().updateInputWindowsLw(false /*force*/);...
}
创建Java层的InputChannel对象mInputChannel。
调用Session.addToDisplay向WMS注册InputChannel信息,进入到WindowManagerService.addWindow。
创建WindowInputEventReceiver对象。
服务端连接的建立
这段代码是Android系统中窗口管理的一部分,主要涉及到WindowState和InputManagerService两个类。
在WindowState的openInputChannel方法中,如果WindowState已经有一个输入通道,则会抛出异常。然后根据WindowState的哈希值和标题生成输入通道的名称,创建一对输入通道,并将其中一个分配给mInputChannel,另一个分配给mClientChannel。如果outInputChannel不为空,则将mClientChannel传递给outInputChannel,并将其销毁。否则,如果窗口可见,会创建一个死窗口事件接收器,以确保输入事件仍然可以被输入监视器通道检测到,并可以重新启动应用程序。最后,将WindowState保存的InputChannel注册到InputManagerService。
在InputManagerService的registerInputChannel方法中,如果输入通道为空,则会抛出异常。然后使用Binder将token设置为输入通道的token。实际的注册过程由NativeInputManager交给InputDispatcher的同名函数完成。
总的来说,这段代码的主要功能是创建和管理窗口的输入通道,并将它们注册到InputManagerService。
在WindowState的openInputChannel方法中,如果WindowState已经有一个输入通道,则会抛出异常。然后根据WindowState的哈希值和标题生成输入通道的名称,创建一对输入通道,并将其中一个分配给mInputChannel,另一个分配给mClientChannel。如果outInputChannel不为空,则将mClientChannel传递给outInputChannel,并将其销毁。否则,如果窗口可见,会创建一个死窗口事件接收器,以确保输入事件仍然可以被输入监视器通道检测到,并可以重新启动应用程序。最后,将WindowState保存的InputChannel注册到InputManagerService。
在InputManagerService的registerInputChannel方法中,如果输入通道为空,则会抛出异常。然后使用Binder将token设置为输入通道的token。实际的注册过程由NativeInputManager交给InputDispatcher的同名函数完成。
总的来说,这段代码的主要功能是创建和管理窗口的输入通道,并将它们注册到InputManagerService。
* frameworks/base/services/core/java/com/android/server/wm/WindowState.java
void openInputChannel(InputChannel outInputChannel) {if (mInputChannel != null) {throw new IllegalStateException("Window already has an input channel.");}//根据WindowState的HashCode以及title来生成InputChannel名称String name = getName();//创建一对InputChannelInputChannel[] inputChannels = InputChannel.openInputChannelPair(name);mInputChannel = inputChannels[0];mClientChannel = inputChannels[1];mInputWindowHandle.token = mClient.asBinder();if (outInputChannel != null) {//socket客户端传递给outInputChannelmClientChannel.transferTo(outInputChannel);mClientChannel.dispose();mClientChannel = null;} else {// If the window died visible, we setup a dummy input channel, so that taps// can still detected by input monitor channel, and we can relaunch the app.// Create dummy event receiver that simply reports all events as handled.mDeadWindowEventReceiver = new DeadWindowEventReceiver(mClientChannel);}//将Windowstate所保存的InputChannel向IMS进行注册 mWmService.mInputManager.registerInputChannel(mInputChannel, mClient.asBinder()); }* frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
public void registerInputChannel(InputChannel inputChannel, IBinder token) {if (inputChannel == null) {throw new IllegalArgumentException("inputChannel must not be null.");}if (token == null) {token = new Binder();}//这里直接调用inputChannel.nativeSetToken将java层窗口通过NativeInputChannel设置到native的InputTransport::InputChannelinputChannel.setToken(token);//实际的注册过程由NI层的NativelnputManager 交给InputDispatcher的同名函数完成注册nativeRegisterInputChannel(mPtr, inputChannel, Display.INVALID_DISPLAY);}
通过WindowState.openInputChannel调用InputChannel.openInputChannelPair,然后进入到InputTransport::InputChannel::openInputChannelPair创建的socket pair,将其中的客户端赋值给mInputChannel。
通过IMS.registerInputChannel()将InputChannel注册到IMS中,通过NativeInputManager::registerInputChannel调用mInputManager->getDispatcher()->registerInputChannel进入到InputDispatcher::registerInputChannel。
通过inputChannel.nativeSetToken将java层窗口通过NativeInputChannel设置到native的InputTransport::InputChannel,这里的token为ViewRootImpl中的W窗口类。
通过InputMonitor.updateInputWindowsLw()将所有窗口的信息更新到IMS。
在WMS添加窗口时,会创建一对InputChannel。 其中一个保存在WindowState中,并注册给IMS,它是服务端。 另外一个则通过传出参数outlnputChannel交给调用者,是客户端。
这段代码是Android系统中输入管理器的一部分,主要涉及到InputDispatcher类。
在InputDispatcher的registerInputChannel方法中,如果输入通道为空,则会抛出异常。然后使用Binder将token设置为输入通道的token。实际的注册过程由NativeInputManager交给InputDispatcher的同名函数完成。
总的来说,这段代码的主要功能是创建和管理窗口的输入通道,并将它们注册到InputManagerService。
在InputDispatcher的registerInputChannel方法中,为传入的InputChannel创建一个Connection对象并对其进行封装。然后监听InputChannel的可读性。mLooper的pollOnce()本质上就是epoll_wait(),因此Looper对象具有监听文件描述符可读性事件的能力,在此注册InputChannel可读性事件,并在事件到来时通过handleReceiveCallback()回调进行处理。
总的来说,这段代码的主要功能是监听并处理输入通道的事件,并将它们注册到InputDispatcher。
* frameworks/native/services/inputflinger/InputDispatcher.cpp
status_t InputDispatcher::registerInputChannel(const sp<InputChannel>& inputChannel,int32_t displayId) {{ // acquire lock...//为传入的Inputchannel创建一个Connection对象并对其进行封装 sp<Connection> connection = new Connection(inputChannel, false /*monitor*/);int fd = inputChannel->getFd();...//监听Inputchannel的可读性。mLooper的pollOnce()本质上就是epoll_wait(),因此 //Looper对象具有监听文件描述符可读性事件的能力,在此注册Inputchannel可读性事件,//并在事件到来时通过handleReceiveCallback()回调进行处理mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, handleReceiveCallback, this);} // release lock// Wake the looper because some connections have changed.mLooper->wake();return OK;
}
atcher中,InputChannel被封装为一个Connection对象。Connection类描述了从ImputDispatcher到目标窗口中的一个连接,其中保存了向窗口发送的事件的状态信息。在Connection中,重要的成员有:
mlnputPublisher,InputTransport::InputPublisher类的一个对象,它封装InputChannel并直接对其进行写入和读取。另外,它也负责ImputMessage结构体的封装与解析。
outboundQueue,用于保存等待通过此Connection进行发送的事件队列。
waitQueue,用于保存已经通过此Connection将事件发送给窗口,正在等待窗口反馈
的事件队列。
registerInputChannel()将InputChannel的可读性事件注册到mLooper中。当来自窗口的反馈到来时,派发线程的mLooper->pollOnce()将会被唤醒,并回调 handleReceiveCallback()进行处理。因此,窗口反馈的到来也会导致派发线程进入下一次派发循环,不过是在handleReceiveCallback()回调完成后。
完成服务端InputChannel的注册之后,InputDispatcher便拥有了向客户端的InputChannel发送InputMessage,以及通过回调handleReceiveCallback()响应来自客户端的反馈的能力。这个过程的基本原理是,先将输入事件放入Connection的outboundQueue队列中,然后再由mInputPublisher依次将队列中的事件封装为InputMessage并写入InputChannel,直到队列为空,或InputChannel的写入缓冲区满。写入的事件将被移存到waitQueue队列里。随后派发线程陷入休眠状态。当窗口在另一端读取事件并发来反馈后,派发线程因InputChannel可读而被唤醒,并在handleReceiveCalback()中通过Connection的mlnputPublisher读取反馈信息,将其与waitQueue中等待反馈的事件进行配对成功后,将事件从waitQueue中移除,完成。
事件派发
窗口端连接的建立及事件的接收
这段代码是Android系统中输入事件接收器的一部分,主要涉及到ViewRootImpl、NativeInputEventReceiver和InputConsumer类。
在ViewRootImpl的WindowInputEventReceiver类中,它继承自InputEventReceiver,用于接收和处理窗口的输入事件。在构造函数中,它接收一个InputChannel和Looper,并调用父类的构造函数进行初始化。
在android_view_InputEventReceiver.cpp中,有一个nativeInit方法,它接收一个Java层的WindowInputEventReceiver对象、一个InputChannel对象和一个MessageQueue对象。这个方法中,它获取InputChannel和MessageQueue,然后创建一个NativeInputEventReceiver对象,并执行其初始化操作。最后,它保留了对这个对象的引用。
在NativeInputEventReceiver类中,它有一个setFdEvents方法,用于设置文件描述符的事件。如果事件发生了变化,它会获取InputChannel的文件描述符,并根据事件类型通过Looper注册InputChannel的可读性事件。如果事件为0,它会从Looper中移除文件描述符。
NativeInputEventReceiver的构造函数中,它接收一个JNI环境、一个弱引用的Java对象、一个InputChannel和一个MessageQueue。在构造函数中,它创建了一个InputConsumer对象,并保存了MessageQueue。
总的来说,这段代码的主要功能是创建和管理输入事件接收器,并处理输入事件。
在ViewRootImpl的WindowInputEventReceiver类中,它继承自InputEventReceiver,用于接收和处理窗口的输入事件。在构造函数中,它接收一个InputChannel和Looper,并调用父类的构造函数进行初始化。
在android_view_InputEventReceiver.cpp中,有一个nativeInit方法,它接收一个Java层的WindowInputEventReceiver对象、一个InputChannel对象和一个MessageQueue对象。这个方法中,它获取InputChannel和MessageQueue,然后创建一个NativeInputEventReceiver对象,并执行其初始化操作。最后,它保留了对这个对象的引用。
在NativeInputEventReceiver类中,它有一个setFdEvents方法,用于设置文件描述符的事件。如果事件发生了变化,它会获取InputChannel的文件描述符,并根据事件类型通过Looper注册InputChannel的可读性事件。如果事件为0,它会从Looper中移除文件描述符。
NativeInputEventReceiver的构造函数中,它接收一个JNI环境、一个弱引用的Java对象、一个InputChannel和一个MessageQueue。在构造函数中,它创建了一个InputConsumer对象,并保存了MessageQueue。
总的来说,这段代码的主要功能是创建和管理输入事件接收器,并处理输入事件。
* frameworks/base/core/java/android/view/ViewRootImpl.java
final class WindowInputEventReceiver extends InputEventReceiver {public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {super(inputChannel, looper);}...
}* frameworks/base/core/jni/android_view_InputEventReceiver.cpp
static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak,jobject inputChannelObj, jobject messageQueueObj) {sp<InputChannel> inputChannel = android_view_InputChannel_getInputChannel(env,inputChannelObj);...//获取UI主线程的消息队列sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);...//创建NativeInputEventReceiver对象sp<NativeInputEventReceiver> receiver = new NativeInputEventReceiver(env,receiverWeak, inputChannel, messageQueue);//执行其初始化操作 status_t status = receiver->initialize();...receiver->incStrong(gInputEventReceiverClassInfo.clazz); // retain a reference for the objectreturn reinterpret_cast<jlong>(receiver.get());
}void NativeInputEventReceiver::setFdEvents(int events) {if (mFdEvents != events) {mFdEvents = events;//获取InputChannel的fd int fd = mInputConsumer.getChannel()->getFd();if (events) {//通过Looper注册InputChannel的可读性事件 mMessageQueue->getLooper()->addFd(fd, 0, events, this, NULL);} else {mMessageQueue->getLooper()->removeFd(fd);}}
}class NativeInputEventReceiver : public LooperCallback {...InputConsumer mInputConsumer;sp<MessageQueue> mMessageQueue;...NativeInputEventReceiver::NativeInputEventReceiver(JNIEnv* env,jobject receiverWeak, const sp<InputChannel>& inputChannel,const sp<MessageQueue>& messageQueue) :mReceiverWeakGlobal(env->NewGlobalRef(receiverWeak)),mInputConsumer(inputChannel), mMessageQueue(messageQueue),mBatchedInputEventPending(false), mFdEvents(0) {...}
}
NativelnputEventReceiver保存了Java层InputEventReceiver对象的引用,并创建了一个InputConsumer类型的对象对InputChannel进行封装。 InputConsumer 与InputPublisher一样,它也封装了InputChannel,负责对其进行写入和读取操作,同时也负责InputMessage的封装与解析。不过它们的功能正好相反,InputConsumer接收的是输入事件,发送的则是反馈。
通过Looper 监听InputChannel的可读性事件,当有InputMessage可读时,NativelnputEventReceiver的handleEvent() 函数会被Looper 调用,然后调用consumeEvents通过一个循环调用InputConsumer从InputChannel中读取事件,然后回调Java层的onlnputEvent()函数。在Java层完成事件的处理后,便可通过InputConsumer发送处理完毕的反馈给InputDispatcher。
这段代码是Android系统中处理输入事件的核心部分,主要分为两个部分:NativeInputEventReceiver的handleEvent和consumeEvents函数。
NativeInputEventReceiver::handleEvent函数:
这个函数主要处理从InputChannel接收到的输入事件。如果事件类型是ALOOPER_EVENT_INPUT,那么会调用consumeEvents函数处理事件。如果事件类型是ALOOPER_EVENT_OUTPUT,那么会将mFinishQueue中的事件发送给InputChannel。
NativeInputEventReceiver::consumeEvents函数:
这个函数从InputChannel中读取输入事件,并根据事件类型创建KeyEvent或MotionEvent对象,然后通过JNI回调Java层的InputEventReceiver的dispatchInputEvent函数。
这段代码的主要流程如下:
从InputChannel中读取输入事件,并解析为InputEvent对象。
根据事件类型创建KeyEvent或MotionEvent对象。
通过JNI回调Java层的InputEventReceiver的dispatchInputEvent函数,将事件分发给Java层处理。
注意:
ALOOPER_EVENT_INPUT:表示有新的输入事件到达。
ALOOPER_EVENT_OUTPUT:表示有新的输入事件需要发送。
consumeEvents函数:从InputChannel中读取输入事件,并处理。
dispatchInputEvent函数:将输入事件分发给Java层处理。
这个代码是Android系统处理输入事件的核心部分,它负责将输入事件从Native层转换为Java层,并分发给Java层的InputEventReceiver处理。
* frameworks/base/core/jni/android_view_InputEventReceiver.cpp
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;}if (events & ALOOPER_EVENT_OUTPUT) {for (size_t i = 0; i < mFinishQueue.size(); i++) {const Finish& finish = mFinishQueue.itemAt(i);//将seq与handle两个信息以InputMessage的形式写入InputChannel中 if (events & ALOOPER_EVENT_OUTPUT) {status_t status = mInputConsumer.sendFinishedSignal(finish.seq, finish.handled);}...}}
}
status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {...bool skipCallbacks = false;for (;;) {InputEvent* inputEvent;//通边mInputConsumer的consume()函数从Inputchannel中读取一条InputMessage。 //解析为InputEvent后,通过inputEvent参数传出status_t status = mInputConsumer.consume(&mInputEventFactory,consumeBatches, frameTime, &seq, &inputEvent,&motionEventType, &touchMoveNum, &flag);...//根据事件的类型分别创建KeyEvent与MotlonEvent类型的Java对象 switch (inputEvent->getType()) {case AINPUT_EVENT_TYPE_KEY:...inputEventObj = android_view_KeyEvent_fromNative(env,static_cast<KeyEvent*>(inputEvent));...}case AINPUT_EVENT_TYPE_MOTION: {...}}//通过JNI回调Java层的InputEventReceiver的dispatchInputEvent()函数 env->CallVoidMethod(receiverObj.get(),gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);...}
dispatchInputEvent函数首先在字典中保存来自InputDispatcher的事件序列号,以满足发送反馈之需。之后便调用onlnputEvent()函数,交由子类进行输入事件的实际处理工作。如果时output事件则调用mInputConsumer.sendFinishedSignal。
事件的反馈动作可由InputEventReceiver.finishlnputEvent()发起,然后进入到NativeInputEventReceiver::finishInputEvent,然后同样调用mInputConsumer.sendFinishedSignal。
这段代码是Android系统中用于处理输入事件的C++代码,主要分为两个部分:nativeFinishInputEvent和NativeInputEventReceiver::finishInputEvent。
nativeFinishInputEvent函数是Java层的JNI接口,用于通知NativeInputEventReceiver对象完成一个输入事件的处理。它接收一个NativeInputEventReceiver对象的指针,序列号(seq)和处理结果(handled)作为参数,然后调用NativeInputEventReceiver对象的finishInputEvent方法。
NativeInputEventReceiver::finishInputEvent方法是NativeInputEventReceiver类的方法,它接收一个序列号(seq)和处理结果(handled)作为参数,然后调用mInputConsumer对象的sendFinishedSignal方法。这个方法的返回值是一个status_t类型的状态码,表示操作的结果。如果返回值非零,表示操作失败,可能是由于WOULD_BLOCK错误。
如果返回值是WOULD_BLOCK,那么会将当前的序列号和处理结果添加到一个名为mFinishQueue的队列中,并设置文件描述符的事件类型为ALOOPER_EVENT_INPUT | ALOOPER_EVENT_OUTPUT。
如果返回值是OK,那么直接返回OK。
总的来说,这段代码的主要功能是处理输入事件的完成信号,如果处理过程中遇到WOULD_BLOCK错误,那么会将这个错误记录下来,等待后续处理。
* frameworks/base/core/jni/android_view_InputEventReceiver.cpp
static void nativeFinishInputEvent(JNIEnv* env, jclass clazz, jlong receiverPtr,jint seq, jboolean handled) {sp<NativeInputEventReceiver> receiver = reinterpret_cast<NativeInputEventReceiver*>(receiverPtr);status_t status = receiver->finishInputEvent(seq, handled);...
}status_t NativeInputEventReceiver::finishInputEvent(uint32_t seq, bool handled) {status_t status = mInputConsumer.sendFinishedSignal(seq, handled);if (status) {if (status == WOULD_BLOCK) {Finish finish;finish.seq = seq;finish.handled = handled;mFinishQueue.add(finish);if (mFinishQueue.size() == 1) {setFdEvents(ALOOPER_EVENT_INPUT | ALOOPER_EVENT_OUTPUT);}return OK;}}return status;
}
当窗口端的InputChannel被写入数 据时,会触发服务端InputChannel可读事件,因此InputDispatcher的派发线程被唤醒并执行 handleReceiveCallback()回调。
根据可读的Inputchannel的描述符获取对应的Connection对象。
然后在循环中不断地读取尽可能多的反馈信息。
调用finishDispatchCycleLocked()函数调用onDispatchCycleFinishedLocked->doDispatchCycleFinishedLockedInterruptible完成对反馈的处理。
对于输入事件反馈的处理主要有两个方面:
将事件从Connection的waitQueue队列中删除。这个删除动作标志着此事件的派发流程完成,也意味着这个事件经过漫长的加工、传递之旅后生命的结束。
最后调用startDispatchCycleLocked0函数继续尝试发送队列中的下一个事件,又回到发送流程。
这段代码是Android系统中InputDispatcher的实现,主要用于处理输入事件的分发。
handleReceiveCallback
函数是处理接收回调的函数,它接收一个文件描述符(fd)、事件类型(events)和数据(data)作为参数。首先,它通过文件描述符获取对应的Connection对象,然后根据事件类型,如果是输入事件,则不断地从Connection对象的inputPublisher中接收反馈信息,并调用finishDispatchCycleLocked
函数处理反馈。如果处理时间超过2s,则打印信息。
doDispatchCycleFinishedLockedInterruptible
函数是处理分发周期完成的函数,它接收一个命令条目(commandEntry)作为参数。首先,它从waitQueue中按照序号取出反馈对应的事件,如果事件处理时间超过2s,则打印信息。然后,将事件从waitQueue中移除,并启动下一次发送循环。注意,这两个函数都在一个锁的保护下执行,以保证线程安全。
这段代码的主要逻辑是:接收输入事件,处理反馈,并管理事件的分发。如果事件处理时间过长,则打印警告信息。
int InputDispatcher::handleReceiveCallback(int fd, int events, void* data) {InputDispatcher* d = static_cast<InputDispatcher*>(data);{ // acquire lockstd::scoped_lock _l(d->mLock);//根据可读的Inputchannel的描述符获取对应的Connection对象ssize_t connectionIndex = d->mConnectionsByFd.indexOfKey(fd);...bool notify;sp<Connection> connection = d->mConnectionsByFd.valueAt(connectionIndex);if (!(events & (ALOOPER_EVENT_ERROR | ALOOPER_EVENT_HANGUP))) {if (!(events & ALOOPER_EVENT_INPUT)) {return 1;}nsecs_t currentTime = now();...for (;;) {uint32_t seq;bool handled;//然后在循环中不断地读取尽可能多的反馈信息 status = connection->inputPublisher.receiveFinishedSignal(&seq, &handled);if (status) {break;}//调用finishDispatchCycleLocked()函数完成对反馈的处理 d->finishDispatchCycleLocked(currentTime, connection, seq, handled);gotOne = true;}...} else {...}...} // release lock
}void InputDispatcher::doDispatchCycleFinishedLockedInterruptible(CommandEntry* commandEntry) {sp<Connection> connection = commandEntry->connection;...//从waitQueue中,按照序号取出反馈对应的事件 DispatchEntry* dispatchEntry = connection->findWaitQueueEntry(seq);if (dispatchEntry) {nsecs_t eventDuration = finishTime - dispatchEntry->deliveryTime;//大于2s,则打印信息if (eventDuration > SLOW_EVENT_PROCESSING_WARNING_TIMEOUT) {std::string msg = StringPrintf("Window '%s' spent %0.1fms processing the last input event: ",connection->getWindowName().c_str(), eventDuration * 0.000001f);dispatchEntry->eventEntry->appendDescription(msg);ALOGI("%s", msg.c_str());}...if (dispatchEntry == connection->findWaitQueueEntry(seq)) {//将事件从waitQueue中移除 connection->waitQueue.dequeue(dispatchEntry);...}//启动下一次发送循环startDispatchCycleLocked(now(), connection);}
}
InputDispatcher线程调用InputPublisher的publishKeyEvent向UI主线程发送input事件;
UI主线程接收到该事件后,调用InputConsumer的consumeEvents来处理该事件, 一路执行到ViewRootImpl.deliverInputEvent()方法;
UI主线程经过一系列的InputStage来处理, 当事件分发完成,则会执行finishInputEvent()方法.再进一步调用InputConsumer::sendFinishedSignal 告知InputDispatcher线程该时事件已处理完成;
InputDispatcher线程收到该事件后, 执行InputDispatcher::handleReceiveCallback();最终会调用doDispatchCycleFinishedLockedInterruptible()方法 ,将dispatchEntry事件从等待队列(waitQueue)中移除。
应用端在setView时,会创建InputEventReceiver并传入Looper.myLooper(),即主线程的looper,初始化时获取主线程的MessageQueue,通过在native层looper监听fd事件,如果有按键消息时,会回调到NativeInputEventReceiver::handleEvent,里面会通过反射InputEventReceiver对象的dispatchInputEvent和dispatchMotionEventInfo。
ANR处理
findTouchedWindowTargetsLocked和findFocusedWindowTargetsLocked会调用handleTargetsNotReadyLocked对原因进行记录,并安排时间尝试重试派发,或者引发ANR,除非调用了resetANRTimeoutsLocked。
主要是以下4个场景,会有机会执行resetANRTimeoutsLocked:
解冻屏幕, 系统开/关机的时刻点 (thawInputDispatchingLw, setEventDispatchingLw)。
wms聚焦app的改变 (WMS.setFocusedApp, WMS.removeAppToken)。
设置input filter的过程 (IMS.setInputFilter)。
再次分发事件的过程(dispatchOnceInnerLocked)。
这段代码是Android系统中InputDispatcher类的部分实现,主要用于处理输入事件的分发。
findFocusedWindowTargetsLocked
方法:
首先尝试从
mFocusedWindowHandlesByDisplay
中获取当前焦点的窗口句柄,如果获取失败,则检查mFocusedApplicationHandlesByDisplay
中是否有应用正在启动,如果没有,则返回一个提示信息。如果获取到窗口句柄,则检查该窗口是否准备好接收更多输入。如果窗口未准备好,则返回一个提示信息。
findTouchedWindowTargetsLocked
方法:
遍历
mTempTouchState.windows
,检查每个窗口是否准备好接收更多输入。如果窗口未准备好,则返回一个提示信息。
checkWindowReadyForMoreInputLocked
方法:
如果窗口被暂停,则返回一个提示信息。
获取窗口的Connection索引,如果索引无效,则返回一个提示信息。
如果Connection的状态不是正常状态,则返回一个提示信息。
如果Connection的
inputPublisher
被阻塞,则返回一个提示信息。对于按键事件,要求Connection必须处于空闲状态,如果Connection的出站队列或等待队列不为空,则返回一个提示信息。
对于Motion事件,只要窗口能在0.5s内发送反馈即可,如果等待队列不为空且当前时间大于等待队列头部事件的交付时间加上0.5s,则返回一个提示信息。
如果以上条件都不满足,则返回空字符串,表示窗口已经准备好接收更多输入。
以上代码中,
InputDispatcher
类中的方法主要用于检查窗口是否准备好接收更多输入,如果窗口未准备好,则返回一个提示信息。如果窗口被暂停、Connection状态异常、inputPublisher
被阻塞、Connection的出站队列或等待队列不为空,或者对于Motion事件,等待队列不为空且当前时间大于等待队列头部事件的交付时间加上0.5s,则认为窗口未准备好。注意:以上代码中的
StringPrintf
函数用于格式化字符串,goto Unresponsive
用于跳转到标记为Unresponsive
的标签处,sp
是Smart Pointer的缩写,用于智能指针,std::vector
是C++标准模板库中的动态数组,std::string
是字符串类型,nsecs_t
是64位无符号整型,用于表示纳秒级的时间戳。
int32_t InputDispatcher::findFocusedWindowTargetsLocked(nsecs_t currentTime,const EventEntry* entry, std::vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime) {...sp<InputWindowHandle> focusedWindowHandle = getValueByKey(mFocusedWindowHandlesByDisplay, displayId);sp<InputApplicationHandle> focusedApplicationHandle = getValueByKey(mFocusedApplicationHandlesByDisplay, displayId);if (focusedWindowHandle == nullptr) {if (focusedApplicationHandle != nullptr) {//一般此类问题都是Android应用首次启动时会发生此类问题,此时我们应用本身需要检查一下我们的Android应用重写的Application onCreate方法,Android应用的启动界面是否在onCreate onStart方法中是否存在耗时操作。当然不排除系统原因造成的启动慢,直接导致ANR问题发生的情况injectionResult = handleTargetsNotReadyLocked(currentTime, entry,focusedApplicationHandle, nullptr, nextWakeupTime,"Waiting because no window has focus but there is a ""focused application that may eventually add a window ""when it finishes starting up.");}}...reason = checkWindowReadyForMoreInputLocked(currentTime,focusedWindowHandle, entry, "focused");if (!reason.empty()) {injectionResult = handleTargetsNotReadyLocked(currentTime, entry,focusedApplicationHandle, focusedWindowHandle, nextWakeupTime, reason.c_str());goto Unresponsive;}...
}int32_t InputDispatcher::findTouchedWindowTargetsLocked(nsecs_t currentTime,const MotionEntry* entry, std::vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime,bool* outConflictingPointerActions) {...for (const TouchedWindow& touchedWindow : mTempTouchState.windows) {if (touchedWindow.targetFlags & InputTarget::FLAG_FOREGROUND) {// Check whether the window is ready for more input.std::string reason = checkWindowReadyForMoreInputLocked(currentTime,touchedWindow.windowHandle, entry, "touched");if (!reason.empty()) {injectionResult = handleTargetsNotReadyLocked(currentTime, entry,nullptr, touchedWindow.windowHandle, nextWakeupTime, reason.c_str());goto Unresponsive;}}}...
}td::string InputDispatcher::checkWindowReadyForMoreInputLocked(nsecs_t currentTime,const sp<InputWindowHandle>& windowHandle, const EventEntry* eventEntry,const char* targetType) {// If the window is paused then keep waiting.if (windowHandle->getInfo()->paused) {return StringPrintf("Waiting because the %s window is paused.", targetType);}//首先获取窗口的Connection ssize_t connectionIndex = getConnectionIndexLocked(getInputChannelLocked(windowHandle->getToken()));if (connectionIndex < 0) {return ...;}//InputPublisher被阻塞 sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);if (connection->status != Connection::STATUS_NORMAL) {return StringPrintf(...);}// If the connection is backed up then keep waiting.if (connection->inputPublisherBlocked) {return StringPrintf(...);}//对按键事件来说,要求Connection必须处于空闲状态if (eventEntry->type == EventEntry::TYPE_KEY) {if (!connection->outboundQueue.isEmpty() || !connection->waitQueue.isEmpty()) {return StringPrintf(...);}} else {//对Motion事件来说,可以发送事件的条件相对宽松些,只要窗口能在0.5s内发送反馈即可 if (!connection->waitQueue.isEmpty()&& currentTime >= connection->waitQueue.head->deliveryTime+ STREAM_AHEAD_EVENT_TIMEOUT) {return StringPrintf(...);}}return "";
}
判断窗口是否可以接受事件的依据有两个:ImputPublisher是否被阻塞以及Connection两个队列的状态。
-
InputPublisher的工作是将事件信息写入InputChannel中,如果窗口端因为某种原因迟迟未能从InputChannel中将事件读取就会导致SocketPair的写入缓冲区满。
-
Connection两个队列的状态体现了发送循环的状态。如果两个队列至少有一个队列为空,则表示Connection正处于发送循环的过程中,否则处于空闲状态。
对按键事件来说,仅当Connection处于空闲状态,也就是窗口已经完成对之前事件的响 应之后才会发送给窗口。因为之前的输入事件有可能会影响焦点窗口,进而影响按键事件的接收者。例如,用户快速地按下了两次BACK键,第一个BACK键将会发送给位于顶端的窗口,这个事件可能会导致窗口关闭,因此第一个BACK键的处理行为决定了第二个BACK应该发送给哪个窗口。因此按键事件的发送要求窗口完成对所有之前事件的处理。
而Motion事件的条件则相对宽松些,允许Connection处于发送循环的过程中,但是如果等待队列中的第一个事件没能在0.5s获得反馈,则判定窗口处于未响应状态。这是因为Motion事件具有实时性的特点-----用户的意图就是希望输入事件发送给他所看到的窗口,所以不在乎之前事件的处理结果。
焦点窗口的更新通过WindowManagerService.addView->WindowManagerService.updateFocusedWindowLocked->RootWindowContainer.updateFocusedWindowLocked->DisplayContent.updateFocusedWindowLocked->DisplayContent.findFocusedWindowIfNeeded
每个DisplayContent都拥有自己的焦点窗口,然而真正的焦点窗口只有一个。于是Id越小的DispayContent的焦点窗口具有更高的优先级。设备的主屏幕Id为0,因此主屏幕将拥有最高的焦点优先级。
寻找焦点窗口的基本原则是沿Z-Order的顺序从上向下遍历窗口,第一WindowState.canReceiveKeys()返回值为true的窗口拥有焦点。
可以得到焦点窗口的选择有如下原则:
DisplayContent的Id值越低,其内部窗口拥有越高的焦点优先级。
窗口在DisplayContent中的显示次序越靠前,其拥有越高的焦点优先级。
焦点窗口必须处于正常显示状态(没有调用removeWindow()),处于可见状态,并且没有指定FLAG_NOT_FOCUSABLE选项。
所有位于当前Activity之下的窗口不得获取焦点。若当前Activity以及其上都没有窗口。
满足上述条件,则此DisplayContent没有焦点窗口。
这段代码是Android系统窗口管理服务的一部分,主要用于更新并管理焦点窗口。
这段代码的主要功能是遍历所有的子窗口内容(DisplayContent),并调用它们的
updateFocusedWindowLocked
方法来更新焦点窗口。这个方法的参数包括模式(mode)和是否更新输入窗口(updateInputWindows)。在遍历过程中,它会记录下当前遍历到的所有显示器的顶级焦点窗口的显示ID(topFocusedDisplayId)。
如果当前的顶级焦点显示ID与之前记录的顶级焦点显示ID不同,那么就更新顶级焦点显示ID,并通过WindowManagerService的InputManager和Policy设置新的顶级焦点显示ID。
最后,返回一个布尔值changed,表示是否有任何改变。
注意:INVALID_DISPLAY是一个无效的显示ID,通常定义为-1。
这个方法的主要目的是确保所有的显示器都有正确的焦点窗口,并且当焦点窗口发生变化时,能够及时更新。
这段代码使用了面向对象的编程思想,将每个显示器的焦点窗口更新操作封装在了DisplayContent类中,使得代码更加模块化,易于维护和扩展。
* frameworks/base/services/core/java/com/android/server/wm/RootWindowContainer.java
boolean updateFocusedWindowLockedupdateFocusedWindowLocked(int mode, boolean updateInputWindows) {...int topFocusedDisplayId = INVALID_DISPLAY;for (int i = mChildren.size() - 1; i >= 0; --i) {final DisplayContent dc = mChildren.get(i);changed |= dc.updateFocusedWindowLocked(mode, updateInputWindows, topFocusedDisplayId);...}...if (mTopFocusedDisplayId != topFocusedDisplayId) {mTopFocusedDisplayId = topFocusedDisplayId;mWmService.mInputManager.setFocusedDisplay(topFocusedDisplayId);mWmService.mPolicy.setTopFocusedDisplay(topFocusedDisplayId);}return changed;}
这段代码是Android系统中InputDispatcher的handleTargetsNotReadyLocked方法的实现,主要功能是处理目标应用或窗口未准备好接收输入事件的情况。
首先,检查应用和窗口是否都为空。如果都为空,说明系统尚未完成启动。
如果应用或窗口不为空,则获取引发ANR(Application Not Responding)的超时时间。如果目标窗口存在,则获取窗口指定的超时时间;如果目标窗口不存在,则从AMS(Activity Manager Service)获取超时时间。
设置引发ANR的时间点,并设置引发ANR的时间,并将
mInputTargetWaitTimeoutExpired
设置为false。获取应用或窗口的token,如果两者都为空,则获取应用的token。
检查是否已经到达引发ANR的时间点。如果到达,则调用
onANRLocked
方法,并设置nextWakeupTime
为最小值,返回INPUT_EVENT_INJECTION_TIMED_OUT
。如果没有到达引发ANR的时间点,则检查
mInputTargetWaitTimeoutTime
是否小于当前的nextWakeupTime
,如果是,则更新nextWakeupTime
,返回INPUT_EVENT_INJECTION_PENDING
。这段代码的主要目的是在应用或窗口未准备好接收输入事件时,处理超时和ANR的情况。
int32_t InputDispatcher::handleTargetsNotReadyLocked(nsecs_t currentTime,const EventEntry* entry,const sp<InputApplicationHandle>& applicationHandle,const sp<InputWindowHandle>& windowHandle,nsecs_t* nextWakeupTime, const char* reason) {if (applicationHandle == nullptr && windowHandle == nullptr) {if (mInputTargetWaitCause != INPUT_TARGET_WAIT_CAUSE_SYSTEM_NOT_READY) {//系统尚未完成启动...}} else {//如果是第一次发生窗口未响应的情况,则记录下未响应的窗口信息,并设置引发ANR的时间点if (mInputTargetWaitCause != INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY) {//获取引发ANR的超时时间 nsecs_t timeout;if (windowHandle != nullptr) {//如果有目标窗口,则获取由窗口所指定的超时时间 timeout = windowHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);} else if (applicationHandle != nullptr) {//如果没有目标窗口,则从AMS获取超时时间 timeout = applicationHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);} else {timeout = DEFAULT_INPUT_DISPATCHING_TIMEOUT;}mInputTargetWaitCause = INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY;//检测到未响应的时间 mInputTargetWaitStartTime = currentTime;//设置引发ANR的时间 mInputTargetWaitTimeoutTime = currentTime + timeout;//当引发ANR后将被置true mInputTargetWaitTimeoutExpired = false;mInputTargetWaitApplicationToken.clear();if (windowHandle != nullptr) {mInputTargetWaitApplicationToken = windowHandle->getApplicationToken();}if (mInputTargetWaitApplicationToken == nullptr && applicationHandle != nullptr) {mInputTargetWaitApplicationToken = applicationHandle->getApplicationToken();}}}if (mInputTargetWaitTimeoutExpired) {return INPUT_EVENT_INJECTION_TIMED_OUT;}//检查是否引发ANRif (currentTime >= mInputTargetWaitTimeoutTime) {//当前时间大于引发ANR的时间后,则引发ANRonANRLocked(currentTime, applicationHandle, windowHandle,entry->eventTime, mInputTargetWaitStartTime, reason);*nextWakeupTime = LONG_LONG_MIN;return INPUT_EVENT_INJECTION_PENDING;} else {//如果尚未到达引发ANR的时间点,设置nextWakeupTime后返回,等待下次再试 if (mInputTargetWaitTimeoutTime < *nextWakeupTime) {*nextWakeupTime = mInputTargetWaitTimeoutTime;}return INPUT_EVENT_INJECTION_PENDING;}
}
InputDispatcher从派发队列中获取了一个事件mPendingEvent,并为它查找目标窗口,然后通过checkWindowReadyForMoreInputLocked确定此窗口是否可以接收事件。如果可以则将事件放入窗口的Connection对象的发送队列中并启动发送循环,否则调用handleTargetsNotReadyLocked()计算引发ANR的时间点,然后通过返回INPUT_EVENT。
INJECTION_PENDING停止对mPendingEvent的派发工作,并通过设置 nextWakeupTime使派发循环进入休眠状态。休眠的过程有可能因为窗口反馈到来、新输入事件到来或新的窗口信息到来而唤醒,派发线程便重新开始对mPendingEvent的派发过程,进而重新寻找目标窗口,再通过checkWindowReadyForMoreInputLocked()检查目标窗口是否准备好接收事件,如果可以接收事件,则将其提交给Connection进行发送,并重置之前所设置的ANR信息。否则再次进入handleTargetsNotReadyLocked,这时将当前时间与ANR时间进行对比,以决定引发ANR还是再次使派发线程进入休眠。
发生ANR调用onANRLocked()的过程会将doNotifyANRLockedInterruptible加入mCommandQueue。 在下一轮InputDispatcher.dispatchOnce的过程中会先执行runCommandsLockedInterruptible()方法,取出 mCommandQueue队列的所有命令逐一执行,调用流程为InputDispatcher::doNotifyANRLockedInterruptible->NativeInputManager::notifyANR->IMS.notifyANR->InputManagerCallback.notifyANR。
这段代码是Android窗口管理器(Window Manager)中的一个方法,用于处理应用程序无响应(ANR)的情况。当应用程序在一定时间内无法响应用户的输入事件时,就会触发这个方法。
这个方法的参数是:
IBinder token
:应用程序窗口的token,用于标识应用程序窗口。
String reason
:超时的原因,例如"Input dispatching timed out"。方法的主要步骤如下:
首先,它获取全局锁
mService.mGlobalLock
,这是一个同步锁,用于保证在同一时间只有一个线程可以访问共享资源。然后,它检查
windowState
是否不为空。如果不为空,说明超时的原因是与窗口状态相关。如果
windowState
不为空,它会记录一条日志信息,说明输入事件分发超时,并发送给windowState.mAttrs.getTitle()
。接着,它获取系统层级
systemAlertLayer
,这个层级用于决定窗口的显示位置。如果
windowState.mBaseLayer
大于systemAlertLayer
,那么窗口就会显示在系统警告之上。如果
appWindowToken
不为空,说明超时的原因是与应用程序窗口相关。它会记录一条日志信息,说明输入事件分发超时,并发送给appWindowToken.stringName
。如果
appWindowToken
和windowState
都为空,说明超时的原因不明。它会记录一条日志信息,说明输入事件分发超时。最后,它调用
mService.saveANRStateLocked(appWindowToken, windowState, reason)
方法保存ANR状态。方法返回0,表示中止分发事件。
这个方法的主要目的是记录ANR事件,并保存相关的信息,以便后续处理。
public long notifyANR(IBinder token, String reason) {...synchronized (mService.mGlobalLock) {...if (windowState != null) {Slog.i(TAG_WM, "Input event dispatching timed out "+ "sending to " + windowState.mAttrs.getTitle()+ ". Reason: " + reason);int systemAlertLayer = mService.mPolicy.getWindowLayerFromTypeLw(TYPE_APPLICATION_OVERLAY, windowState.mOwnerCanAddInternalSystemWindow);aboveSystem = windowState.mBaseLayer > systemAlertLayer;} else if (appWindowToken != null) {Slog.i(TAG_WM, "Input event dispatching timed out "+ "sending to application " + appWindowToken.stringName+ ". Reason: " + reason);} else {Slog.i(TAG_WM, "Input event dispatching timed out "+ ". Reason: " + reason);}mService.saveANRStateLocked(appWindowToken, windowState, reason);}...return 0; // abort dispatching}
InputMonitor.notifyANR完成, 当发生ANR时system log中会出现以下信息, 并且TAG=WindowManager: Input event dispatching timed out xxx. Reason: + reason, 其中xxx取值:
窗口类型: sending to windowState.mAttrs.getTitle()。
应用类型: sending to application appWindowToken.stringName。
其他类型: 则为空。
ANR reason主要有以下几类:
这段代码是Android系统中的InputDispatcher类的一部分,用于检查一个特定的窗口(window)是否准备好接收更多的输入事件。InputDispatcher是Android系统中负责管理输入事件的分发和处理的组件。
代码的主要功能如下:
首先,它会检查窗口是否被暂停(paused)。如果窗口被暂停,那么它将等待,直到窗口被激活。
接下来,它会检查窗口的输入通道是否已经注册到输入分发器中。如果没有,那么它将等待,直到窗口的输入通道被注册。
然后,它会检查连接的状态是否正常。如果连接状态异常,那么它将等待,直到连接状态恢复正常。
接着,它会检查连接是否被阻塞。如果连接被阻塞,那么它将等待,直到连接不被阻塞。
如果当前事件是按键事件(TYPE_KEY),它会检查连接的出站队列(outboundQueue)和等待队列(waitQueue)是否为空。如果不为空,那么它将等待,直到窗口处理完之前发送的所有输入事件。
如果当前事件不是按键事件,它会检查等待队列是否不为空,并且当前时间是否超过了等待队列头部事件的交付时间加上一个预设的超时时间(STREAM_AHEAD_EVENT_TIMEOUT)。如果是,那么它将等待,直到窗口处理完某些之前发送的输入事件。
如果以上所有条件都不满足,函数将返回一个空字符串,表示窗口已经准备好接收更多的输入。
这段代码的主要目的是确保输入事件能够被正确、有效地分发到正确的窗口,并且避免在窗口处理输入事件时出现阻塞或延迟。
注意,这段代码中的
StringPrintf
函数用于格式化字符串,getConnectionIndexLocked
和getInputChannelLocked
函数可能是InputDispatcher类中的其他方法,用于获取连接索引和输入通道,sp<>
和wp<>
是Android的智能指针类型,用于管理对象的生命周期。这段代码是Android系统输入事件分发流程中的一个关键部分,它确保了输入事件的正确分发和处理。
std::string InputDispatcher::checkWindowReadyForMoreInputLocked(nsecs_t currentTime,const sp<InputWindowHandle>& windowHandle, const EventEntry* eventEntry,const char* targetType) {// If the window is paused then keep waiting.if (windowHandle->getInfo()->paused) {return StringPrintf("Waiting because the %s window is paused.", targetType);}// If the window's connection is not registered then keep waiting.ssize_t connectionIndex = getConnectionIndexLocked(getInputChannelLocked(windowHandle->getToken()));if (connectionIndex < 0) {return StringPrintf("Waiting because the %s window's input channel is not ""registered with the input dispatcher. The window may be in the process ""of being removed.", targetType);}// If the connection is dead then keep waiting.sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);if (connection->status != Connection::STATUS_NORMAL) {return StringPrintf("Waiting because the %s window's input connection is %s.""The window may be in the process of being removed.", targetType,connection->getStatusLabel());}// If the connection is backed up then keep waiting.if (connection->inputPublisherBlocked) {return StringPrintf("Waiting because the %s window's input channel is full. ""Outbound queue length: %d. Wait queue length: %d.",targetType, connection->outboundQueue.count(), connection->waitQueue.count());}if (eventEntry->type == EventEntry::TYPE_KEY) {if (!connection->outboundQueue.isEmpty() || !connection->waitQueue.isEmpty()) {return StringPrintf("Waiting to send key event because the %s window has not ""finished processing all of the input events that were previously ""delivered to it. Outbound queue length: %d. Wait queue length: %d.",targetType, connection->outboundQueue.count(), connection->waitQueue.count());}} else {if (!connection->waitQueue.isEmpty()&& currentTime >= connection->waitQueue.head->deliveryTime+ STREAM_AHEAD_EVENT_TIMEOUT) {return StringPrintf("Waiting to send non-key event because the %s window has not ""finished processing certain input events that were delivered to it over ""%0.1fms ago. Wait queue length: %d. Wait queue head age: %0.1fms.",targetType, STREAM_AHEAD_EVENT_TIMEOUT * 0.000001f,connection->waitQueue.count(),(currentTime - connection->waitQueue.head->deliveryTime) * 0.000001f);}}return "";
}
无窗口, 有应用:
Waiting because no window has focus but there is a focused application that may eventually add a > window when it finishes starting up
。
分析:
一般此类问题都是Android应用首次启动时会发生此类问题,此时我们应用本身需要检查一下我们的Android应用重写的Application onCreate方法,Android应用的启动界面是否在onCreate onStart方法中是否存在耗时操作。当然不排除系统原因造成的启动慢,直接导致ANR问题发生的情况。
Android应用启动的时候都是先创建Application,创建号Application之后,才会执行应用启动的主Activity,再执行该Activity的onCreate,onStart,onResume方法,然后我们的应用窗口便是在onResume中才去向WindowManager添加注册的。因此在注册添加窗口之前,application或者启动的Activity的生命周期onCreate,onStart的任意方法,做了耗时操作,或者他们加载一起的执行时间过长,都是能够导致无窗口,有应用类型的Input ANR问题发生的。
非按键事件,事件等待队列不为空且头事件分发超时500ms:Waiting to send non-key event because the [targetType] window has not finished processing certain input events that were delivered to it over 500ms ago. Wait queue length: [waitQueue长度]. Wait queue head age: [等待时长]
。
分析:
这种情况一般时应用在处理touch事件超时引起的。
比如在使用Handler向UI主线程的looper messagequeue post一个runnable callback,这个runnable里面便是执行的耗时超过5s,则会抛出异常,因为应用端的消息接收处理时在主线的一次Looper循环中读取并分发事件,处理完成后反馈到InputDIspatch,如果主线程的Looper耗时,事件超过5s没处理,则会引起anr。
或者应用在处理touch事件耗时超过5s,则也会抛出anr。
CPU占用过大,引起事件处理不及时,比如启动多个线程同时并发执行
07-18 13:20:19.853 1948 2127 I WindowManager: Input event dispatching timed out sending to com.example.anrtest/com.example.anrtest.MainActivity. Reason: Waiting to send non-key event because the touched window has not finished processing certain input events that were delivered to it over 500.0ms ago. Wait queue length: 5. Wait queue head age: 8503.9ms
。
按键事件,输出队列或事件等待队列不为空:Waiting to send key event because the [targetType] window has not finished processing all of the input events that were previously delivered to it. Outbound queue length: [outboundQueue长度]. Wait queue length: [waitQueue长度]
。
分析:
这种情况一般时在应用处理key事件超时引起的,比如应用重载onKeyPreIme方法处理耗时,则会引起anr
07-18 13:41:07.925 1948 2127 I WindowManager: Input event dispatching timed out sending to com.example.anrtest/com.example.anrtest.MainActivity. Reason: Waiting to send key event because the focused window has not finished processing all of the input events that were previously delivered to it. Outbound queue length: 0. Wait queue length: 2.
下面四种情况比较少见:
窗口暂停:
Waiting because the [targetType] window is paused.
窗口未连接,窗口所在的进程可能正在被移除:
Waiting because the [targetType] window’s input channel is not registered with the input dispatcher. The window may be in the process of being removed.
窗口连接已死亡:
Waiting because the [targetType] window’s input connection is [Connection.Status]. The window may be in the process of being removed.
窗口连接已满:
Waiting because the [targetType] window’s input channel is full. Outbound queue length: [outboundQueue长度]. Wait queue length: [waitQueue长度].
targetType: 取值为”focused”或者”touched”。
Connection.Status: 取值为”NORMAL”,”BROKEN”,”ZOMBIE。
错误:
E InputChannel-JNI: Error 24 dup channel fd 74
我们可以在系统源码中找到它的定义 ERRNO_VALUE(EMFILE, 24);指的就是我们的应用进程fd泄漏,越过上限值。
进入应用进程/proc/1948 查看limits文件:
Limit Soft Limit Hard Limit Units
Max cpu time unlimited unlimited seconds
Max file size unlimited unlimited bytes
Max data size unlimited unlimited bytes
Max stack size 8388608 unlimited bytes
Max core file size 0 unlimited bytes
Max resident set unlimited unlimited bytes
Max processes 21523 21523 processes
Max open files 31744 32768 files
这里Max open files 31744就是系统对当前进程fd打开数量的限制。cd 到fd目录,然后ls -l | wc -l 查看进程创建了多少fd。
相关文章:
Android Input的流程和原理
Android Input事件机制 Android系统是由事件驱动的,而Input是最常见的事件之一,用户的点击、滑动、长按等操作,都属于Input事件驱动,其中的核心就是InputReader和InputDispatcher。InputReader和InputDispatcher是跑在system_serv…...
InfiMM-WebMath-40B——利用由 24 亿数学文档组成的数据集提高 LLM 的数学性能
1. 前言 论文地址:https://arxiv.org/abs/2409.12568 本文提出了一个新的大规模多模态预训练数据集 InfiMM-WebMath-40B,以提高数学推理能力。该数据集包含 24 亿个科学和数学相关的网络文档、85 亿个图片 URL 和约 400 亿个文本标记。该数据集支持多模…...
Swarm-LIO: Decentralized Swarm LiDAR-inertial Odometry论文翻译
文章目录 前言一、介绍二、相关工作三、方法A. 问题表述B. 框架概述C. 群体系统的初始化D. 去中心化激光雷达-惯性状态估计 四. 实验A. 室内飞行B. 退化环境飞行C. 去中心化部署 五. 结论和未来工作 前言 原文:原文 准确的自我状态和相对状态估计是完成群体任务的关…...
第十八章 Vue组件样式范围配置之scoped
目录 一、引言 二、案例演示 2.1. 工程结构图 2.2. 核心代码 2.2.1. main.js 2.2.2. App.vue 2.2.3. BaseOne.vue 2.2.4. BaseTwo.vue 2.3. 运行效果 2.4. 调整代码 2.4.1. BaseTwo.vue 2.4.2. 运行效果 三、scoped原理 一、引言 前面的几个章节在介绍组件的时…...
【JavaScript】JavaScript 进阶-3-编程思想构造函数原型(更新中)
目录 编程思想构造函数原型 编程思想 构造函数 原型...
头歌网络安全爬虫
#!/usr/bin/env python # -*- coding: utf-8 -*- # Time : 2020/4/8 8:19 # File : info.py # ---------------------------------------------- # ☆ ☆ ☆ ☆ ☆ ☆ ☆ # >>> Author : Alex # >>> QQ : 2426671397 # >>> Mail…...
二、k8s快速入门之docker+Kubernetes平台搭建
centosmaster192.168.100.10centosnode1192.168.100.20centosnode2192.168.100.30 除特殊说明命令都需要在三台都执行 ⭐️ k8s 的指令: kubeadm:用来初始化集群的指令kubelet: 在集群中的每个节点上用来启动Pod和容器kubectl: 用来与集群通信的命令行…...
k8s的发展历史
Kubernetes(通常缩写为 K8s)是一个开源的容器编排平台,用于自动化应用程序的部署、扩展和管理。它的发展历史可以追溯到多个关键的里程碑: 1. 起源(2013 年) Kubernetes 的起源可以追溯到 Google 的内部项…...
Pytorch lightning多机多卡训练通讯问题(NCCL error)排查
一、问题 单机多卡可以正常训练模型,多机多卡数据加载完成后卡住不动,排查两台机器可以ping通,表明网络没有问题,查看bug信息是NCCL通信问题。报错信息大致如下: torch.distributed.DistBackendError: NCCL error in: …/torch/c…...
React如何实现Vue的keepAlive功能
前言 在React中,默认情况下组件在被卸载后会销毁状态,这与Vue的keep-alive功能不同。在Vue中,keep-alive组件可以缓存组件状态,在路由切换时重新挂载。实现这一功能在React中并不简单,但我们可以借助一个第三方库——…...
在 Ubuntu 22.04 LTS 上安装 NVM (Node Version Manager) 管理和切换不同版本的 Node.js npm
安装 nvm curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash# nvm --version 0.40.1安装 Node.js 的不同版本 列出所有可用的 Node.js 远程版本 nvm ls-remotenvm install v18.20.4# node --version v18.20.4# nvm current v18.20.4npm 是 …...
如何搭建题库管理小序❓
土著刷题小🍊序不仅能够作为组织考试的利器,它同样可以帮助教育培训机构构建一个强大且高效的题库管理系统。 下面跟随我们的指导,一起来看看如何利用土著刷题小🍊序轻松快捷地建立起自己的题库,并享受其所带来的诸多好…...
Spring Boot框架下校园社团信息管理的创新实践
2相关技术 2.1 MYSQL数据库 MySQL是一个真正的多用户、多线程SQL数据库服务器。 是基于SQL的客户/服务器模式的关系数据库管理系统,它的有点有有功能强大、使用简单、管理方便、安全可靠性高、运行速度快、多线程、跨平台性、完全网络化、稳定性等,非常…...
vscode clangd for cuda 插件配置
这里写目录标题 1. 下载插件clangd,并且安装server到host2. 配置3. 安装调试插件 1. 下载插件clangd,并且安装server到host 步骤 extension下载 altshiftp, 下服务,如果下不下来请考虑用🪜 下载好后check一下,检查是否正常 正常的标志 注意…...
软件测试学习笔记丨SeleniumPO模式
本文转自测试人社区,原文链接:https://ceshiren.com/t/topic/22525 本文为霍格沃兹测试开发学社的学习经历分享,写出来分享给大家,希望有志同道合的小伙伴可以一起交流技术,一起进步~ 说明:本篇博客基于sel…...
研发效能DevOps: Vite 使用 Vue Router
目录 一、实验 1.环境 2.初始化前端项目 3.安装vue-router 4.Vite 使用 Vue Router 二、问题 1.运行出现空页面 2.Vue Router如何禁止页面回退 一、实验 1.环境 (1)主机 表1 主机 系统 软件版本备注Windows11VS Code1.94.2Node.jsv18.20.4(LT…...
记第一次本地编译seatunnel源码
拉取代码 git clone https://github.com/apache/seatunnel.git 使用版本 我们生产环境用的是2.3.5版本,所以基于2.3.5-release分支代码进行编译。 maven package过程 遇到的第一个问题:‘com.sun.tools.javac.tree.JCTree com.sun.tools.javac.tree…...
《云主机配置全攻略》
《云主机配置全攻略》 一、云主机配置的重要性二、配置云主机的关键要素(一)CPU 的选择(二)内存的考量(三)硬盘的抉择(四)带宽的确定(五)机房线路的考虑&…...
RHCE nginx架构和安装
nginx架构和安装 nginx架构和安装1.1 nginx架构1.2 安装nginx1.1.1 本地安装1.1.2 官网安装1.1.3 源码安装 1.3 控制服务1.4 页面自定义 nginx架构和安装 nginx是多进程组织模式,而且是一个由 Master 主进程和 Worker 工作进程组成 1.1 nginx架构 1.2 安装nginx …...
Jmeter自动化实战
一、前言 由于系统业务流程很复杂,在不同的阶段需要不同的数据,且数据无法重复使用,每次造新的数据特别繁琐,故想着能不能使用jmeter一键造数据 二、创建录制模板 可参考:jmeter录制接口 首先创建一个录制模板 因为会有各种请求头,cookies,签名,认证信息等原因,导致手动复制…...
构建高效的Java SOCKS5代理:从零开始的网络转发实现
✅作者简介:2022年博客新星 第八。热爱国学的Java后端开发者,修心和技术同步精进。 🍎个人主页:Java Fans的博客 🍊个人信条:不迁怒,不贰过。小知识,大智慧。 💞当前专栏…...
spring-boot(绑定配置文件及应用)
配置文件 SpringBoot使用一个全局的配置文件,配置文件名是固定的; application.properties application.yml 配置文件的作用:修改SpringBoot自动配置的默认值;SpringBoot在底层都给我们自动配置好; YAML&#x…...
Mac OS 搭建MySQL开发环境
Mac OS 搭建MySQL开发环境 文章目录 Mac OS 搭建MySQL开发环境一、安装Mysql:二、配置环境变量三、安装Navicat 本地环境: Mac OS Sequoia15.0.1(M3 Max) 目标状态: 下载安装Mysql,配置相关环境。 一、安装Mysql&…...
windows下安装python库wordCloud报错
换电脑安装wordcloud半天安装失败,记录一下遇到的坑,也给大家节省点时间。 方法1: 错误呢就是下面这个,说没c编译器,要不就去他给的地址上安装一下,我安装了一下好像没什么用,也没太敢勾选&am…...
Spring IOC 自动装配(注入)
注解⽅式注⼊ Bean 对于 bean 的注⼊,除了使⽤ xml 配置以外,可以使⽤注解配置。注解的配置,可以简化配置⽂件, 提⾼开发的速度,使程序看上去更简洁。对于注解的解释,Spring对于注解有专⻔的解释器&#…...
Go使用SIMD指令——以string转为整数为例
本文Go使用SIMD指令采用如下方式: C编写对应的程序clang编译成汇编c2goasm将上述生成的汇编转为go的汇编 准备工具 clang。直接使用apt-get install clang安装即可c2goasm。 go get -u github.com/minio/c2goasm来进行安装asm2plan9s。 go get -u github.com/min…...
分享资源合集
为了方便临时使用到的一些软件,提供百度网盘下载。 通过百度网盘分享的文件:WinHex 21.2 SR-2_x86_x64.exe 链接:https://pan.baidu.com/s/19RAnHl_VcKUcIKADU9z9Gw?pwd6666 提取码:6666 通过百度网盘分享的文件:Zi…...
C#/WinForm 鼠标穿透自定义区域截图(后续实现录屏)
效果 窗体截图录屏 git地址:https://gitee.com/feng-cai/screenshot-recording...
基于SpringBoot的“社区维修平台”的设计与实现(源码+数据库+文档+PPT)
基于SpringBoot的“社区维修平台”的设计与实现(源码数据库文档PPT) 开发语言:Java 数据库:MySQL 技术:SpringBoot 工具:IDEA/Ecilpse、Navicat、Maven 系统展示 管理员登录页面 住户管理页面 社区公关管理页面 维…...
图书管理系统汇报
【1A536】图书管理系统汇报 项目介绍1.用户登录注册功能1. 1用户角色管理2.图书管理功能2.1 添加图书2.2 编辑图书2.3 删除图书 3.图书搜索和筛选3.1 图书搜索3.2 图书筛选 4.图书借阅、图书归还4.1 图书借阅4.2 图书归还 5.用户信息管理5.1上传头像5.2修改头像5.3 修改密码 项…...
做网站收录真的假的/百度极速版客服人工在线咨询
MHA高可用架构用一个管理节点监控后端数据库主库可用性提供VIP漂移接口,不提供具体方法提供补全从库日志的脚本MHA监控主库,提供自动主从切换;提供VIP漂移接口提供补全从库日志的脚本MHA的安装步骤规划配置服务器间域名和ssh互信访问在manage…...
珊瑚绒毯移动网站建设/云南网络营销公司哪家好
已知aview 1.3 需要依赖 aalib 1.4及以上版本 其它版本兼容需要自测 一、下载 aalib 和 aview 网上教程的地址很多都是旧的 作者可能迁移过或其它原因 总之 我们要自己进去网站里面观察一下 https://sourceforge.net/projects/aa-project/files 右键复制链接就能看到下载地…...
wordpress seo by yoast 设置/电商平台运营
给GridView中的buttonField添加一个删除确认功能 问题:GridView的第一列是ButtonField,字段名是"删除",想一点之后弹出确认框,否则返回.应该如何写?解决方法: 1、点击GridView的快捷箭头,选‘编辑列’。 2、加入一个ButtonFiled࿰…...
wordpress 购买主题/微信小程序建站
官方文档 qt只有在数据从系统io缓冲区到达Qt应用程序才能感知到有数据的到来,需要注意的是文档说的only once,也就是说当数据刚到达QTcpSocket时会提醒一次,而如果这时候还没有执行readyread信号,而这时候又来了很多次数据&#…...
做网站优化需要做什么/seo网站外链工具
下面我们配置serivce层到项目中 1.service包中创建ItemsService.java接口,和service.imp包中创建一个service实现类ItemsServiceImpl.java package cn.my.ssm.serive;import java.util.List;import cn.my.ssm.po.Items;public interface ItemsService {List<Item…...
官方网站建设/怎样做网站卖自己的产品
1、write与wall 命令write英文原意write所在路径/usr/bin/write执行权限所有用户功能描述给用户发消息,以CtrlD保存结束语法write [用户]范例write zhenghaojie 命令wall英文原意write all所在路径/usr/bin/wall执行权限所有用户功能描述广播信息语法wall [信息]范…...