Android Input系统分析和调试手段介绍
一、背景
Android
系统中有很多的系统服务管理不同的内容,比如WMS(WindowManagerService)
负责Android
的窗口管理、布局及动画的工作机制。窗口不仅是内容绘制的载体,同时也是用户输入事件的目标。
这里将讨论下Andorid
输入系统的原理,包括输入设备的管理、输入事件的加工方式以及派发流程。
主要内容涉及两方面:
- 输入设备
- 输入事件
触摸屏与键盘是Android
最普遍也是最标准的输入设备。其实Android
所支持的输入设备的种类不止这两个,鼠标、游戏手柄均在支持之列。当输入设备可用时,Linux
内核会在/dev/input/
下创建对应的名为event0~n
或其他名称的设备节点。
当用户操作输入设备时,Linux
内核接收到对应的硬件中断,然后将中断加工成原始的输入事件数据并写入其对应的设备节点中,在用户空间可以用过read()
函数将事件数据读出。
Android
输入系统的工作原理概括来说,就是监控/dev/input/
下的所有设备节点,当某个节点有数据可读时,将数据读出并进行一系列的翻译加工,然后在所有的窗口中寻找合适的事件接收者,并派发给它。
我们要怎么直观的看到一直输入操作会产生什么样的事件数据呢?获取答案的最简单的办法就是用getevent
工具
getevent
监听输入设备节点的内容,当输入事件被写入节点时,getevent
会将其读出并打印在屏幕上。由于getevent
不会对事件数据做任何加工,因此其输出的内容是由内核提供的最原始的事件。
比如我们可以通过adb shell getevent -lrt
来通过点击Power
键查看打印:
generic_x86_64:/dev/input $ getevent -t
add device 1: /dev/input/event0
name: "Power Button"
add device 2: /dev/input/event1
name: "qwerty2"
add device 3: /dev/input/event2
name: "goldfish_rotary"
[ 5850.489165] /dev/input/event1: 0001 0074 00000001
[ 5850.489165] /dev/input/event1: 0000 0000 00000000
[ 5850.549444] /dev/input/event1: 0001 0074 00000000
[ 5850.549444] /dev/input/event1: 0000 0000 00000000
----------------------------------------------------------------
generic_x86_64:/dev/input $ getevent -lrt
add device 1: /dev/input/event0
name: "Power Button"
add device 2: /dev/input/event1
name: "qwerty2"
add device 3: /dev/input/event2
name: "goldfish_rotary"
[ 5391.608726] /dev/input/event1: EV_KEY KEY_POWER DOWN
[ 5391.608726] /dev/input/event1: EV_SYN SYN_REPORT 00000000
[ 5391.678479] /dev/input/event1: EV_KEY KEY_POWER UP
[ 5391.678479] /dev/input/event1: EV_SYN SYN_REPORT 00000000
每条数据有5项信息:
- 产生事件时的事件戳([ 5850.489165])
- 产生事件的设备节点(
/dev/input/event1
) - 事件类型(
0001
) - 事件代码(
0074
) - 事件的值(
00000001
) 注意它的输出是十六进制的,0x01
即EV_KEY
表示此事件为一条按键事件,代码0x74
表示电源键的扫描码,值0x01
表示按下,0x00
表示抬起。
二、Android输入系统简介
1. 输入系统总体流程
背景中讲述了输入事件的源头是位于/dev/input/
下的设备节点,而输入系统的终点是WMS
管理的某个窗口。最初的输入事件为内核生成的原始事件,而最终交付给窗口的则是KeyEvent
或MotionEvent
对象。因此Android
输入系统的主要工作就是读取设备节点中的原始事件,将其加工封装,然后派发给一个特定的窗口以及窗口中的控件。这个过程由InputManagerService
(以下简称IMS
)系统服务为核心的多个参与者共同完成。
输入系统的总体流程和参与者如图所示:
Linux
内核,接受输入设备的中断,并将原始事件的数据写入设备节点中。- 设备节点,作为内核与
IMS
的桥梁,它将原始事件的数据暴露给用户空间,以便IMS
可以从中读取事件。 InputManagerService
,一个Android
系统服务,它分为Java
层和Native
层两部分。Java
层负责与WMS
通信。而Native
层则是InputReader
和InputDispatcher
两个输入系统关键组件的运行容器。EventHub
,直接访问所有的设备节点。并且正如其名字所描述的,它通过一个名为getEvents()
的函数将所有输入系统相关的待处理的底层事件返回给使用者。这些事件包括原始输入事件、设备节点的增删等。InputReader
,是IMS
中的关键组件之一。它运行与一个独立的线程中,负责管理输入设备的列表与配置,以及进行输入事件的加工处理。它通过其线程循环不断地通过getEvents()
函数从EventHub
中将事件取出并进行处理。对于设备节点的增删事件,它会更新输入设备列表与配置。对于原始输入事件,InputReader
对其进行翻译、组装、封装为包含更多信息、更具可读性的输入事件,然后交给InputDispatcher
进行派发。InputReaderPolicy
,它为InputReader
的事件加工处理提供一些策略配置,列入键盘布局信息等。InputDispatcher
,是IMS
中另一个关键组件。它也运行于一个独立的进程中。InputDispatcher
中保管了来自WMS
的所有窗口的信息,其收到来自InputReader
的输入事件后,会在其保管的窗口中寻找合适的窗口,并将事件派发给此窗口。InputDispatcherPolicy
,它为InputDispatcher
的派发过程提供策略控制。例如截取某些特定的输入事件用作特殊用途,或者阻止将某些事件派发给目标窗口。一个典型的例子就是HOME
键被InputDispatcherPolicy
截取到PhoneWindowManager
中进行处理,并阻止窗口收到HOME
键按下的事件。- WMS,虽说不是输入系统中的一员,但是它却对
InputDispatcher
的正常工作起到了一个至关重要的作用。当新建窗口时,WMS
为新窗口创建了事件传递所用的通道。另外,WMS
还将所有窗口的信息,包括窗口的可点击区域、焦点窗口等信息,实时地更新到IMS
的InputDispatcher
中,使得InputDispatcher
可以正确地将事件派发到指定的窗口。 ViewRootImpl
,对某些窗口,如壁纸窗口、SurfaceView
的窗口来说,窗口就是输入事件派发的终点。而对其他的如Activity
、对话框等使用了Android
控件系统的窗口来说,输入事件的终点就是控件ViewRootImp
l将窗口所接收到的输入事件沿着控件树将事件派发给感兴趣的控件。
简单来说,内核将原始事件写入设备节点中,InputReader
不断地通过EventHub
将原始事件取出来并翻译加工成Android
输入事件,然后交给InputDispatcher
。InputDispatcher
根据WMS
提供的窗口信息将事件交给合适的窗口。窗口的ViewRootImpl
对象再沿着控件树将事件派发给感兴趣的控件。控件对其收到的事件做出相应,更新自己的画面、执行特定的动作。所有这些参与者以IMS
为核心,构建了Android
庞大而复杂的输入体系。
2.IMS结构
InputManager
是输入控制中心,含有两个关键线程:
- InputReaderThread:主要功能在
InputReader
,负责解析获取的输入事件,并分发给对应的Mapper
进行处理。其中,EventHub
是输入设备的控制中心,直接与Input
驱动交互,负责处理输入设备的增减、查询,处理输入事件并向上层提供getEvents()
接口接收事件。 EventHub 的构造函数主要做三件事:
-
- 创建
epoll
对象,之后就可以把多路输入设备的fd
(类似设备名称)挂到epoll
上,等待输入事件。
- 创建
-
- 建立用于唤醒的
pipe
,把读端挂到epoll
上,如果有设备参数的变化需要处理,而getEvents()
又阻塞在设备上,调用wake()
在pipe
的写端写入,可以让线程从等待中返回。
- 建立用于唤醒的
-
- 利用
inotify
机制监听/dev/input
目录下的变更,如有则标识着设备的变化,需要处理。
- 利用
- InputDispatcherThread:主要功能在
InputDispacher
,用于将事件分发给目标窗口。 由于事件的处理是流水线模式,需要需要EventHub
先读取事件,InputReader
解析处理事件,然后InputDispatcher
才能进一步分发。
3. 启动时序图
三、多点触控协议分析
类型部分:
1.EV_SYN
同步事件,在事件开始或完成时会有
对应的code
:
SYN_REPORT
:代表一个事件的结束 (必要)
2.EV_ABS
事件的一种绝对坐标类型
对应code
:
ABS_MT_SLOT
本质代表者不同手指,它的value
代表手指id
ABS_MT_TRACKING_ID
该类型特有的,实际上,每个slot
会和一个ID
相对应,一个非负数的表示一次接触,-1
表示这是一个无用的slot
(或者理解为一次接触的结束) 。无论在接触的类型相对应的slot
发生了改变,驱动都应该通过改变这个值来使这个slot
失效。并且下一次触摸的ID
值会是这次的值加1。
ABS_MT_POSITION_X, ABS_MT_POSITION_Y
相对于屏幕中心的x,y坐标。
ABS_MT_TOUCH_MAJOR
接触部分的长轴长度。相当于椭圆的长轴。
ABS_MT_TOUCH_MINOR
接触部分的短轴长度。相当于椭圆的短轴。
ABS_MT_PRESSURE
代表按下压力,有的设备不一定有
3.EV_KEY
事件的一种类型。表示是按键(不仅仅指的物理按键也包括TOUCH
)事件
对应code
:
BTN_TOUCH
触碰按键。其值是DOWN
或者UP
。
BTN_TOOL_FINGER
按键的是finger
,并且其值也是DOWN
或者UP
4.案例分析
两个手指分别按下,移动,然后分别抬起:
[ 150.532210] EV_ABS ABS_MT_SLOT 00000000 // 代表第一个手指,其实第一个也可以没有,有的机器就第一次0是没有这个slot
[ 150.532210] EV_ABS ABS_MT_TRACKING_ID 00000013 // 第一个手指对应的TRACKING_ID
[ 150.532210] EV_ABS ABS_MT_POSITION_X 000001ca // 按下X轴坐标
[ 150.532210] EV_ABS ABS_MT_POSITION_Y 0000019e // 按下Y轴坐标
[ 150.532210] EV_ABS ABS_MT_TOUCH_MAJOR 00000063 // 按下的椭圆长轴
[ 150.532210] EV_KEY BTN_TOUCH DOWN // 触摸按下
[ 150.532210] EV_SYN SYN_REPORT 00000000 // 同步尾(不省略)
[ 150.680774] EV_ABS ABS_MT_POSITION_X 000001cb
[ 150.680774] EV_SYN SYN_REPORT 00000000 rate 50
[ 150.701444] EV_ABS ABS_MT_TOUCH_MAJOR 00000069
[ 150.701444] EV_ABS ABS_MT_SLOT 00000001 // 代表第二手指出来了
[ 150.701444] EV_ABS ABS_MT_TRACKING_ID 00000014 // 第二个手指对应TRACKING_ID
[ 150.701444] EV_ABS ABS_MT_POSITION_X 000002e8
[ 150.701444] EV_ABS ABS_MT_POSITION_Y 0000020f
[ 150.701444] EV_ABS ABS_MT_TOUCH_MAJOR 00000040
[ 150.701444] EV_SYN SYN_REPORT 00000000 rate 48
[ 150.711338] EV_ABS ABS_MT_SLOT 00000000
[ 150.711338] EV_ABS ABS_MT_TOUCH_MAJOR 0000006a
[ 150.711338] EV_ABS ABS_MT_SLOT 00000001
[ 150.811646] EV_ABS ABS_MT_TOUCH_MAJOR 0000005f
[ 150.811646] EV_SYN SYN_REPORT 00000000 rate 98
[ 150.821404] EV_ABS ABS_MT_SLOT 00000000
[ 150.821404] EV_ABS ABS_MT_TOUCH_MAJOR 00000068
[ 150.821404] EV_ABS ABS_MT_SLOT 00000001
[ 150.821404] EV_ABS ABS_MT_POSITION_Y 00000210
[ 150.821404] EV_ABS ABS_MT_TOUCH_MAJOR 00000060
[ 150.821404] EV_SYN SYN_REPORT 00000000 rate 102
[ 150.831514] EV_ABS ABS_MT_SLOT 00000000
[ 150.831514] EV_ABS ABS_MT_TOUCH_MAJOR 00000064
[ 150.831514] EV_ABS ABS_MT_SLOT 00000001
[ 150.831514] EV_ABS ABS_MT_POSITION_X 000002e9
[ 150.831514] EV_ABS ABS_MT_TOUCH_MAJOR 00000062
[ 150.831514] EV_SYN SYN_REPORT 00000000 rate 98
[ 150.861588] EV_ABS ABS_MT_SLOT 00000000
[ 150.861588] EV_ABS ABS_MT_TOUCH_MAJOR 0000003e
[ 150.861588] EV_ABS ABS_MT_SLOT 00000001
[ 150.861588] EV_ABS ABS_MT_TOUCH_MAJOR 00000067
[ 150.861588] EV_SYN SYN_REPORT 00000000 rate 100
[ 150.871400] EV_ABS ABS_MT_TOUCH_MAJOR 00000068
[ 150.871400] EV_SYN SYN_REPORT 00000000 rate 101
[ 150.881384] EV_ABS ABS_MT_SLOT 00000000 // 第一个手指有事件
[ 150.881384] EV_ABS ABS_MT_TOUCH_MAJOR 00000000
[ 150.881384] EV_SYN SYN_REPORT 00000000 rate 100
[ 150.890727] EV_ABS ABS_MT_TRACKING_ID ffffffff // RACKING_ID为-1代表第一个手指抬起消失
[ 150.890727] EV_ABS ABS_MT_SLOT 00000001 // 第二个手指有事件
[ 150.890727] EV_ABS ABS_MT_TOUCH_MAJOR 00000069
[ 150.890727] EV_SYN SYN_REPORT 00000000 rate 107
[ 151.120431] EV_ABS ABS_MT_TRACKING_ID ffffffff // 第二个手指消失抬起
[ 151.120431] EV_KEY BTN_TOUCH UP // 抬起
[ 151.120431] EV_SYN SYN_REPORT 00000000 rate 98
四、调试方法
1. 开发者选项,显示点按操作反馈
通过开发者选项——显示点按操作反馈打开
P: X / Y
P
就是pointers
; x 是 current number pointers
, y 是 max number pointers
,这些都是指在一个完整手势中的。也就是,当同时用三手指触摸时x=y=3,而当只抬起一根手指时,当前屏幕上只有两根手指了,但是整个手势事件中最大pointers
数是3,所以,x=2,y=3。显示为P:2/3
X:640.9 Y:1250.9
X是active pointer
的X轴坐标;Y是active pointer
的Y轴坐标。当多点触摸时只有一个pointer
是激活pointer
(ActivePointer
),所以X,Y表示的就是这个ActivePointer
的X和Y轴坐标。dX和dY分别代表整个手势结束后活动点(ActivePointer
)在X轴和Y轴方向上起始点到终止点的差值,其中X轴上从左到右为正值,Y轴上从上到下是正值,否则为负值。
Xv:0.0 Yv:0.0
Xv和Yv分别代表了pointer
当前触摸点point
的X轴和Y轴方向上的速度,X轴向右,Y轴向下代表了正方向,否则为负数。多点触摸的情况下,Xv和Yv代表了ActivePointer
的状态。
Prs:0.50
Prs 表示 Press
,代表一个手指或者其他设备作用在屏幕上的压力值。取值范围为0~1。
Size:0.0
描述了设备的最大可探测区域上pointer touch area
的近似大小,代表了屏幕被按压区域的近似大小。
2.dumpsys查看信息
通过adb shell dumpsys input
打印获取input
信息
wylin@wylin-virtual-machine:~$ adb shell dumpsys input
INPUT MANAGER (dumpsys input)
Input Manager State:
Interactive: true
System UI Visibility: 0x8008
Pointer Speed: 0
Pointer Gestures Enabled: true
Show Touches: false
Pointer Capture Enabled: false
Event Hub State:
BuiltInKeyboardId: 2
Devices:
-1: Virtual
Classes: 0x40000023
Path: <virtual>
...
2: qwerty2 (aka device 0 - built-in keyboard)
Classes: 0x0000009f
Path: /dev/input/event1
...
Unattached video devices:
<none>
Input Reader State:
Device -1: Virtual
...
Device 0: qwerty2
Generation: 12
IsExternal: false
AssociatedDisplayPort: <none>
HasMic: false
Sources: 0x80011107
KeyboardType: 2
Motion Ranges:
...
Switch Input Mapper:
SwitchValues: 0
Keyboard Input Mapper:
...
Cursor Input Mapper:
...
Touch Input Mapper (mode - direct):
...
Device 1: Power Button
...
Device 3: goldfish_rotary
...
Input Classifier State:
Motion Classifier:
<nullptr>
Input Dispatcher State:
DispatchEnabled: true
DispatchFrozen: false
InputFilterEnabled: false
FocusedDisplayId: 0
FocusedApplications:
displayId=0, name='AppWindowToken{5834e9c token=Token{92ade0f ActivityRecord{f10076e u0 com.android.gallery3d/.app.GalleryActivity t24}}}', dispatchingTimeout=5000.000ms
FocusedWindows:
displayId=0, name='Window{6969d63 u0 com.android.gallery3d/com.android.gallery3d.app.GalleryActivity}'
TouchStates: <no displays touched>
Display: 0
Windows:
0: name='Window{acc843f u0 NavigationBar0}', displayId=0, portalToDisplayId=-1, paused=false, hasFocus=false, hasWallpaper=false, visible=true, canReceiveKeys=false, flags=0x21840068, type=0x000007e3, layer=0, frame=[0,728][480,800], globalScale=1.000000, windowScale=(1.000000,1.000000), touchableRegion=[0,728][480,800], inputFeatures=0x00000000, ownerPid=2070, ownerUid=10089, dispatchingTimeout=5000.000ms
1: name='Window{62de810 u0 StatusBar}', displayId=0, portalToDisplayId=-1, paused=false, hasFocus=false, hasWallpaper=false, visible=true, canReceiveKeys=false, flags=0x81840048, type=0x000007d0, layer=0, frame=[0,0][480,36], globalScale=1.000000, windowScale=(1.000000,1.000000), touchableRegion=[0,0][480,36], inputFeatures=0x00000000, ownerPid=2070, ownerUid=10089, dispatchingTimeout=5000.000ms
2: name='Window{6969d63 u0 com.android.gallery3d/com.android.gallery3d.app.GalleryActivity}', displayId=0, portalToDisplayId=-1, paused=false, hasFocus=true, hasWallpaper=false, visible=true, canReceiveKeys=true, flags=0x01810120, type=0x00000001, layer=0, frame=[0,0][480,800], globalScale=1.000000, windowScale=(1.000000,1.000000), touchableRegion=[0,0][480,800], inputFeatures=0x00000000, ownerPid=25962, ownerUid=10091, dispatchingTimeout=5000.000ms
3: name='Window{14fdc42 u0 com.android.systemui.ImageWallpaper}', displayId=0, portalToDisplayId=-1, paused=false, hasFocus=false, hasWallpaper=false, visible=false, canReceiveKeys=false, flags=0x00014318, type=0x000007dd, layer=0, frame=[0,0][971,800], globalScale=1.000000, windowScale=(1.280124,1.280000), touchableRegion=[0,0][759,625], inputFeatures=0x00000000, ownerPid=2070, ownerUid=10089, dispatchingTimeout=5000.000ms
Global monitors in display 0:
0: 'PointerEventDispatcher0 (server)',
RecentQueue: length=10
KeyEvent(deviceId=0, source=0x00000101, displayId=0, action=DOWN, flags=0x00000008, keyCode=26, scanCode=116, metaState=0x00000000, repeatCount=0), policyFlags=0x02000000, age=16484555.0ms
...
MotionEvent(deviceId=0, source=0x00001002, displayId=0, action=DOWN, actionButton=0x00000000, flags=0x00000000, metaState=0x00000000, buttonState=0x00000000, classification=NONE, edgeFlags=0x00000000, xPrecision=68.3, yPrecision=41.0, pointers=[0: (205.0, 494.0)]), policyFlags=0x62000000, age=15328.5ms
PendingEvent: <none>
InboundQueue: <empty>
ReplacedKeys: <empty>
Connections:
0: channelName='PointerEventDispatcher0 (server)', windowName='PointerEventDispatcher0 (server)', status=NORMAL, monitor=true, inputPublisherBlocked=false
OutboundQueue: <empty>
WaitQueue: <empty>
1: channelName='2c87510 AssistPreviewPanel (server)', windowName='2c87510 AssistPreviewPanel (server)', status=NORMAL, monitor=false, inputPublisherBlocked=false
OutboundQueue: <empty>
WaitQueue: <empty>
2: channelName='acc843f NavigationBar0 (server)', windowName='acc843f NavigationBar0 (server)', status=NORMAL, monitor=false, inputPublisherBlocked=false
OutboundQueue: <empty>
WaitQueue: <empty>
...
OutboundQueue: <empty>
WaitQueue: <empty>
AppSwitch: not pending
Configuration:
KeyRepeatDelay: 50.0ms
KeyRepeatTimeout: 500.0ms
该命令会打印出Input
系统中的一些状态
比如EventHub
中会显示当前监听的设备,如上面显示
可以看到触摸屏响应的是/dev/input/event1
,它的对应device name
是qwerty2
,可以在dumpsys
根据name
找到它的信息,如XScale: 1.000 并且这里可以看到该设备目前有几个可以分发的Mapper
,用于通过该节点读出后可能分发到的几个地方
而InputDispatcher
中显示Display:0
表示当前的屏幕,已经当前Focused
的窗口和应用,Windows
中显示当前存在的连接,用于分发触摸事件
RecentQueue
:显示的是过去消费掉的事件
这里需要注意有三个队列比较重要
inboundQueue
:还未加工的数据outboundQueue
:已经在InputDispatcher
中做了处理,等待发送waitQueue
:已经通过socket
发送了,等待响应InboundQueue
用于从InputReader
处理添加到InboundQueue
队列中,而OutboundQueue
,WaitQueue
放在不同的Connections
中,说明是分发到哪个连接中,哪里就会进行处理 最后会放在WaitQueue
中,当事件由窗口处理完成后,再通知到这里将其移除,而这里如果出现Input ANR
的话,可以看下这里是否有未消费的Input
事件,可能就是这的原因,然后再根据这个队列哪里添加哪里移除在对应代码段中排查问题
3.常见IMS问题排查流程
- 驱动层排查
通过
adb shell getevent
确认硬件驱动已上报事件,未上报需找驱动,硬件确认按键或屏 framework
层IMS->APP
排查派发 通过dumpsys input
查看input
事件派发情况APP
层 通过log
和堆栈检查派发
转载自:https://juejin.cn/post/7371716394301571110