玩转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的处理,我都会持续更新,希望看完感觉有帮助的兄弟能点个赞,谢谢。

相关推荐
万能的小裴同学36 分钟前
Android M3U8视频播放器
android·音视频
q***57741 小时前
MySql的慢查询(慢日志)
android·mysql·adb
JavaNoober2 小时前
Android 前台服务 "Bad Notification" 崩溃机制分析文档
android
城东米粉儿2 小时前
关于ObjectAnimator
android
zhangphil3 小时前
Android渲染线程Render Thread的RenderNode与DisplayList,引用Bitmap及Open GL纹理上传GPU
android
火柴就是我4 小时前
从头写一个自己的app
android·前端·flutter
lichong9515 小时前
XLog debug 开启打印日志,release 关闭打印日志
android·java·前端
用户69371750013846 小时前
14.Kotlin 类:类的形态(一):抽象类 (Abstract Class)
android·后端·kotlin
火柴就是我6 小时前
NekoBoxForAndroid 编译libcore.aar
android
Kaede67 小时前
MySQL中如何使用命令行修改root密码
android·mysql·adb