Android源码分析 - InputManagerService与触摸事件
0. 前言
有人问到:“通过TouchEvent,你可以获得到当前的触点,它更新的频率和屏幕刷新的频率一样吗?”。听到这个问题的时候我感到很诧异,我们知道Android是事件驱动机制的设计,可以从多种服务中通过IPC通信获取通知,许多功能并不能混为一谈。
所以我今天想跟踪一下Input系统是怎么一个逻辑。结合多个优秀博客与源码跟踪分析,记录分享。
本文基于 Android 8.0 源码,高版本可能会有不同,仅供参考。本文的图片均来自参考博客,他们已经画的很完善了。
Window相关的知识点欢迎参考:
Android从屏幕刷新到View的绘制(一)之 Window、WindowManager和WindowManagerService之间的关系
1. InputChannel输入事件的消息通道——Socket
1.1 Window与Input输入事件的关系
我们猜想也知道,一个输入系统想要分发触摸事件,首先就要知道触摸在哪个Window上,而APP进程中与Window紧密关联的就是ViewRootImpl与WindowManagerGlobal。果然我们发现,在ViewRootImpl的setView()中有输入事件的注册,我们来看看:
//ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {//1.实例化一个InputChannelif ((mWindowAttributes.inputFeatures& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {//实例化一个InputChannelmInputChannel = new InputChannel();}//2.向WMS添加window,同时交出自己的InputChannelres = mWindowSession.addToDisplay(...,mInputChanel);//向WMS添加window//...//3.创建WindowInputEventReceiver接受Input事件if (mInputChannel != null) {if (mInputQueueCallback != null) {mInputQueue = new InputQueue();mInputQueueCallback.onInputQueueCreated(mInputQueue);}//创建了一个WindowInputEventReceiver对象,看名字就才到这是输入事件的监听者,我们着重关注它是如何监听输入事件的。mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,Looper.myLooper());}}
正如我们分析所知,输入事件与Window是紧密联系的,我们接下来做两件事:
- 添加window的时候,为Input系统做了哪些铺垫?
- 注册了Input事件的监听者,它如何接受Input事件的?
解决第一个问题,我们先来看向WMS添加windows的方法 mWindowSession.addToDisplay(),它最终回来到WMS的addWindow():
//WindowManagerService
public int addWindow(Session session,IWindow client,...,InputChannel outInputChannel) {//...//1. 创建WindowState与Window一一对应,进行管理WIndowState win = new WindowState(this,session,client,...);//...//2. 开启输入通道final boolean openInputChannels = (outInputChannel != null&& (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);if (openInputChannels) {win.openInputChannel(outInputChannel);}}
接下来就进入到WindowState.openInputChannel(outInputChannel):
//WindowState
// Input channel and input window handle used by the input dispatcher.
//没错,这里的handle就是binder通信中的handle引用
final InputWindowHandle mInputWindowHandle;
InputChannel mInputChannel;
private InputChannel mClientChannel;void openInputChannel(InputChannel outInputChannel){//1. 开启一个channelInputChannel[] inputChannels = InputChannel.openInputChannelPair(name);//2.返回channel引用//服务端的socketmInputChannel = inputChannels[0];//客户端的socketmClientChannel = inputChannels[1];//将服务端的socket引用交给客户端mInputWindowHandle.inputChannel = inputChannels[0];//3. 让WMS中的InputManager注册这个InputChannelmService.mInputManager.registerInputChannel(mInputChannel, mInputWindowHandle);
}
这是在WindowState类中的操作,mInputWindowHandle是对与WindowState对应的Window的handle引用,WindowState又持有WMS,可以告知WMS让IMS注册这个InputChannel输入通道。其实到这里,我们按InputChannel的名字就能猜测到,这里的IPC通信用的不是Binder,而是其他的,如管道或者socket,我们再往后看看。
InputManagerService与WMS、AMS类似的,都是系统服务,都将自己的binder引用交给了ServiceManager的svclist中。服务中相互调用方法也是常见的。这里WMS持有IMS的引用,调用了它的registerInputChannel方法。这个引用,在SystemServer启动服务,实例化WMS的时候就传入了,我们简单看一下就略过:
//SystemServer
private void startOtherServices(){inputManager = new InputManagerService(context);//这里将IMS交给了WMSwm = WindowManagerService.main(context, inputManager,mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL,!mFirstBoot, mOnlyCore, new PhoneWindowManager());//向ServiceManager注册服务ServiceManager.addService(Context.WINDOW_SERVICE, wm);ServiceManager.addService(Context.INPUT_SERVICE, inputManager);
}//InputManagerService
public InputManagerService(Context context) {this.mContext = context;//也是有一个looper在跑this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper());//这里需要注意,看后文解释LocalServices.addService(InputManagerInternal.class, new LocalService());
}
LocalService这个类和ServiceManager的功能类似,可以用于注册服务与获取服务,只不过这里注册的服务不是Binder实体,只能在同一个进程中使用(不用跨进程通信,例如WMS与IMS都在SystemServer进程之下)。顾名思义,LocalService为本地服务,与远程服务的区别就是,远程服务在不同的进程,例如AMS对于APP进程来说就是远程服务,而WMS对于AMS来说就是本地服务,都在SystemServer进程之下。但它们又都开了Binder线程池,所以也同时都注册到了ServiceManager的svclist之中。这里就点到为止,本文主要目的还是探讨输入事件。
1.2 InputChannel的创建与注册
我们接下来关注两个点:
- InputChannel是如何打开的?
- 如何向InputManagerService注册这个inputChannel
先来看到InputChannel:
//InputChannel
public static InputChannel[] openInputChannelPair(String name) {//走了native方法return nativeOpenInputChannelPair(name);
}
来到native层:
//android_view_InputChannel
static jobjectArray android_view_InputChannel_nativeOpenInputChannelPair(JNIEnv* env,jclass clazz, jstring nameObj) {//1. 服务端channel和客户端channelsp<InputChannel> serverChannel;sp<InputChannel> clientChannel;//2.openInputChannelPair()来开启两个channelstatus_t result = InputChannel::openInputChannelPair(name, serverChannel, clientChannel);//...jobjectArray channelPair = env->NewObjectArray(2, gInputChannelClassInfo.clazz, NULL);//...//将两个channel放到channelPair中//...return channelPair;
}
来到InputTransport.cpp:
//InputTransport.cpp
status_t InputChannel::openInputChannelPair(const String8& name,sp<InputChannel>& outServerChannel, sp<InputChannel>& outClientChannel) {int sockets[2];//从名字就可以看出来,使用的是socket了if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) {//errorreturn result;}//2.处理两个socketint bufferSize = SOCKET_BUFFER_SIZE;setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));setsockopt(sockets[0], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));//给两个socket命名,放到InputChannel之中String8 serverChannelName = name;serverChannelName.append(" (server)");//实例化一个InputChannel引用回传outServerChannel = new InputChannel(serverChannelName, sockets[0]);String8 clientChannelName = name;clientChannelName.append(" (client)");outClientChannel = new InputChannel(clientChannelName, sockets[1]);return OK;
}
跟到这里我们发现,这个InputChannel使用的是socket,构建InputChannel对象时,传入的第一个参数是name,第二个参数是fd,文件描述符。源码注释是这样解释的:InputChannel输入通道是由本地Unix的socket组成的,用于跨进程发送、接收消息。每个通道都有一个用于调试的描述性名称,也就是第一个参数name。
这下就清楚了,创建InputChannel,就是给客户端、服务端都开启了一个socket,用于进行IPC通信。换句话说,openInputChannelPair生成了两个socket的fd(文件描述符),代表一个双向通道的两端。初始化了两端的包括Native层和Java层的InputChannel对象,native层的InputChannel封装了name和fd。
解决完InputChannel是如何打开的问题后,我们来看IMS如何注册这个InputChannel的:
来到InputManagerService的registerInputChannel():
//InputManagerService
public void registerInputChannel(InputChannel inputChannel,InputWindowHandle inputWindowHandle) {nativeRegisterInputChannel(mPtr, inputChannel, inputWindowHandle, false);
}
private static native void nativeRegisterInputChannel(long ptr, InputChannel inputChannel,InputWindowHandle inputWindowHandle, boolean monitor);
它调用到了native方法:
//com_android_server_input_InputManagerService.cpp
static void nativeRegisterInputChannel(JNIEnv* env, jclass /* clazz */,jlong ptr, jobject inputChannelObj, jobject inputWindowHandleObj, jboolean monitor) {//1. NativeInputManagerNativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);//2. 传入参数可以看到//inputChannelObj是服务端channel//inputWindowHandleObj是客户端channel//...//3. 来到NativeInputManager的registerInputChannelstatus_t status = im->registerInputChannel(env, inputChannel, inputWindowHandle, monitor);//...
}
注册InputChannel输入通道的任务又交给了native层来做,来到乐NativeInputManagerService.registerInputChannel():
//com_android_server_input_InputManagerService.cpp
status_t NativeInputManager::registerInputChannel(JNIEnv* /* env */,const sp<InputChannel>& inputChannel,const sp<InputWindowHandle>& inputWindowHandle, bool monitor) {return mInputManager->getDispatcher()->registerInputChannel(inputChannel, inputWindowHandle, monitor);
}
其中mInputManager是在初始化NativeInputManager的时候初始化的:
//com_android_server_input_InputManagerService.cpp
NativeInputManager::NativeInputManager(jobject contextObj,jobject serviceObj, const sp<Looper>& looper) :
mLooper(looper), mInteractive(true) {JNIEnv* env = jniEnv();//...sp<EventHub> eventHub = new EventHub();mInputManager = new InputManager(eventHub, this, this);
}
这里的EventHub叫做事件集线器,时间总线。顾名思义可以获取到各种事件。我们后面再讨论,先来看一下mInputManager->getDispatcher()->registerInputChannel()
//InputDispatcher.cpp
status_t InputDispatcher::registerInputChannel(const sp<InputChannel>& inputChannel,const sp<InputWindowHandle>& inputWindowHandle, bool monitor) {// acquire lockAutoMutex _l(mLock);//1.判断,这个连接connection是否已经建立过了if (getConnectionIndexLocked(inputChannel) >= 0) {ALOGW("Attempted to register already registered input channel '%s'",inputChannel->getName().string());return BAD_VALUE;}//2. 建立连接sp<Connection> connection = new Connection(inputChannel, inputWindowHandle, monitor);//3. 服务端的socket的fdint fd = inputChannel->getFd();//将这个connection保存在mConnectionsByFd中,索引为socket的fdmConnectionsByFd.add(fd, connection);//分发器的looper中添加这个fdmLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, handleReceiveCallback, this);//唤醒looper,因为连接状态变化了mLooper->wake();return OK;
}
一个connection对象被创建出来,这个connection表示客户端和服务端之间的一个连接通道,用于传输数据。每个Connection都对应一个服务端的InputChannel,通过这个InputChannel的fd索引,InputDispatcher将所有的connection都保存在mConnectionsByFd中。再将这个fd注册到Looper的监控列表里,这样一旦对socket写入数据,Looper就会被唤醒,接着就会调用回调函数的handleReceiveCallback。由于一个屏幕上可能会有多个Window正在显示,所以一个Dispatcher可能会有多个Connection同时存在。
至此,InputChannel被创建出来,InputChannel两端的socket的fd分别被注册在了所在的Looper中。最后模型大致如下:
2. InputManagerService 服务端从设备中捕获输入事件并派发
我们看完了InputManagerService到APP进程的通信方式,接下来就需要探索:
- 如何捕获事件
- 如何派发事件
回到在SystemServer中启动InputManagerService的时候,我们提到了一个EventHub但是没有深入探究么?它主要利用Linux的inotify和epoll机制,监听设备事件,例如设备插拔、各种触摸、物理按钮等事件。
顾名思义,EventHub就是一个不同设备事件的集线器,它主要面向的是 /dev/input目录下的设备节点,比如 /dev/input/event0 上的事件就是输入事件:
//com_android_server_input_InputManagerService.cpp
NativeInputManager::NativeInputManager(jobject contextObj,jobject serviceObj, const sp<Looper>& looper) :
mLooper(looper), mInteractive(true) {JNIEnv* env = jniEnv();//...sp<EventHub> eventHub = new EventHub();mInputManager = new InputManager(eventHub, this, this);
}
初始化InputManager时,将eventHub作为参数传入:
//InputManager.cpp
InputManager::InputManager(const sp<EventHubInterface>& eventHub,const sp<InputReaderPolicyInterface>& readerPolicy,const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {//1. InputDispatcher在这里初始化的mDispatcher = new InputDispatcher(dispatcherPolicy);//2. 同时还初始化了一个InputReader,它用于事件读取!mReader = new InputReader(eventHub, readerPolicy, mDispatcher);initialize();
}//initialize()开启了两个线程,一个接受输入的线程,一个派发线程
void InputManager::initialize() {mReaderThread = new InputReaderThread(mReader);mDispatcherThread = new InputDispatcherThread(mDispatcher);
}
源码的注释给了我们InputReaderThread和InputDispatcherThread的定义:
- InputReaderThread:用于读取并预处理原始输入事件,最后发送消息到dispatcherThread中
- InputDispatcherThread:阻塞等待新的事件到来,并异步地将他们派发给APP进程。两者之间只有单向通知,即从InputReaderThread将消息发送给InputDispatcherThread,仅此单程。
InputReader:
//InputReader.cpp
bool InputReaderThread::threadLoop() {//开启循环mReader->loopOnce();return true;
}void InputReader::loopOnce() {//...//1.从eventhub中获取设备传来的输入事件size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);if (count) {//2. 如果读取到了事件,处理该事件processEventsLocked(mEventBuffer, count);}if (inputDevicesChanged) {//输入域设备变化了mPolicy->notifyInputDevicesChanged(inputDevices);}//3. 通知派发mQueuedListener->flush();
}
从eventhub中读取事件是阻塞的,在eventhub.getEvents()中:
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {//...RawEvent* event = buffer;//1.从设备中读取数据到readBuffer[]//循环读取,从不同的设备device中read()数据到readBuffer[]中Device* device = mDevices.valueAt(deviceIndex);read(device->fd,readBuffer,sizeof(struct input_event)*capacity);//2.将readbuffer中的输入事件input_event放到buffer中,回传回去//for循环,从readbuffer中拿到iev,填入event,也就是返回的buffer中struct input_event& iev = readBuffer[i];event->deviceId = deviceId;event->type = iev.type;event->code = iev.code;event->value = iev.value;event += 1;//...return event-buffer;//返回count,数量。实际数据已经在buffer中了
}
这就简单了,IMS通过其InputReader阻塞地从设备中读取输入信息,并进行数据处理,然后进行事件派发。
我们知道InputReader通过EventHub阻塞从设备中读取输入信息,接下来就进行数据处理:
//InputReader.cpp
void InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) {//传入的是之前EventHub从设备中读来的原始输入数据,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;while (batchSize < count) {if (rawEvent[batchSize].type >= EventHubInterface::FIRST_SYNTHETIC_EVENT|| rawEvent[batchSize].deviceId != deviceId) {break;}batchSize += 1;}//批处理设备信息processEventsForDeviceLocked(deviceId, rawEvent, batchSize);} else {//物理设备变化的处理switch (rawEvent->type) {case EventHubInterface::DEVICE_ADDED:addDeviceLocked(rawEvent->when, rawEvent->deviceId);break;case EventHubInterface::DEVICE_REMOVED:removeDeviceLocked(rawEvent->when, rawEvent->deviceId);break;case EventHubInterface::FINISHED_DEVICE_SCAN:handleConfigurationChangedLocked(rawEvent->when);break;default:ALOG_ASSERT(false); // can't happenbreak;}}count -= batchSize;rawEvent += batchSize;}
}
可以看到InputManager管理的不仅仅是触摸输入,各种输入设备的信息他都可以获取到。我们不细看如何处理消息的了,直接到派发消息的过程。
InputReader读取到数据之后,通过mQueuedListener(这其实是InputDispatcher对象)来唤醒InputDispatcher来派发事件。InputDispatcherThread是一个Looper线程,基于native的Looper实现了Handler消息处理模型,如果有input事件到来就被唤醒,去处理事件,处理完毕后继续睡眠放弃CPU使用权,等待唤醒。我们来看看InputDispactherThread:
//InputDispatcher.cpp
bool InputDispatcherThread::threadLoop() {mDispatcher->dispatchOnce();return true;
}void InputDispatcher::dispatchOnce() {nsecs_t nextWakeupTime = LONG_LONG_MAX;{ // acquire lockAutoMutex _l(mLock);//被唤醒,来处理Input消息if (!haveCommandsLocked()) {dispatchOnceInnerLocked(&nextWakeupTime);}//...} // release locknsecs_t currentTime = now();int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);//进入睡眠,等待input事件。mLooper->pollOnce(timeoutMillis);
}
InputDispatcherThread的派发逻辑在dispatchOnceInnerLocked(),里面有很多事件分支,我们关注到TYPE_MOTION,也就是触摸事件分支:
//InputDispatcher.cpp
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {switch(mPendingEvent->type){case EventEntry::TYPE_CONFIGURATION_CHANGED:case ...:case EventEntry::TYPE_MOTION:{done = dispatchMotionLocked(currentTime, typedEntry,&dropReason, nextWakeupTime);break;}//...}
}
通过dispatchMotionLocked()来处理触摸事件:
//InputDispatcher.cpp
bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, MotionEntry* entry, DropReason* dropReason, nsecs_t* nextWakeupTime) {//...// Identify targets.Vector<InputTarget> inputTargets;bool conflictingPointerActions = false;int32_t injectionResult;if (isPointerEvent) {//1. 找到目标windowinjectionResult = findTouchedWindowTargetsLocked(currentTime,entry, inputTargets, nextWakeupTime, &conflictingPointerActions);} else {injectionResult = findFocusedWindowTargetsLocked(currentTime,entry, inputTargets, nextWakeupTime);}setInjectionResultLocked(entry, injectionResult);//2. 派发事件dispatchEventLocked(currentTime, entry, inputTargets);return true;
}
可以看到,触摸事件首先会通过 findTOuchedWindowTargetsLocked 找到目标 Window,然后再通过 dispatchEventLocked进行消息派发。这下就和WindowManangerService注册Window的功能挂钩了,可以通过判断触摸事件的位置以及窗口属性来确定将事件发送到哪个窗口Window。
我们来看到这个寻找窗口的方法findTOuchedWindowTargetsLocked():
//InputDispatcher.cpp
//如果是触摸事件,有pointer_event,进入这里派发
int32_t InputDispatcher::findTouchedWindowTargetsLocked(nsecs_t currentTime,const MotionEntry* entry, Vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime,bool* outConflictingPointerActions) {//...sp<InputWindowHandle> newTouchedWindowHandle;bool isTouchModal = false;// Traverse windows from front to back to find touched window and outside targets.//1. 遍历windows,找到目标size_t numWindows = mWindowHandles.size();for (size_t i = 0; i < numWindows; i++) {sp<InputWindowHandle> windowHandle = mWindowHandles.itemAt(i);const InputWindowInfo* windowInfo = windowHandle->getInfo();if (windowInfo->displayId != displayId) {continue; // wrong display}int32_t flags = windowInfo->layoutParamsFlags;if (windowInfo->visible) {if (! (flags & InputWindowInfo::FLAG_NOT_TOUCHABLE)) {isTouchModal = (flags & (InputWindowInfo::FLAG_NOT_FOCUSABLE| InputWindowInfo::FLAG_NOT_TOUCH_MODAL)) == 0;//2. 如果触摸的点在这个窗口上,记录一下if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) {newTouchedWindowHandle = windowHandle;break; // found touched window, exit window loop}}}}
}
mWindowHandles表示着所有窗口,根据点击位置与Z轴特性等进行具体确定。这个mWindowHandles是在WMS.addWindow()的时候,通过InputMonitor间接地调用InputDispatcher::setInputWindows来设置的。所以每次窗口变化,InputDispatcher都能获知最新的状态,找到窗口后,InputDispatcher::dispatchMotionLocked()最后通过dispatchEventLocked()进行了事件派发:
//InputDispatcher.cpp
void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,EventEntry* eventEntry, const Vector<InputTarget>& inputTargets) {pokeUserActivityLocked(eventEntry);for (size_t i = 0; i < inputTargets.size(); i++) {const InputTarget& inputTarget = inputTargets.itemAt(i);//拿到InputChannel连接ssize_t connectionIndex = getConnectionIndexLocked(inputTarget.inputChannel);if (connectionIndex >= 0) {//Connection中包含了双方的socket的fdsp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);//进入派发prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget);} }
}
最后,通过prepareDispatchCycleLocked()进入了消息入队处理:
//InputDispatcher.cpp
void InputDispatcher::prepareDispatchCycleLocked(nsecs_t currentTime,const sp<Connection>& connection, EventEntry* eventEntry, const InputTarget* inputTarget) {//...//把消息入队enqueueDispatchEntriesLocked(currentTime, connection, eventEntry, inputTarget);
}void InputDispatcher::enqueueDispatchEntriesLocked(nsecs_t currentTime,const sp<Connection>& connection, EventEntry* eventEntry, const InputTarget* inputTarget) {bool wasEmpty = connection->outboundQueue.isEmpty();// Enqueue dispatch entries for the requested modes.//根据不同的情况进行入队enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,InputTarget::FLAG_DISPATCH_AS_HOVER_EXIT);enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,InputTarget::FLAG_DISPATCH_AS_OUTSIDE);enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,InputTarget::FLAG_DISPATCH_AS_HOVER_ENTER);enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,InputTarget::FLAG_DISPATCH_AS_IS);enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,InputTarget::FLAG_DISPATCH_AS_SLIPPERY_EXIT);enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,InputTarget::FLAG_DISPATCH_AS_SLIPPERY_ENTER);// If the outbound queue was previously empty, start the dispatch cycle going.//全部事件都入队完成后,派发任务,通知更新if (wasEmpty && !connection->outboundQueue.isEmpty()) {startDispatchCycleLocked(currentTime, connection);}
}
消息入队:
//InputDispatcher.cpp
void InputDispatcher::enqueueDispatchEntryLocked(const sp<Connection>& connection, EventEntry* eventEntry, const InputTarget* inputTarget,int32_t dispatchMode) {//1. new 一个新的派发事件DispatchEntry* dispatchEntry = new DispatchEntry(eventEntry, // increments refinputTargetFlags, inputTarget->xOffset, inputTarget->yOffset,inputTarget->scaleFactor);//2.根据event的不同进行处理//最后入队connection->outboudQueue.enqueueAtTail(dispatchEntry);
}
所有消息入队之后,InputDispatcher通过startDispatchCycleLocked()启动connection的事件发送,将队列中的任务逐个发送:
//InputDispatcher.cpp
void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,const sp<Connection>& connection) {//如果是按键:status = connection->inputPublisher.publishKeyEvent();//如果是触摸status = connection->inputPublisher.publishMotionEvent();
}
InputDispatcher->Connection->inputPublisher->InputChannel.sendMessage()->socket.send()
接下来就到了我们之前跟踪到的InputChannel的socket通信了。输入事件从服务端,通过socket发送给了客户端。需要注意的是,APP进程也可能会有多个Socket,因为一个Activity或者Dialog等都会拥有一个Window,而这个Window最终都会通过WMS进行注册,同时在WMS中生成一个WindowState,以及用于输入事件传递的InputChannel。一个APP进程会有多个Window,自然就会有多个InputChannel。每个InputChannel为一个Window服务。
3. 客户端Window的InputChannel接收从服务端传来的输入事件
3.1 注册输入事件的监听器
我们回顾到ViewRootImpl的setView()中,除了让WMS.addToDisplay()添加window并建立inputchannel,接着还注册了一个WindowInputEventReceiver:
//ViewRootImpl
public void setView(...){res = mWindowSession.addToDisplay();if (mInputChannel != null) {if (mInputQueueCallback != null) {mInputQueue = new InputQueue();mInputQueueCallback.onInputQueueCreated(mInputQueue);}//建立了一个输入事件监听器。mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,Looper.myLooper());}
}
我们看到这个WindowInputEventReceiver的初始化,它的父类是InputEventReceiver:
//InputEventReceiver
public InputEventReceiver(InputChannel inputChannel, Looper looper) {//传入的是这个window添加时创立好的InputChannelmInputChannel = inputChannel;mMessageQueue = looper.getQueue();//调用了native方法mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),inputChannel, mMessageQueue);mCloseGuard.open("dispose");
}private static native long nativeInit(WeakReference<InputEventReceiver> receiver,InputChannel inputChannel, MessageQueue messageQueue);
在这里,我们就可以注意到,将APP进程主线程的Looper的消息队列MessageQueue的引用往下传递了,可以猜到,后续有消息,会加入到MessageQueue中去。到了native层进行初始化:
//android_view_InputEventReceiver.cpp
static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak,jobject inputChannelObj, jobject messageQueueObj) {sp<NativeInputEventReceiver> receiver = new NativeInputEventReceiver(env, receiverWeak, inputChannel, messageQueue);//1. NativeInputEventReceiver::initialize();status_t status = receiver->initialize();return reinterpret_cast<jlong>(receiver.get());
}status_t NativeInputEventReceiver::initialize() {//2. setFdEvent()setFdEvents(ALOOPER_EVENT_INPUT);return OK;
}void NativeInputEventReceiver::setFdEvents(int events) {if (mFdEvents != events) {mFdEvents = events;int fd = mInputConsumer.getChannel()->getFd();if (events) {//将socket客户端的fd添加到主线程的消息池mMessageQueue->getLooper()->addFd(fd, 0, events, this, NULL);} else {mMessageQueue->getLooper()->removeFd(fd);}}
}
我们还记得一开始添加window的时候创建了客户端和服务端的InputChannel。他们都是在system_server进程中创建的。socket服务端fd保存到了WMS的WindowState的mInputChannel。而socket客户端fd则通过binder通信,传回给了APP进程的ViewRootImpl的mInputChannel。两端都通过格子的Looper监听对端的写操作,一旦对端写入数据,我端收到数据,马上回调响应。例如屏幕有输入,通过socket传到了APP进程,立刻将消息发送到主线程Looper,进行后续的InputEvent分发。
3.2 回调处理InputEvent
关于回调:
-
服务端socket收到客户端的消息后,回调的是inputDispatcher.handleReceiveCallback()
-
客户端socket收到服务端的消息后,回调的是NativeInputEventReceiver.handleEvent()
我们主要关注APP进程如何接受消息。首先在注册监听器的时候,native层通过mMessageQueue->getLooper()->addFd()添加了socket的fd,这部分做了什么呢?如何让looper可以接收到socket消息呢?我们看到Looper的addFd()方法,这是native层的:
//Looper.cpp
int Looper::addFd(int fd, int ident, int events, Looper_callbackFunc callback, void* data) {return addFd(fd, ident, events, callback ? new SimpleLooperCallback(callback) : NULL, data);
}int Looper::addFd(int fd, int ident, int events, const sp<LooperCallback>& callback, void* data) {// acquire lockAutoMutex _l(mLock);Request request;request.fd = fd;request.ident = ident;request.events = events;request.seq = mNextRequestSeq++;request.callback = callback;request.data = data;if (mNextRequestSeq == -1) mNextRequestSeq = 0; // reserve sequence number -1struct epoll_event eventItem;request.initEventItem(&eventItem);//epoll机制int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, fd, & eventItem);//...return 1;
}
当接收到消息之后,会通过LooperCallback回调。Looper根据fd来找到对应的监听器,一个APP进程可能有多个Window,就会有多个InputEventReceiver,所以需要查找。最后调用到其handleEvent来处理对应事件:
//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) {//...}return 1;
}
consumeEvents()中进行消费事件,就是进一步读取事件,封装成java层的对象,传递给java层,进行相应的回调处理:
//android_view_InputEventReceiver.cpp
status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {//...for (;;) {uint32_t seq;InputEvent* inputEvent;//1. 获取事件status_t status = mInputConsumer.consume(&mInputEventFactory,consumeBatches, frameTime, &seq, &inputEvent, &displayId);//...//根据事件类型,包装在switch (inputEvent->getType()) {case AINPUT_EVENT_TYPE_KEY://如果是按键类型:inputEventObj = android_view_KeyEvent_fromNative(env,static_cast<KeyEvent*>(inputEvent));break;case AINPUT_EVENT_TYPE_MOTION: {//如果是触摸类型MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);break;}//...}//回调处理,通过反射来调用Java层的方法,进行回调if (inputEventObj) {env->CallVoidMethod(receiverObj.get(),gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj,displayId);env->DeleteLocalRef(inputEventObj);}}
}
最后,触摸事件也就被封装成了InputEvent,通过反射回调到InputEventReceiver的dispatchInputEvent进行处理。最后,也就通过ViewPostImeInputState来到了View,进行触摸事件分发。
4. 总结
4.1 触摸事件分发流程
我们知道InputManagerService不仅能捕获触摸InputEvent,还可以捕获各种输入设备的事件,我们这里以触摸事件为例,梳理流程
- 点击屏幕
- InputManagerService的InputReader线程通过EventHub捕获输入事件
- 经过处理后将消息发给InputDispatcher线程
- InputDispatcher找到触摸的目标窗口,通过InputChannel的 socket将事件发送给客户端的主线程Looper
- 主线程Looper通过socket的fd,找到对应的窗口,将事件交给这个window
- 具体window处理具体事件。例如Activity中的某个Button被按下了。
4.2 Socket 与 binder 的区别
我认为有几点:
- 假设使用binder通信,一个APP进程有多个window,为每个window都开启一个binder线程进行监听,binder的内存映射空间可能很快就会到达4M,使得其他必须要binder通信的功能受到影响
- binder虽然是“一次拷贝”但还是在内核态、用户态切换了多次,binder的优势在于减少了一次数据拷贝.但触摸事件传递的数据很小,并不需要考虑对减少数据拷贝次数的优化,而且减少了切换内核态的次数,更高效。
- Socket可以实现异步通知,只需要客户端服务端各一个线程即可,是全双工,而binder是单工通信,如果要实现异步,服务端和客户端各要两个线程。如此看来,为了满足全双工异步通知,socket需要的线程明显少于binder,所以socket更为高效.
- 此外,socket基于tcp,还保证了消息的有序性、可靠性。
- 像AMS这种与APP进程就用的是binder通信,因为中间可能要通过Intent来传递parcel数据,这个数据虽然不超过1M,但相对于触摸消息来说已经非常大了,如果也用socket,将会进行两次的数据拷贝,所以它选择用binder通信,通过内存映射来完成只需要一次的”数据拷贝“
5. 参考文章
-
十分钟了解Android触摸事件原理(InputManagerService)
-
Android Input(五)-InputChannel通信
-
Android Input子系统为什么要使用socket,而不是binder?
相关文章:
Android源码分析 - InputManagerService与触摸事件
0. 前言 有人问到:“通过TouchEvent,你可以获得到当前的触点,它更新的频率和屏幕刷新的频率一样吗?”。听到这个问题的时候我感到很诧异,我们知道Android是事件驱动机制的设计,可以从多种服务中通过IPC通信…...
python库--urllib
目录 一.urllib导入 二.urllib爬取网页 三.Headers属性 1.使用build_opener()修改报头 2.使用add_header()添加报头 四.超时设置 五.get和post请求 1.get请求 2.post请求 urllib库和request库作用差不多,但比较起来request库更加容易上手,但该了…...
美团前端二面常考react面试题及答案
什么原因会促使你脱离 create-react-app 的依赖 当你想去配置 webpack 或 babel presets。 React 16中新生命周期有哪些 关于 React16 开始应用的新生命周期: 可以看出,React16 自上而下地对生命周期做了另一种维度的解读: Render 阶段&a…...
环境搭建04-Ubuntu16.04更改conda,pip的镜像源
我常用的pipy国内镜像源: https://pypi.tuna.tsinghua.edu.cn/simple # 清华 http://mirrors.aliyun.com/pypi/simple/ # 阿里云 https://pypi.mirrors.ustc.edu.cn/simple/ #中国科技大学1、将conda的镜像源修改为国内的镜像源 先查看conda安装的信息…...
【C++进阶】四、STL---set和map的介绍和使用
目录 一、关联式容器 二、键值对 三、树形结构的关联式容器 四、set的介绍及使用 4.1 set的介绍 4.2 set的使用 五、multiset的介绍及使用 六、map的介绍和使用 6.1 map的介绍 6.2 map的使用 七、multimap的介绍和使用 一、关联式容器 前面已经接触过 STL 中的部分…...
JavaSE学习进阶 day1_01 static关键字和静态代码块的使用
好的现在我们进入进阶部分的学习,看一张版图: 前面我们已经学习完基础班的内容了,现在我们已经来到了第二板块——基础进阶,这部分内容就不是那么容易了。学完第二板块,慢慢就在向java程序员靠拢了。 面向对象进阶部分…...
苹果笔可以不买原装吗?开学必备性价比电容笔
在当今的时代,电容笔日益普及,而且相关的功能也逐渐完善。因此,在使用过程中,怎样挑选一款性价比比较高的电容笔成为大家关心的焦点。随着电容笔的普及,更好更便宜的电容笔成为了一种趋势。那么,哪个品牌的…...
数据库连接与properties文件
管理properties数据库: 现在pom文件中加入Druid的坐标: <dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.8</version></dependency>配置文件中添加相应的数据&…...
Linux上的校验和验证
校验和(checksum)程序用来从文件中生成相对较小的唯一密钥。我们可以重新计算该密钥,用以检查文件是否发生改变。修改文件可能是有意为之(添加新用户会改变密码文件),也可能是无意而为(从CD-ROM…...
杂记——14.git在idea上的使用及其实际开发介绍
这篇文章我们来讲一下git在idea上的使用,以及在实际开发过程中各个分支的使用及其具体的流程 目录 1.git在idea上的使用 1.1 idea上的git提交 1.2 idea上的分支切换 2.git在实际运用时的分支及其流程 2.1分支介绍 2.2具体流程 3.小结 1.git在idea上的使用 …...
记一次Nodejs减低npm版本的踩坑日记
使用了npm install -g npm6.4.1指令之后,把npm版本减低了,让后悲催的就来了。 由于npm 6.4.1 已经过时,导致运行npm时出现 npm does not support Node.js v18.14.2 版本不兼容问题 升级npm版本,npm install -g npmlatest 没用还是…...
【iOS】—— 初识RAC响应式编程
RAC(ReactiveCocoa) 文章目录RAC(ReactiveCocoa)响应式编程和函数式编程的区别函数式编程响应式编程响应式编程的优点RAC操作1.利用button点击实现点击事件和传值2.RACSignal用法RACSignal总结:3.对于label的TapGestur…...
Java——面向对象
目录 前言 一、什么是面向对象? 面向过程 & 面向对象 面向对象 二、回顾方法的定义和调用 方法的定义 方法的调用 三、类与对象的创建 类和对象的关系 创建与初始化对象 四、构造器详解 五、创建对象内存分析 六、封装详解 七、什么是继承&#x…...
电影《毒舌律师》观后感
上周看了《毒蛇律师》这部电影,讲述一位’大律师’在法庭为己方辩护,最终赢得辩护的故事。 (1)人之常情 说起法律相关,不禁会让人联想到讲法律相关知识的罗翔老师,平时也会看他相关视频,无论是亲…...
【活学活用掌握trap命令】
trap 命令用于指定在接收到信号后将要采取的动作,常见的用途是在脚本程序被中断时完成清理工作。当 shell 接收到 sigspec 指定的信号时, arg 参数(通常是执行命令)会被读取,并被执行。 1. 命令介绍 开始掌握基本的使用方式和方法 [1] 语法…...
计算机组成原理4小时速成6:输入输出系统,io设备与cpu的链接方式,控制方式,io设备,io接口,并行串行总线
计算机组成原理4小时速成6:输入输出系统,io设备与cpu的链接方式,控制方式,io设备,io接口,并行串行总线 2022找工作是学历、能力和运气的超强结合体,遇到寒冬,大厂不招人,…...
MyBatis源码分析(三)SqlSession的执行主流程
文章目录一、熟悉主要接口二、SqlSession的获取1、通过数据源获取SqlSession三、Mapper的获取与代理1、从SqlSession获取Mapper2、执行Mapper方法前准备逻辑3、SqlCommand的创建4、构造MethodSignature四、执行Mapper的核心方法1、执行Mapper的方法逻辑五、简单SELECT处理过程1…...
环境搭建01-Ubuntu16.04如何查看显卡信息及安装NVDIA显卡驱动
1. 查看显卡型号、驱动 ubuntu-drivers devices2. 安装NVIDIA显卡驱动 (1)验证是否禁用nouveau lsmod | grep nouveau若有输出,没有禁用,进行以下操作禁用。 sudo gedit /etc/modprobe.d/blacklist.conf在文件末尾中添加两条&…...
内网渗透测试理论学习之第四篇内网渗透域的横向移动
文章目录一、IPC二、HashDump三、PTH四、PTT五、PsExec六、WMI七、DCOM八、SPN九、Exchange在内网中,从一台主机移动到另外一台主机,可以采取的方式通常有文件共享、计划任务、远程连接工具、客户端等。 一、IPC IPC(Internet Process Conn…...
20 | k8s v1.20集群搭建master和node
1 单节点master 1.1 服务器整体规划 1.2 单Master架构图 1.3 初始化配置 1.3.1 关闭防火墙 systemctl stop firewalld systemctl disable firewalld1.3.2 关闭selinux sed -i s/enforcing/disabled/ /etc/selinux/config # 永久 setenforce 0 # 临时 1.3.3 关闭swap …...
《商用密码应用与安全性评估》第一章密码基础知识1.1应用概念
密码的概念与作用 概念 密码:采用特定变换的方法对信息进行加密保护、安全认证的技术、产品和服务。 密码技术:密码编码、实现、协议、安全防护、分析破译、以及密钥产生、分发、传递、使 用、销毁等技术。 密码技术核心:密码算法…...
【博学谷学习记录】超强总结,用心分享丨人工智能 深度学习 神经网络基础知识点总结
目录神经网络激活函数引入激活函数原因:sigmoid激活函数tanh 激活函数ReLU 激活函数(最常用)SoftMax如何选择反向传播参数初始化方法优化方法正则化批量归一层网络模型调优的思路神经网络 简单的神经网络包括三层:输入层…...
Python+tkinter添加滚动条
大家好,我是IKUN的真爱粉,有时候我们需要在tkinter上加滚动条,那么怎么制作呢,我们先看下面的视频展示效果,是不是你想要的 展示 感觉制作的略微粗糙,各位可以后期自己慢慢调整 创建滚动条重要的步骤是&a…...
大V龚文祥造谣董明珠恋情被禁言
我是卢松松,点点上面的头像,欢迎关注我哦! 因造谣董明珠与王自如恋情,知名大V龚文祥老师被今日头条禁言。龚文祥说,69岁的董明珠,找了一个小自己34岁的男友,引的网友议论纷纷。 2月26日&#…...
深入浅出Reactjs
深入浅出Reactjs 介绍 React是一个流行的JavaScript库,用于开发复杂的用户界面。它可以帮助开发人员构建灵活、高效和可维护的应用程序。本文将深入浅出地介绍React开发框架。 React的核心概念 React框架的核心概念是组件。组件是一个独立的、可复用的代码块&am…...
《C++ Primer Plus》第18章:探讨 C++ 新标准(1)
本章首先复习前面介绍过的C11功能,然后介绍如下主题: 移动语义和右值引用。Lambda 表达式。包装器模板 function。可变参数模板。 本章重点介绍 C11 对 C 所做的改进。本书前面介绍过多项 C11 功能,本章首先复习这些功能,并详细…...
PCB板漏孔、漏槽怎么办?看工程师避坑“SOP”
本文为大家介绍PCB画板时常见的钻孔问题,避免后续踩同样的坑。钻孔分为三类,通孔、盲孔、埋孔。不管是哪种孔,孔缺失的问题带来的后果是直接导致整批产品不能使用。因此钻孔设计的正确性尤为重要。 案例讲解 问题1:Altium设计的文…...
mysql数据库同步方案:springboot+集成cannal
1授权 -- 使用命令登录:mysql -u root -p -- 创建用户 用户名:canal 密码:Canal123456 create user canal% identified by Canal123456; -- 授权 *.*表示所有库 grant SELECT, REPLICATION SLAVE, REPLICATION CLIENT on *.* to canal% ident…...
oracle 19c 创建物化视图并测试logminer进行日志挖掘
1、创建物化视图 alter session set containerpdb; grant create materialized view to scott; create materialized view 物化视图名 -- 1. 创建物化视图 build [immediate | deferred] -- 2. 创建方式,默认 immediate refre…...
2.1 黑群晖驱动:10代u核显硬解驱动(解决掉IP、重启无法连接问题)
本文提供了两种10代核显驱动方式:1)第一种(本文:二、仅修改i915.ko驱动10代u核显方法)为网上流传最多但是对主板兼容性要求很高,网上评论常会出现操作后无法识别IP(掉IP)的问题。因此,采用第一种…...
深圳商城网站哪家做的好/万网官网域名查询
我正在拍摄照片并将其存储到SD卡中,然后将其从SD卡中查看到ImageView中,但获得轮换…我在纵向模式下捕获它,但在横向模式下获得结果图像…有什么我想念的吗?/*** Displaying captured image/video on the screen* */private void previewMedia(boolean isImage) {//…...
wordpress esc_url/怎样建网站
编译好的程序的下载链接:百度网盘 请输入提取码(提取码:ocmm) 概述 通常情况下,我们是在电脑里面开一个Linux虚拟机, 在虚拟机里面用交叉编译工具链编译好可执行文件后,将可执行文件拷贝到板子…...
自己做网站免费/网站推广步骤
精装房真的好么?是不是墙面可以直接贴墙纸,很多人都不了解嗨喽大家好,今天继续为大家分享家居中的那些小常识,今天我们要说的,和很多人都关注的新房有管,相对于毛胚房来说,不少人都更喜欢多花一…...
网站建设公司怎么做好/应用关键词优化
一,创建列表 只要把逗号分隔的不同的数据项使用方括号([ ])括起来即可 下标(角标,索引)从0开始,最后一个元素的下标可以写-1 list [1,‘2,‘3’] list [] 空列表 二&am…...
动态网站建设试题/青岛网站建设制作
寒假工作坊Python&Stata数据分析课寒假工作坊现在开始招生了,有兴趣的同学和老师可以戳进来了解课程安排 1月9-10日 Python爬虫&文本数据分析(模块Ⅰ) 1月11-16日 Stata 应用能力提升与实证前沿(模块Ⅱ) 地点浙江 杭州(浙江工…...
商城网站建设行情/网络优化软件
基础数据类型什么是数据: x 10, 10是我们要存储的数据为什么分数据类型: 数据是用来表示状态的, 不同的状态就应该用不同的类型的数据去表示。数字 int数字主要用于计算用# bit_length() 当十进制用二进制表示时,最少使用的位数v 11data v.bit_length()print(dat…...