- Android框架层
- Android输入系统
- Android框架层
本文首发于微信公众号「刘望舒」
基于Android 8.1
前言
在Android输入系统(三)InputReader的加工类型和InputDispatcher的分发过程这篇文章中,由于文章篇幅的原因,InputDispatcher的分发过程还有一部分没有讲解,这一部分就是事件分发到目标窗口的过程。
1. 为事件寻找合适的分发目标
我们先来回顾上一篇文章讲解的InputDispatcher的dispatchOnceInnerLocked函数: frameworks/native/services/inputflinger/InputDispatcher.cpp
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
...
DropReason dropReason = DROP_REASON_NOT_DROPPED;//1
...
switch (mPendingEvent->type) {//2
...
case EventEntry::TYPE_MOTION: {
MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);
//如果没有及时响应窗口切换操作
if (dropReason == DROP_REASON_NOT_DROPPED && isAppSwitchDue) {
dropReason = DROP_REASON_APP_SWITCH;
}
//事件过期
if (dropReason == DROP_REASON_NOT_DROPPED
&& isStaleEventLocked(currentTime, typedEntry)) {
dropReason = DROP_REASON_STALE;
}
//阻碍其他窗口获取事件
if (dropReason == DROP_REASON_NOT_DROPPED && mNextUnblockedEvent) {
dropReason = DROP_REASON_BLOCKED;
}
done = dispatchMotionLocked(currentTime, typedEntry,
&dropReason, nextWakeupTime);//3
break;
}
default:
ALOG_ASSERT(false);
break;
}
...
}
dispatchOnceInnerLocked函数中主要做了5件事,这里只截取了其中的一件事:事件的丢弃。 注释1处的dropReason代表了事件丢弃的原因,它的默认值为DROP_REASON_NOT_DROPPED,代表事件不被丢弃。 注释2处根据mPendingEvent的type做区分处理,这里主要截取了对Motion类型的处理。经过条件语句过滤,会调用注释3处的dispatchMotionLocked函数为Motion事件寻找合适的窗口。 frameworks/native/services/inputflinger/InputDispatcher.cpp
bool InputDispatcher::dispatchMotionLocked(
nsecs_t currentTime, MotionEntry* entry, DropReason* dropReason, nsecs_t* nextWakeupTime) {
if (! entry->dispatchInProgress) {
//标记当前已经进入分发的过程
entry->dispatchInProgress = true;
logOutboundMotionDetailsLocked("dispatchMotion - ", entry);
}
// 如果事件是需要丢弃的,则返回true,不会去为该事件寻找合适的窗口
if (*dropReason != DROP_REASON_NOT_DROPPED) {//1
setInjectionResultLocked(entry, *dropReason == DROP_REASON_POLICY
? INPUT_EVENT_INJECTION_SUCCEEDED : INPUT_EVENT_INJECTION_FAILED);
return true;
}
bool isPointerEvent = entry->source & AINPUT_SOURCE_CLASS_POINTER;
// 目标窗口信息列表会存储在inputTargets中
Vector<InputTarget> inputTargets;//2
bool conflictingPointerActions = false;
int32_t injectionResult;
if (isPointerEvent) {
//处理点击形式的事件,比如触摸屏幕
injectionResult = findTouchedWindowTargetsLocked(currentTime,
entry, inputTargets, nextWakeupTime, &conflictingPointerActions);//3
} else {
//处理非触摸形式的事件,比如轨迹球
injectionResult = findFocusedWindowTargetsLocked(currentTime,
entry, inputTargets, nextWakeupTime);//4
}
//输入事件被挂起,说明找到了窗口并且窗口无响应
if (injectionResult == INPUT_EVENT_INJECTION_PENDING) {
return false;
}
setInjectionResultLocked(entry, injectionResult);
//输入事件没有分发成功,说明没有找到合适的窗口
if (injectionResult != INPUT_EVENT_INJECTION_SUCCEEDED) {
if (injectionResult != INPUT_EVENT_INJECTION_PERMISSION_DENIED) {
CancelationOptions::Mode mode(isPointerEvent ?
CancelationOptions::CANCEL_POINTER_EVENTS :
CancelationOptions::CANCEL_NON_POINTER_EVENTS);
CancelationOptions options(mode, "input event injection failed");
synthesizeCancelationEventsForMonitorsLocked(options);
}
return true;
}
//分发目标添加到inputTargets列表中
addMonitoringTargetsLocked(inputTargets);//5
// Dispatch the motion.
if (conflictingPointerActions) {
CancelationOptions options(CancelationOptions::CANCEL_POINTER_EVENTS,
"conflicting pointer actions");
synthesizeCancelationEventsForAllConnectionsLocked(options);
}
//将事件分发给inputTargets列表中的目标
dispatchEventLocked(currentTime, entry, inputTargets);//6
return true;
}
注释1处说明事件是需要丢弃的,这时就会直接返回true,不会为该事件寻找窗口,这次的分发任务就没有完成,会在下一次InputDispatcherThread的循环中再次尝试分发。注释3和注释4处会对点击形式和非触摸形式的事件进行处理,将事件处理的结果交由injectionResult。后面会判断injectionResult的值,如果injectionResult的值为INPUT_EVENT_INJECTION_PENDING,这说明找到了窗口并且窗口无响应输入事件被挂起,这时就会返回false;如果injectionResult的值不为INPUT_EVENT_INJECTION_SUCCEEDED,这说明没有找到合适的窗口,输入事件没有分发成功,这时就会返回true。 注释5处会将分发的目标添加到inputTargets列表中,最终在注释6处将事件分发给inputTargets列表中的目标。 从注释2处可以看出inputTargets列表中的存储的是InputTarget结构体: frameworks/native/services/inputflinger/InputDispatcher.h
struct InputTarget {
enum {
//此标记表示事件正在交付给前台应用程序
FLAG_FOREGROUND = 1 << 0,
//此标记指示MotionEvent位于目标区域内
FLAG_WINDOW_IS_OBSCURED = 1 << 1,
...
};
//inputDispatcher与目标窗口的通信管道
sp<InputChannel> inputChannel;//1
//事件派发的标记
int32_t flags;
//屏幕坐标系相对于目标窗口坐标系的偏移量
float xOffset, yOffset;//2
//屏幕坐标系相对于目标窗口坐标系的缩放系数
float scaleFactor;//3
BitSet32 pointerIds;
}
InputTarget结构体可以说是inputDispatcher与目标窗口的转换器,其分为两大部分,一个是枚举中存储的inputDispatcher与目标窗口交互的标记,另一部分是inputDispatcher与目标窗口交互参数,比如注释1处的inputChannel,它实际上是一个SocketPair,SocketPair用于进程间双向通信,这非常适合inputDispatcher与目标窗口之间的通信,因为inputDispatcher不仅要将事件分发到目标窗口,同时inputDispatcher也需要得到目标窗口对事件的响应。注释2处的xOffset和yOffset,屏幕坐标系相对于目标窗口坐标系的偏移量,MotionEntry(MotionEvent)中的存储的坐标是屏幕坐标系,因此就需要注释2和注释3处的参数,来将屏幕坐标系转换为目标窗口的坐标系。
2. 处理点击形式的事件
在InputDispatcher的dispatchMotionLocked函数的注释3和注释4处,分别对Motion事件中的点击形式事件和非触摸形式事件做了处理,由于非触摸形式事件不是很常见,这里对点击形式事件进行解析。InputDispatcher的findTouchedWindowTargetsLocked函数如有400多行,这里截取了需要了解的部分,并且分两个部分来讲解。 frameworks/native/services/inputflinger/InputDispatcher.cpp
1.findTouchedWindowTargetsLocked函数part1:
int32_t InputDispatcher::findTouchedWindowTargetsLocked(nsecs_t currentTime,
const MotionEntry* entry, Vector<InputTarget>& inputTargets, nsecs_t* nextWakeupTime,
bool* outConflictingPointerActions) {
...
if (newGesture || (isSplit && maskedAction == AMOTION_EVENT_ACTION_POINTER_DOWN)) {
//从MotionEntry中获取坐标点
int32_t pointerIndex = getMotionEventActionPointerIndex(action);
int32_t x = int32_t(entry->pointerCoords[pointerIndex].
getAxisValue(AMOTION_EVENT_AXIS_X));
int32_t y = int32_t(entry->pointerCoords[pointerIndex].
getAxisValue(AMOTION_EVENT_AXIS_Y));
sp<InputWindowHandle> newTouchedWindowHandle;
bool isTouchModal = false;
size_t numWindows = mWindowHandles.size();//1
// 遍历窗口,找到触摸过的窗口和窗口之外的外部目标
for (size_t i = 0; i < numWindows; i++) {//2
//获取InputDispatcher中代表窗口的windowHandle
sp<InputWindowHandle> windowHandle = mWindowHandles.itemAt(i);
//得到窗口信息windowInfo
const InputWindowInfo* windowInfo = windowHandle->getInfo();
if (windowInfo->displayId != displayId) {
//如果displayId不匹配,开始下一次循环
continue;
}
//获取窗口的flag
int32_t flags = windowInfo->layoutParamsFlags;
//如果窗口时可见的
if (windowInfo->visible) {
//如果窗口的flag不为FLAG_NOT_TOUCHABLE(窗口是touchable)
if (! (flags & InputWindowInfo::FLAG_NOT_TOUCHABLE)) {
// 如果窗口是focusable或者flag不为FLAG_NOT_FOCUSABLE,则说明该窗口是”可触摸模式“
isTouchModal = (flags & (InputWindowInfo::FLAG_NOT_FOCUSABLE
| InputWindowInfo::FLAG_NOT_TOUCH_MODAL)) == 0;//3
//如果窗口是”可触摸模式或者坐标点落在窗口之上
if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) {
newTouchedWindowHandle = windowHandle;//4
break; // found touched window, exit window loop
}
}
if (maskedAction == AMOTION_EVENT_ACTION_DOWN
&& (flags & InputWindowInfo::FLAG_WATCH_OUTSIDE_TOUCH)) {
//将符合条件的窗口放入TempTouchState中,以便后续处理。
mTempTouchState.addOrUpdateWindow(
windowHandle, InputTarget::FLAG_DISPATCH_AS_OUTSIDE, BitSet32(0));//5
}
}
}
开头先从MotionEntry中获取坐标,为了后面筛选窗口用。注释1处获取列表mWindowHandles的InputWindowHandle数量,InputWindowHandle中存储保存了InputWindowInfo,InputWindowInfo中又包含了WindowManager.LayoutParams定义的窗口标志,关于窗口标志见Android解析WindowManager(二)Window的属性这篇文章。除了窗口标志,InputWindowInfo中还包含了InputChannel和窗口各种属性,InputWindowInfo描述了可以接收输入事件的窗口的属性。这么看来,InputWindowHandle和WMS中的WindowState很相似。通俗来讲,WindowState用来代表WMS中的窗口,而InputWindowHandle用来代表输入系统中的窗口。 那么输入系统是如何得到窗口信息的呢?这是因为mWindowHandles列表就是WMS更新到InputDispatcher中的。 注释2处开始遍历mWindowHandles列表中的窗口,找到触摸过的窗口和窗口之外的外部目标。注释3处,如果窗口是focusable或者flag不为FLAG_NOT_FOCUSABLE,则说明该窗口是”可触摸模式“。经过层层的筛选,如果窗口是”可触摸模式“或者坐标点落在窗口之上,会在注释4处,将windowHandle赋值给newTouchedWindowHandle。最后在注释5处,将newTouchedWindowHandle添加到TempTouchState中,以便后续处理。
2.findTouchedWindowTargetsLocked函数part2:
...
// 确保所有触摸过的前台窗口都为新的输入做好了准备
for (size_t i = 0; i < mTempTouchState.windows.size(); i++) {
const TouchedWindow& touchedWindow = mTempTouchState.windows[i];
if (touchedWindow.targetFlags & InputTarget::FLAG_FOREGROUND) {
// 检查窗口是否准备好接收更多的输入
String8 reason = checkWindowReadyForMoreInputLocked(currentTime,
touchedWindow.windowHandle, entry, "touched");//1
if (!reason.isEmpty()) {//2
//如果窗口没有准备好,则将原因赋值给injectionResult
injectionResult = handleTargetsNotReadyLocked(currentTime, entry,
NULL, touchedWindow.windowHandle, nextWakeupTime, reason.string());//3
//不做后续的处理,直接跳到Unresponsive标签
goto Unresponsive;//3
}
}
}
...
//代码走到这里,说明窗口已经查找成功
injectionResult = INPUT_EVENT_INJECTION_SUCCEEDED;//5
//遍历TempTouchState中的窗口
for (size_t i = 0; i < mTempTouchState.windows.size(); i++) {
const TouchedWindow& touchedWindow = mTempTouchState.windows.itemAt(i);
//为每个mTempTouchState中的窗口生成InputTargets
addWindowTargetLocked(touchedWindow.windowHandle, touchedWindow.targetFlags,
touchedWindow.pointerIds, inputTargets);//6
}
//在下一次迭代中,删除外部窗口或悬停触摸窗口
mTempTouchState.filterNonAsIsTouchWindows();
...
Unresponsive:
//重置TempTouchState
mTempTouchState.reset();
nsecs_t timeSpentWaitingForApplication = getTimeSpentWaitingForApplicationLocked(currentTime);
updateDispatchStatisticsLocked(currentTime, entry,
injectionResult, timeSpentWaitingForApplication);
#if DEBUG_FOCUS
ALOGD("findTouchedWindow finished: injectionResult=%d, injectionPermission=%d, "
"timeSpentWaitingForApplication=%0.1fms",
injectionResult, injectionPermission, timeSpentWaitingForApplication / 1000000.0);
#endif
return injectionResult;
}
注释1处用于检查窗口是否准备好接收更多的输入,并将结果赋值给reason。注释2处,如果reason的值不为空,说明该窗口无法接收更多的输入,注释3处的handleTargetsNotReadyLocked函数会得到无法接收更多输入的原因,赋值给injectionResult,其函数内部会计算窗口处理的时间,如果超时(默认为5秒),就会报ANR,并设置nextWakeupTime的值为LONG_LONG_MIN,强制InputDispatcherThread在下一次循环中立即被唤醒,InputDispatcher会重新开始分发输入事件。这个时候,injectionResult的值为INPUT_EVENT_INJECTION_PENDING。因为窗口无法接收更多的输入,因此会在注释4处,调用goto语句跳到Unresponsive标签,Unresponsive标签中会调用TempTouchState的reset函数来重置TempTouchState。 如果代码已经走到了注释5处,说明窗口已经查找成功,会遍历TempTouchState中的窗口,在注释6处为每个TempTouchState中 的窗口生成inputTargets。 在第一小节,InputDispatcher的dispatchMotionLocked函数的注释6处,会调用InputDispatcher的dispatchEventLocked函数 将事件分发给inputTargets列表中的分发目标,接下来我们来查看下是如何实现的。
3. 向目标窗口发送事件
InputDispatcher的dispatchEventLocked函数如下所示。 frameworks/native/services/inputflinger/InputDispatcher.cpp
void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,
EventEntry* eventEntry, const Vector<InputTarget>& inputTargets) {
#if DEBUG_DISPATCH_CYCLE
ALOGD("dispatchEventToCurrentInputTargets");
#endif
ALOG_ASSERT(eventEntry->dispatchInProgress); // should already have been set to true
pokeUserActivityLocked(eventEntry);
//遍历inputTargets列表
for (size_t i = 0; i < inputTargets.size(); i++) {
const InputTarget& inputTarget = inputTargets.itemAt(i);
//根据inputTarget内部的inputChannel来获取Connection的索引
ssize_t connectionIndex = getConnectionIndexLocked(inputTarget.inputChannel);//1
if (connectionIndex >= 0) {
//获取保存在mConnectionsByFd容器中的Connection
sp<Connection> connection = mConnectionsByFd.valueAt(connectionIndex);
//根据inputTarget,开始事件发送循环
prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget);//2
} else {
#if DEBUG_FOCUS
ALOGD("Dropping event delivery to target with channel '%s' because it "
"is no longer registered with the input dispatcher.",
inputTarget.inputChannel->getName().string());
#endif
}
}
}
遍历inputTargets列表,获取每一个inputTarget,注释1处,根据inputTarget内部的inputChannel来获取Connection的索引,再根据这个索引作为Key值来获取mConnectionsByFd容器中的Connection。Connection可以理解为InputDispatcher和目标窗口的连接,其内部包含了连接的状态、InputChannel、InputWindowHandle和事件队列等等。注释2处调用prepareDispatchCycleLocked函数根据当前的inputTarget,开始事件发送循环。最终会通过inputTarget中的inputChannel来和窗口进行进程间通信,最终将Motion事件发送给目标窗口。
4. Motion事件分发过程总结
结合Android输入系统(二)IMS的启动过程和输入事件的处理和Android输入系统(三)InputReader的加工类型和InputDispatcher的分发过程这两篇文章,可以总结一下Motion事件分发过程,简化为下图。
- Motion事件在InputReaderThread线程中的InputReader进行加工,加工完毕后会判断是否要唤醒InputDispatcherThread,如果需要唤醒,会在InputDispatcherThread的线程循环中不断的用InputDispatcher来分发 Motion事件。
- 将Motion事件交由InputFilter过滤,如果返回值为false,这次Motion事件就会被忽略掉。
- InputReader对Motion事件加工后的数据结构为NotifyMotionArgs,在InputDispatcher的notifyMotion函数中,用NotifyMotionArgs中的事件参数信息构造一个MotionEntry对象。这个MotionEntry对象会被添加到InputDispatcher的mInboundQueue队列的末尾。
- 如果mInboundQueue不为空,取出mInboundQueue队列头部的EventEntry赋值给mPendingEvent。
- 根据mPendingEvent的值,进行事件丢弃处理。
- 调用InputDispatcher的findTouchedWindowTargetsLocked函数,在mWindowHandles窗口列表中为Motion事件找到目标窗口,并为该窗口生成inputTarget。
- 根据inputTarget获取一个Connection,依赖Connection将输入事件发送给目标窗口。
这里只是简单的总结了Motion事件分发过程,和Motion事件类似的还有key事件,就需要读者自行去阅读源码了。
后记
实际上输入系统还有很多内容需要去讲解,比如inputChannel如何和窗口进行进程间通信,InputDispatcher如何得到窗口的反馈,这些内容会在本系列的后续文章中进行讲解。
感谢 《深入理解Android》卷三