玩转Android Framework:Inputflinger 触摸事件的获取,转换与分发

系统版本: Ubuntu 22.04 lts

AOSP分支: android-14.0.0_r28

Android的触摸事件是如何获取的

平常我们在应用层开发的时候,当我们想去处理view的触摸事件,可能会写以下的代码:

Kotlin 复制代码
mView.setOnTouchListener(object : OnTouchListener {
    override fun onTouch(v: View?, event: MotionEvent?): Boolean {
       
    }
})

然后根据获取的MotionEvent去处理具体的触摸事件,达成我们想要的效果,那么问题来了,从手指去触摸手机屏幕开始,事件是怎么传递到自定义View的onTouch这里的?

这其实就是Inputflinger的工作了。

首先打开adb shell, 然后输入getevent -lrt,可以看到如下界面:

这个时候我们使用手指滑动屏幕,就可以看到如下的打印:

这里,其实就是驱动层输出的数据了,我们的Inputflinger在触摸这里的处理,就是要将我们从驱动层获取的这些数据,最终发送到我们触摸的View中去,然后去处理我们的上层逻辑,比如滑动,点击等。

Android驱动层协议

Android触摸相关的官方文档,可以看这里:

source.android.com/docs/core/i...

从文档中我们可以得知,Android的多点触控基于的是Linux的multi-touch-protocol,这里可以看到具体的协议文档:

www.kernel.org/doc/Documen...

在官方文档中,我们可以看到驱动层传输的这些数据都代表着什么:

现在,我们清楚了这些之后,就可以看看,Android是怎么初步帮我们去解析并分发这些数据的。

启动InputManagerService

首先,我们先打开/frameworks/base/services/java/com/android/server/SystemServer.java,可以看到InputManagerService的初始化和启动相关的代码:

我们先来看InputManagerService的初始化方法:

可以看到里面有个mNative,从上面的代码,我们可以得知这个mNativeNativeInputManagerService类型,我们再看mNative的初始化:

可以看到,最终的实现都是Native方法,然后在初始化的时候就调用了Native方法init,我们再打开/frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp,就可以看到Nativeinit的具体实现:

可以看到里面初始化了NativeInputManager,我们再去它的初始化方法看一看:

可以看到这里创建了一个Native层的InputManager,并且将它添加为了一个Binder服务,我们再看InputManager的初始化方法:

可以看到这里初始化了四个对象mReadermBlockermProcessormDispatcher,这里面的BlockerProcessor,其实都是过滤事件的,我们重点要关注的其实是mReadermDispatcher,因为他们负责的就是事件的读取与分发,并且可以看到,这里使用的是责任链的模式去一层层的传递事件数据。

这里的初始化我们先略过,接下来我们回到/frameworks/base/services/java/com/android/server/SystemServer.java,可以看到接下来就是启动我们的InputManagerService:

我们再进入start方法,可以看到最终调用的还是Native得start方法:

可以看到,这里先调用的是mDispatcherstart,然后才是mReaderstart,我们先看mReaderstart:

可以看到这里就是开启了一个线程,并且最终不断调用自身的loopOnce,那么这个loopOnce里面,其实就是我们最终InputReader去读取驱动事件的主要逻辑了。

加载并读取驱动数据

首先,我们进入loopOnce方法,可以看到这里调用了mEventHubgetEvents方法:

在此方法中找到如下代码,可以看到这里调用了scanDevicesLocked

我们再进入此方法:

这个方法的主要作用,就是扫描我们的输入驱动设备了,可以看到目录扫描的DEVICE_INPUT_PATH的值为/dev/input 如果对Linux系统有了解的话就会知道这个目录就是Linux的输入子系统,不光是触摸屏,键盘,手柄等其它输入设备也会挂载到这个目录下面。

先进入scanDirLocked:

然后再进入openDeviceLocked:

可以看到这个方法,就是用openioctl等系统调用去打开我们的驱动设备了,并且最终,会转换成一个Device对象,并存储到mOpeningDevices中。

打开设备之后,会进入第一段循环,这里会取出所有mOpeningDevices,往要返回的事件中添加打开设备事件,并且把Device实例添加到mDevice里面:

然后再下面是这段:

这段,是往要返回的事件中添加扫描设备结束事件。

再然后,是第二段循环:

注意,这里当我们第一次进入的时候,是不会进来的,因为这个时候我们的mPendingEventCount是0,所以继续往下看:

到这里,我们就会直接返回设备扫描和添加的相关事件,这里我们先跳过,因为我们知道InputThread会不断的运行loopOnce,那么当我们第二次进入这里的时候,因为我们的设备已经扫描过了,所以会跳过这一段,接着往下运行:

这里,线程就调用了epoll_wait进入等待状态了,然后当我们读取到触摸事件的时候,这个时候就会继续往下运行并且将mPendingEventCount置为非0,并且再次循环,并且会进入到刚才提到的第二段循环中,进行epoll事件的处理,进入此循环,直接看这段代码:

可以看到这里就会把获取到的触摸事件添加到events里,然后再通过后面相同的逻辑把事件返回回去:

InputReader加工数据

现在,我们已经从驱动层获取到了触摸数据,但是现在的数据是RawEvent,是非常原始的数据,所以下一步,我们就要对获取的数据进行加工:

回到InputReaderloopOnce方法,可以看到接下来就调用了processEventsLocked,我们进入此方法:

可以看到,首先,我们处理数据之后返回的对象是是一个NotifyArgslistNotifyArgs具体的类型可能会是这些:

然后第一步,会先取出RawEventtype,并进行判断type < EventHubInterface::FIRST_SYNTHETIC_EVENT,首先这个判断条件大概率是true的,因为我们触摸的时候返回的type基本上都是EV_ABSEV_KEY之类的,在/bionic/libc/kernel/uapi/linux/input-event-codes.h可以看到这些type的值都很低:

FIRST_SYNTHETIC_EVENT的值却很高:

所以当我们触摸的时候,这里的判断基本都是true

然后再看里面的处理,是调用了processEventsForDeviceLocked

然后再下面,就是InputDeviceprocess方法:

可以看到,这里最终调用的就是mapperprocess,那么这是什么意思呢?

Mapper的工作方式

打开/frameworks/native/services/inputflinger/reader/mapper/,我们可以看到很多的Mapper文件:

这些Mapper文件,最主要的作用其实就是转换我们的RawEvent的,他们会根据不同的输入协议,采用不同的逻辑,最后将我们的RawEvent转换成不同的NotifyArgs,比如我们想关心的是触摸屏的输入处理,那我们就打开MultiTouchInputMapper.cpp看下它的处理:

具体的代码十分复杂,有兴趣可以自己看看,里面实现的各个方法其实就是对驱动层输入的数据的各种校准,储存,和计算,最后生成数据供更上层处理,建议结合协议文档去看,比如里面Slot的概念,其实就是来自于协议中的概念:

传递事件给InputDispacher

我们再回到InputReaderloopOnce,可以看到当我们的notifyArgs准备好的时候,就会调用notifyAll:

这个mQueuedListener,其实就是InputManager初始化的时候传进来的mBlocker,因为这里使用了责任链模式,事件最终经过过滤会传递给InputManagermDispacher,我们再看下notify方法:

可以看到,不同的NotifyArgs会调用不同的notify方法,因为我们传递进来的是触摸事件,也就是NotifyMotionArgs,所以最后调用的其实是mDispachernotifyMotion方法。

那么,接下来我们再进入/frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cppnotifyMotion,略过上面的一些打印log和检查的逻辑,直接进入方法的后面:

可以看到,这部分的逻辑主要就是用传入进来的NotifyMotionArgs转换成一个MotionEntry,之后调用enqueueInboundEventLocked方法,然后进入此方法,可以看到这里主要就是将此MotionEntrypush给mInboundQueue:

注意,MotionEntryEventEntry的子类,mInboundQueue后续处理的时候,都是使用EventEntry来进行处理的

那么接下来,InputDispatcher是怎么处理的呢?

InputDispatcher的分发处理

接下来,我们回到Native端的InputManager,找到它的start方法:

可以看到,在调用InputReaderstart之前,InputManager先调用了InputDispatcherstart方法,我们进入此方法:

可以看到这里所做的,就是新开始一个线程,然后不停调用dispatchOnce方法,我们进入此方法:

可以看到里面调用了dispatchOnceInnerLocked,我们再进入此方法,然后跳转到以下这段:

可以看到这里的逻辑,就是从mInboundQueue取出一个EventEntry,然后将它赋值给mPendingEvent,接下来,就会对mPendingEventtype进行判断:

我们直接进入Motion部分:

可以看到,这里调用了dispatchMotionLocked,我们进入此方法,找到如下代码段:

这部分的逻辑,就是找到我们的目标窗口,也就是我们事件分发的最终目的地,最后再执行真正的分发方法dispatchEventLocked:

可以看到,这里的逻辑,就是根据我们的InputTarget中的inputChannelConnectionToken,找到我们对应的Connection:

然后再调用prepareDispatchCycleLocked,这里的逻辑,我先按下不表,目前只需要知道Connection这里是都能取到值就行,因为InputChannel涉及到WMS

我们再进入prepareDispatchCycleLocked,可以看到最后调用的是enqueueDispatchEntriesLocked:

我们可以看到,这里先调用了6次enqueueDispatchEntryLocked,最后调用startDispatchCycleLocked,我们先进入enqueueDispatchEntryLocked,可以看到它的主要逻辑就是根据我们的EventEntry,创建了一个DispatchEntry,然后将此DispatchEntry添加给对应ConnectionoutboundQueue中:

然后我们再进入startDispatchCycleLocked,可以看到里面调用了publishMotionEvent:

然后可以看到,最终里面调用的就是对应ConnectioninputPublisherpublishMotionEvent:

我们再进入此方法,可以看到这里又将事件封装成了一个InputMessage,最后调用了InputChannelsendMessage:

再进入此方法,可以看到事件最终通过Socket的方式发送出去了:

到这一步,我们的问题就来了,最终这个事件到底会被谁接收? 还有上面提到的那些InputChannelConnection到底是什么? 那么我们接下来就要找到这些问题的答案。

WMS注册InputChannel

首先,我们要知道我们的Activity的根View实际上是一个DecorView,当我们想要去显示我们的界面的时候,需要去通过WindowManager去执行addView方法,这个方法最终执行的,是WindowManagerGlobaladdView方法,而这个方法里面会创建一个ViewRootImpl,最后会调用它的setView,这个方法中,我们可以找到如下代码:

我们可以看到,在这个方法里,初始化了一个InputChannel,并且将这个InputChannel传递给了这个方法addToDisplayAsUser,我们再跳转到/frameworks/base/services/core/java/com/android/server/wm/Session.java,找到这个方法,可以看到这里调用的是WMSaddWindow:

我们进入此方法,可以看到这里又调用了openInputChannel:

openInputChannel中,调用了InputManagerServicecreateInputChannel:

在其中,又调用了NativeInputManagerServicecreateInputChannel

接下来我们再回到Native层:

可以看到,这里最终又调用回到了InputDispatchercreateInputChannel

我们先来看openInputChannelPair:

可以看到,这个方法主要就是创建了一个socketPair和一个BBinder,并且将fd和创建的binder传给新创建的两个InputChannel,分别代表服务端和客户端。

我们再回到createInputChannel,可以看到接下来就是根据服务端的InputChannel创建一个Connection,并且将它保存到InputDispatchermConnectionsByToken,最后再将客户端的InputChannel传递回去。

最后,这个客户端的InputChannel会被保存在应用层,至此,我们就完成了应用层的WindowInputDispatcher的联系,数据最终会通过Socket的方式发送给不同进程中的应用。

InputReceiver的创建与初始化

接下来,我们再回到ViewRootImplsetView,找到如下代码块:

这个时候,我们已经知道了,这块的inputChannelSocketPair通信中代表客户端的InputChannel,在这段代码中,我们可以看到mInputEventReceiver被初始化为了一个WindowInputEventReceiver,进入此类,我们可以看到它继承了InputEventReceiver,进入InputEventReceiver的构造方法:

可以看到这里调用nativeInit,并且将InputChannelMessageQueue都传了进去,接下来我们再打开/frameworks/base/core/jni/android_view_InputEventReceiver.cpp,找到nativeInit方法:

可以看到这里创建了一个NativeInputEventReceiver,并且调用了它的initialize,我们进入此方法:

可以看到这里调用了setFdEvents,我们再进入此方法:

可以看到它是将InputChannelfd传给了Looper,并且设置了callBack为当前的NativeInputEventReceiver,也就是说当通过Socket接收到数据时,最终会调用当前NativeInputEventReceiverhandleEvent,因为我们的NativeInputEventReceiver是一个LooperCallback的子类:

也就是说,当我们的触摸事件通过Socket的方式传输出去的时候,最终调用的,就是NativeInputEventReceiverhandleEvent

InputReceiver处理事件

我们进入handleEvent,可以看到里面调用了consumeEvents:

进入此方法,找到如下代码块:

可以看到它先调用了mInputConsumerconsumemInputConsumer是在NativeInputReceiver初始化的时候根据InputChannel创建的,而它的consume方法主要就是为了用InputChannel通过Socket的方式读取到传输过来的触摸数据,并且将其写入到InputEvent里面:

接下来我们回到consumeEvents往下看,可以看到在这创建了一个inputEventObj,并且将上面获取的InputEvent转换为MotionEvent,并且将其赋值给inputEventObj:

最后,我们就可以看到这个inputEventObj会被传递给Java层并回调其dispatchInputEvent方法:

我们再回到Java层的InputEventReceiver,找到对应的dispatchInputEvent方法,可以看见这里调用的是自身的onInputEvent方法:

我们再回到ViewRootImpl,可以看到里面的WindowInputEventReceiver类重写了onInputEvent方法:

我们再进入enqueueInputEvent:

可以看到,这个方法主要就是将我们的InputEvent存储进这个QueuedInputEvent中,最后执行doProcessInputEvents:

在这里,会不断的取出待处理的InputEvent,并且执行deliverInputEvent,在这个方法中,可以看到其中调用的是InputStagedeliver方法:

注意,这里的InputStage,使用的也是类似于责任链的模式,deliver主要负责调用自身的OnProcess方法,然后调用自身的apply,然后根据OnProcess的返回,决定调用forward或者finish,如果继续向下传递数据,就会调用forwardforward会去调用onDeliverToNextmNext也会是一个InputStage,他也会执行下一个InputStagedeliver方法,知道mNext是空值,就会调用finishInputEvent,结束这个InputEvent的处理

我们可以看到在ViewRootImplsetView方法中,一共有这些InputStage:

最终我们事件的派发,实际上是在ViewPostImeInputStageOnProcess方法中:

因为我们是事件来源是触摸屏,所以会执行processPointerEvent:

可以看到,这里会调用dispatchPointerEvent:

在进入方法,可以看到最终调用的是自身的dispatchTouchEvent:

因为我们目前的这个ViewRootImpl中的mView实际上是一个DecorView,所以最终调用的就是DecorView中的dispatchTouchEvent:

这里的mWindow实际上就是Activity持有的PhoneWindow,在Activity执行attach的时候,会将PhoneWindowCallBack设置为自身:

所以最终这里执行的,就是ActivitydispatchTouchEvent:

可以看到这里执行的就是持有的PhoneWindowsuperDispatchTouchEvent,进入此方法,可以看到这里执行的是DecorViewsuperDispatchTouchEvent:

我们可以看到,最终执行的,其实就是ViewGroupdispatchTouchEvent,在这个方法中,主要处理的就是根据触摸事件的坐标等信息,找到对应的ChildView,并且调用dispatchTransformedTouchEvent方法,然后调用子ViewdispatchTouchEvent方法:

最终在子ViewdispatchTouchEvent,回调自身的OnTouchListeneronTouch方法,完成最后的任务:

总结

整个Inputflinger的关于触摸的事件处理及分发,主要的流程已经梳理出来了,可以看到里面涉及到了一些WMSAMS的内容,还有finishTouchEvent的部分,这部分涉及到ANR的处理,我都会持续更新,希望看完感觉有帮助的兄弟能点个赞,谢谢。

相关推荐
GEEKVIP6 分钟前
手机使用技巧:8 个 Android 锁屏移除工具 [解锁 Android]
android·macos·ios·智能手机·电脑·手机·iphone
model20052 小时前
android + tflite 分类APP开发-2
android·分类·tflite
彭于晏6892 小时前
Android广播
android·java·开发语言
与衫3 小时前
掌握嵌套子查询:复杂 SQL 中 * 列的准确表列关系
android·javascript·sql
500了9 小时前
Kotlin基本知识
android·开发语言·kotlin
人工智能的苟富贵10 小时前
Android Debug Bridge(ADB)完全指南
android·adb
小雨cc5566ru15 小时前
uniapp+Android面向网络学习的时间管理工具软件 微信小程序
android·微信小程序·uni-app
bianshaopeng16 小时前
android 原生加载pdf
android·pdf
hhzz16 小时前
Linux Shell编程快速入门以及案例(Linux一键批量启动、停止、重启Jar包Shell脚本)
android·linux·jar
火红的小辣椒17 小时前
XSS基础
android·web安全