前言
这篇文章主要解决以下问题:
- 什么是Linux标准输入协议?
- Android Input System架构是怎样的?
- Android ANR产生的原理是什么?如何避免ANR?
- 如何调试Android Input System?
本文结构
1、功能介绍
2、总体设计
3、详细设计
4、开发调试
5、总结
6、资料
一、功能介绍
Linux 输入协议在 linux/input.h 内核头文件中定义了一组标准事件类型和代码,输入设备驱动程序负责通过 Linux 输入协议将设备特定信号转换为标准输入事件格式。
接下来,Android EventHub 组件通过打开与每个输入设备关联的 evdev 驱动程序从内核读取输入事件。然后,Android InputReader 组件根据设备类别解码输入事件,并生成 Android 输入事件流。在此过程中,Linux 输入协议事件代码将根据输入设备配置、键盘布局文件和各种映射表,转化为 Android 事件代码。
最后,InputReader 将输入事件发送到 InputDispatcher,后者将这些事件转发到相应的窗口。(注:这段话选自官方输入系统文档)
二、总体设计
2.1 Input System UML图
2.1.1 InputReader主要做三件事
- mEventHub获取驱动的RawEvent进行处理
- mDevice将RawEvent 加工成成熟的NotifyArgs,并添加到mQueueInputListener
- 调用mQueueInputListener.flush(),触发队列里的NotifyArgs.notify(),Dispatcher把NotifyArgs加工成EventEntry,并添加到mInboundQueue。
2.1.2 InputDispatcher主要做三件事,
- 从mInboundQueue获取EventEntry
- 通过mWindowHandlesByDisplay找到焦点窗口
- mConnectionsByFd把EventEntry转行成InputMessage并分发到InputChannel
2.1.3 ViewRootImpl主要做三件事
- 在scheduleTraversals()中mChoreographer.postCallback()一个mConsumedBatchedInputRunnable
- 等待下一帧到来时,调用mInputEventReceiver.consumeBatchedInputEvents()开始取出InputChannel内的InputMessage转化成InputEvent。
-
mFirstInputStage根据不同的策略把InputEvent分发到不同字View处理。
ViewRootImpl对InputEvent分发过程如下图。
2. 2 InputEvent 处理流程图。
根据UML我们可以得出InputEvent数据加工流程图,如下图。
- 首先EventHub从驱动获取RawEvent,接着InputReader根据事件的type把RawEvent加工成Android事件NotifyArgs,比如NotifyMotionArgs或NotifyKeyArgs,然后InputDispatcher把NotifyArgs转化成EventEntry,然后根据当前焦点窗口,把EventEntry转化成InputMessage,存放到InputChannel。
- 当处于焦点窗口的应用下一帧渲染触发的时候,会从InputChannel取出InputMessage,再把InputMessage转化成InputEvent,比如MotionEvent或KeyEvent,最终发到ViewRootImpl分发到给对应的子View处理。
三、详细设计
3.1 关键类的职责
- InputManager:事件处理的核心,负责事件使用
- InputReaderThread(称为“InputReader”)读取和预处理原始输入事件,应用策略,并将消息发送到DispatcherThread管理的队列中。
- InputDispatcherThread(称为“InputDispatcher”)线程等待队列上的新事件,并异步地将它们分派给应用程序。
根据设计,InputReaderThread类和InputDispatcherThread类不共享任何内部状态。 而且,所有通信都是从InputReaderThread到InputDispatcherThread的一种方式,绝不会相反。 但是,这两个类都可以与InputDispatchPolicy交互。
InputManager类从不对Java本身进行任何调用。 相反,InputDispatchPolicy负责与系统执行所有外部交互,包括调用DVM服务。
EventHub: 事件的中心车站。
EventHub汇总系统上所有输入设备(包括模拟器)接收的输入事件。 此外,EventHub通过生成伪造的输入事件以指示何时添加或删除设备。
EventHub还提供输入事件流(通过getEvent方法)。
它还支持查询输入设备的当前状态,例如识别当前按下的键。 最后,EventHub还有跟踪各个输入设备的功能,例如它们的类别和它们支持的键控代码集。InputReader: InputReader从EventHub读取原始事件RawEvent数据,并将其处理为InputEvent,并将其发送到InputListener。 InputReader的某些功能(例如低功耗状态下的早期事件过滤)由单独的策略对象控制。
InputReader拥有InputMappers的集合。 它所做的大部分工作都在InputReaderThread上进行,InputReader也可以接收在任意线程上运行的其他系统组件的查询。 为了使内容易于管理,InputReader使用单个Mutex来保护其状态。 互斥对象可以在调用EventHub或InputReaderPolicy时保留,但从不保留在调用InputListener时保留。InputDevice: 表示单个输入设备的状态。
InputMapper :输入映射器将源数据(raw data)转成熟数据(cooked data),单个输入设备可以具有多个关联的输入映射器,以便解释事件的不同类别。InputReaderThread:InputReaderThread实现了类Threads.cpp的threadLoop(),Threads内部实现了一个线程循环,会不断调用threadLoop(),threadLoop()内调用InputReader loopOnce(),loopOnce()方法先从EventHub的getEvent获取RawEvent,然后把RawEvent传递给EventDevice的process(),process()方法内调用EventDevice内所有的InputMapper,InputMapper调用process()把RawEvent加工成不同类型的Event
InputDispatcherThread:InputDispatcherThread循环处理入队和调度事件。
InputDispatcherPolicyInterface :输入调度程序策略接口。
输入阅读器策略由InputReader用来与Window Manager和其他系统组件进行交互。
通过JNI到DVM的回调部分支持了实际的实现。 单元测试中也模拟了此接口。InputChannel InputChannel由一个本地unix域套接字组成,该套接字用于跨进程发送和接收输入消息。 每个通道都有一个用于调试目的的描述性名称。每个端点都有自己的InputChannel对象,用于指定其文件描述符。释放所有对输入通道的引用后,将关闭该通道。
InputPublisher: InputDispatcher通过InputPublisher将InputEvent发布到InputChannel。
InputConsumer 消费来自InputChannel的Input Event。
3.2 InputMapper如何加工Raw数据
我们以InputMapper的子类TouchInputMapper为例分析下Raw数据是如何加工成Touch数据的
首先TouchMapper调用process(),接着调用sync(),然后调用processRawTouches(), 在调用cookAndDispatch(),最后cookPointerData(),这个方法才是处理对raw数据处理成触摸数据的。这样处理完数据后,回到cookAndDispatch()方法,它接下去调用dispatchPointerUsage()分发cookEvent
dispatchMotion()发送事件.
InputDispatcher的dispatchOnce() 接着dispatchOnceInnerLocked(),然后dispatchMotionLocked(),接着dispatchEventLocked() prepareDispatchCycleLocked() ->enqueueDispatchEntriesLocked()->enqueueDispatchEntryLocked() -> traceOutboundQueueLength()
比如我们在MotionEvent收到的压感Pressure,是乘以mPressureScale才得到我们MotionEvent中获取的压感Pressure。如下图
这样当我们想在驱动层对Pressure增加协议时就需要这个知识点了,我们应用上层要先对Pressure进行还原成原始数据,最后才能获取我们协议的内容。
有了这些知识之后,如果提一个需求,底层要用倾斜角作为协议字段,上层解析协议,技术可行吗?
首先我们找到Liunx Input定义的文件
https://source.android.com/devices/input/touch-devices#orientation-and-tilt-fields
接着我们查看官方文档 Liunx Input ABS_TILT_X和ABS_TILT_Y对应着raw.tiltX和raw.tiltY
最后从Android源码中找到TouchInputMapper::cookPointerData()中raw.tiltX和raw.tiltY的部分
把raw.tiltX和raw.tiltY三角函数操作得出tilt,tilt是无法逆还原成raw.tiltX和raw.tiltY,所以我们计划倾斜角作为协议字段技术是不可行的。
3.1 InputDispatcher是如何调度数据的
分析思路:下面我们围绕MotionEvent来分析调度流程。
当InputReader处理完事件后,会调用mQueuedInputListener.flush(),flush()会把集合所有的NotifyArgs都notify() ,notify()最终调用InputDispatcher的notify(),notify()内调用mLooper.wake() 唤醒线程loop,最后触发dispatcherOnce()。
dispatcherOnce()内部实际上是调用dispatchOnceInnerLocked()来调度事件, dispatchOnceInnerLocked()主要作用是从mInboundQueue取出mPendingEvent,然后根据事件类型调度事件。dispatchOnceInnerLocked()代码如下:
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
// 准备处理新的Event
if (!mPendingEvent) {
if (mInboundQueue.isEmpty()) {
return;
} else {
// 入站队列中至少有一个Entry
mPendingEvent = mInboundQueue.dequeueAtHead();
traceInboundQueueLengthLocked();
}
// 准备发送事件
resetANRTimeoutsLocked();
}
// 现在我们有一个事件要调度,根据事件type调度事件
// 所有事件最终都会以这种方式出队和处理,即使我们打算删除它们也是如此
bool done = false;
switch (mPendingEvent->type) {
case EventEntry::TYPE_CONFIGURATION_CHANGED: {
//省略
break;
}
case EventEntry::TYPE_DEVICE_RESET: {
//省略
break;
}
case EventEntry::TYPE_KEY: {
//省略
break;
}
case EventEntry::TYPE_MOTION: {
MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);
// 调度MotionEvent
done = dispatchMotionLocked(currentTime, typedEntry, &dropReason, nextWakeupTime);
break;
}
default:
ALOG_ASSERT(false);
break;
}
if (done) {
// 调度完事件后,重置全局变量状态,比如mPendingEvent置空。
releasePendingEventLocked();
// 强制下一个轮询强制唤醒。
*nextWakeupTime = LONG_LONG_MIN;
}
}
从上面代码我们知道,dispatchOnceInnerLocked()内调用dispatchMotionLocked()来处理MotionEvent,代码如下:
bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, MotionEntry* entry,
DropReason* dropReason, nsecs_t* nextWakeupTime) {
bool isPointerEvent = entry->source & AINPUT_SOURCE_CLASS_POINTER;
// 确定输入目标。
std::vector<InputTarget> inputTargets;
bool conflictingPointerActions = false;
int32_t injectionResult;
if (isPointerEvent) {
// 指针 event. (eg. 触摸屏)
// 1. 找到事件所在的焦点窗口inputTargets
injectionResult =
findTouchedWindowTargetsLocked(currentTime, entry, inputTargets, nextWakeupTime,
&conflictingPointerActions);
}
// 2. 从事件或重点显示中添加监视频道。
addGlobalMonitoringTargetsLocked(inputTargets, getTargetDisplayId(entry));
// 省略代码
// 3.调度MotionEvent
dispatchEventLocked(currentTime, entry, inputTargets);
return true;
}
通过上面的代码我们发现dispatchMotionLocked()方法主要做三件事,首先通过findTouchedWindowTargetsLocked()找到事件所在的焦点窗口,然后inputTargets保存焦点窗口的副本。最后dispatchEventLocked()调度MotionEvent。
findTouchedWindowTargetsLocked()是如何获取焦点窗口呢?我们看看findTouchedWindowTargetsLocked()方法的实现,代码如下:
int32_t InputDispatcher::findTouchedWindowTargetsLocked(nsecs_t currentTime,
const MotionEntry* entry,
std::vector<InputTarget>& inputTargets,
nsecs_t* nextWakeupTime,
bool* outConflictingPointerActions) {
// 1.根据Touch类型判断是否刷新mTempTouchState(可以理解为缓存了焦点窗口状态)里面保存的窗口
// 当新手势或是Down时,查找焦点窗口
if (newGesture || (isSplit && maskedAction == AMOTION_EVENT_ACTION_POINTER_DOWN)) {
bool isDown = maskedAction == AMOTION_EVENT_ACTION_DOWN;
// 找焦点窗口
sp<InputWindowHandle> newTouchedWindowHandle =
findTouchedWindowAtLocked(displayId, x, y, isDown /*addOutsideTargets*/,
true /*addPortalWindows*/);
if (newTouchedWindowHandle != nullptr) {
// mTempTouchState添加查找到的newTouchedWindowHandle
mTempTouchState.addOrUpdateWindow(newTouchedWindowHandle, targetFlags, pointerIds);
}
mTempTouchState.addGestureMonitors(newGestureMonitors);
} else {
// 事件类型 move/up/cancel/不可分割的指针down
// 从mTempTouchState获取焦点窗口
}
// 2.检查mTempTouchState.windows所有窗口是否都已经准备好了。
for (const TouchedWindow& touchedWindow : mTempTouchState.windows) {
if (touchedWindow.targetFlags & InputTarget::FLAG_FOREGROUND) {
// 通过checkWindowReadyForMoreInputLocked()检测窗口是否ready。
std::string reason =
checkWindowReadyForMoreInputLocked(currentTime, touchedWindow.windowHandle,
entry, "touched");
if (!reason.empty()) {
// 没有ready则发送ANR
injectionResult = handleTargetsNotReadyLocked(currentTime, entry, nullptr,
touchedWindow.windowHandle,
nextWakeupTime, reason.c_str());
// 代码跳转到无响应部分
goto Unresponsive;
}
}
}
}
我们知道方法checkWindowReadyForMoreInputLocked()用于检测窗口是否准备好了,那他检测窗口准备好的条件是哪些呢?老方法我们从方法代码入手,代码如下:
std::string InputDispatcher::checkWindowReadyForMoreInputLocked(
nsecs_t currentTime, const sp<InputWindowHandle>& windowHandle,
const EventEntry* eventEntry, const char* targetType) {
// 如果窗口被暂停了,则继续等待
if (windowHandle->getInfo()->paused) {
return StringPrintf("Waiting because the %s window is paused.", targetType);
}
if (eventEntry->type == EventEntry::TYPE_KEY) {
// 判断key事件的逻辑
} else {
// Touch事件窗口是否ready逻辑
// 会导致暂停输入事件传递的一种情况是:由于应用程序没有响应,因此waitQueue等待队列中堆积了很多事件。
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);
}
}
}
以上代码我们可以看出会导致Touch事件无法消费的一种情况是,应用程序的等待队列堆积了事件,并且等待队列的头部事件deliveryTime + STREAM_AHEAD_EVENT_TIMEOUT超过当前事件,就会导致ANR。checkWindowReadyForMoreInputLocked()返回超时原因之后,就会执行handleTargetsNotReadyLocked()发送ANR,代码如下:
// 默认超时时间5s
constexpr nsecs_t DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5000 * 1000000LL;
int32_t InputDispatcher::handleTargetsNotReadyLocked(
nsecs_t currentTime, const EventEntry* entry,
const sp<InputApplicationHandle>& applicationHandle,
const sp<InputWindowHandle>& windowHandle, nsecs_t* nextWakeupTime, const char* reason) {
nsecs_t timeout;
// 窗口
if (windowHandle != nullptr) {
timeout = windowHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);
// Application
} else if (applicationHandle != nullptr) {
timeout =
applicationHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);
} else {
timeout = DEFAULT_INPUT_DISPATCHING_TIMEOUT;
}
onANRLocked(currentTime, applicationHandle, windowHandle, entry->eventTime,
mInputTargetWaitStartTime, reason);
}
handleTargetsNotReadyLocked() 方法会调到onANRLocked(),onANRLocked()会doNotifyANRLockedInterruptible(),doNotifyANRLockedInterruptible()会调用NativeInputManager::notifyANR(),最终调到Java层的InputManagerService.notifyANR(),最终通过WindowManagerServer的InputMonitor把ANR抛给对应的窗口。这就解答了为什么ANR?这是因为应用的UI线程有耗时的操作,导致InputDispatcher的TouchEvent堆积,当超过设置的超时时间(5s)时,就抛出ANR,所以我们把耗时操作尽量放在子线程执行,避免ANR的产生。
扩展问题:在View的onTouchEvent()做耗时操作会导致后续的MotionEvent接收变慢吗?
答案:是会,原因同样onTouchEvent()是执行在UI线程,如果做耗时操作,TouchEvent会无法及时分发导致堆积,分发事件的速率变慢。
接着我们回到方法dispatchMotionLocked(),获取MotionEvent所在焦点窗口后,我们看看dispatchEventLocked()是如何调度MotionEvent,看下代码
void InputDispatcher::dispatchEventLocked(nsecs_t currentTime, EventEntry* eventEntry,
const std::vector<InputTarget>& inputTargets) {
}
dispatchEventLocked() -> prepareDispatchCycleLocked() -> enqueueDispatchEntriesLocked() -> startDispatchCycleLocked() -> connection->InputPublisher .publishMotionEvent()
InputDispatcher最终把InputEvent发布到InputPublisher,那Android系统什么时候处理事件呢?
我们追踪代码,发现处理InputEvent的触发是下一帧渲染开始执行的,下面是方法调用链。
ViewRootImpl.scheduleTraversals() -> ViewRootImpl.scheduleConsumeBatchedInput()
FrameDisplayEventReceiver.onVsync() -> Choreographer.doFrame() ->
mConsumedBatchedInputRunnable.run() -> android_view_InputEventReceiver.nativeConsumeBatchedInputEvents() -> NativeInputEventReceiver.consumeEvents() -> InputEventReceiver.dispatchInputEvent() ->WindowInputEventReceiver.onInputEvent()
方法调用链大概执行了这些处理,ViewRootImpl执行scheduleTraversals()后在Choreographer内注册一个mConsumedBatchedInputRunnable,在下一帧开始的时候,mConsumedBatchedInputRunnable.run()会执行,调到native层nativeConsumeBatchedInputEvents(),方法内部实际上是调用NativeInputEventReceiver.consumeEvents()来调批量处理InputEvents,consumeEvents()代码如下:
status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {
for (;;) {
uint32_t seq;
InputEvent* inputEvent;
// 1. 从InputConsumer取出事件
status_t status = mInputConsumer.consume(&mInputEventFactory,
consumeBatches, frameTime, &seq, &inputEvent);
jobject inputEventObj;
switch (inputEvent->getType()) {
case AINPUT_EVENT_TYPE_KEY:
// key 事件
break;
case AINPUT_EVENT_TYPE_MOTION: {
// 2. inputEvent强转成MotionEvent,再生成Java MotionEvent对象
MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);
inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
break;
}
default:
inputEventObj = NULL;
}
// 3.inputEventObj分发到ViewRootImpl内部类WindowInputEventReceiver.onInputEvent()
env->CallVoidMethod(receiverObj.get(),
gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
}
}
大概是从InputConsumer取出数据InputEvent,并根据InputEvent生成Java成的MotionEvent,最后InputEventReceiver.dispatchInputEvent()把数据分发出去,
四、开发调试
4.1 dumpsys input
比如我们系统触摸不稳定,我们怀疑Android Input System传上来的Input Event有问题,我们如何获取EventHub/InputReader/InputDispatcher信息呢?
Android 中提供了dumpsys 输入命令可转储系统输入设备(例如键盘和触摸屏)的状态以及输入事件的处理。
命令如下:
adb shell dumpsys input
我们根据InputEvent 的流程分析EventHub/InputReader/InputDispatcher这三种状态数据是否正确或符合预期,从而排查问题。
dumpsys 输入诊断详细官方文档
4.2 getevent与sendevent
网上有很多资料这里不再赘述。
getevent/sendevent 使用说明
五、总结
我们关于Android Input System源码分析就到这里,源码分析总的思路就是顺着RawEvent的处理过程为突破口,研究RawEvent是如何加工成InputEvent的?为何要这样加工,加工完InputEvent是如何分发到对应的焦点窗口所在的应用进程,应用进程在什么时机读取InputEvent,ViewRootImpl如何把InputEvent分发到对应的子View。数据WindowManager与InputManager的关系、ViewRootImpl接收到事件是如何分发的,WindowManager本文暂时没有分析,留在下节。