Android开发input问题解决分析
作者:咖啡你冲不冲 发布时间:2021-11-10 08:58:46
Android Input
Android Input指的是输入事件,主要是触摸滑动,当然还包括类似蓝牙外设的输入。Input涉及到的主要模块
EventHub :对输入事件进行映射
InputReader : 收集input事件
InputDispatcher : 将事件分发到上层
InputManager : framework中对input事件的接收和分发
WMS : 管理窗口,收集和分发input事件
本篇主要以framework的视角来debug input问题,介绍input的资料已经很多了,所以不讲input传递流程和机制,只看如何去解决问题。
从framework的视角,首先我们要排查input driver的问题,比如从屏幕触摸输入的,那就是显示屏的input驱动;如果是蓝牙外设输入的,那就需要找BT的驱动层。
adb shell getEvent
然后再输入,看键值是否正常,如果getEvent都没有收到,就不属于framework的范畴了。
确定驱动没有问题之后,就可以通过动态或静态开启debug log。不同厂商的开关log的命令有些差异,打印log的内容也不太一样。
这里我们直接以本地debug为例,参考Android T版本的common code自己添加关键log,然后开始复现问题,检查问题时间点的log。顺便补充一下,可以通过如下命令使时间显示到秒,这样方便复现问题时对应log时间
adb shell settings put secure clock_seconds 1
Step1.查看ViewRootImpl是否有收到input event
/frameworks/base/core/java/android/view/ViewRootImpl.java
@UnsupportedAppUsage
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
if (event instanceof MotionEvent) {
MotionEvent me = (MotionEvent) event;
if (me.getAction() == MotionEvent.ACTION_CANCEL) {
EventLog.writeEvent(EventLogTags.VIEW_ENQUEUE_INPUT_EVENT, "Motion - Cancel",
getTitle().toString());
}
} else if (event instanceof KeyEvent) {
KeyEvent ke = (KeyEvent) event;
if (ke.isCanceled()) {
EventLog.writeEvent(EventLogTags.VIEW_ENQUEUE_INPUT_EVENT, "Key - Cancel",
getTitle().toString());
}
}
// Always enqueue the input event in order, regardless of its time stamp.
// We do this because the application or the IME may inject key events
// in response to touch events and we want to ensure that the injected keys
// are processed in the order they were received and we cannot trust that
// the time stamp of injected events are monotonic.
QueuedInputEvent last = mPendingInputEventTail;
if (last == null) {
mPendingInputEventHead = q;
mPendingInputEventTail = q;
} else {
last.mNext = q;
mPendingInputEventTail = q;
}
mPendingInputEventCount += 1;
Trace.traceCounter(Trace.TRACE_TAG_INPUT, mPendingInputEventQueueLengthCounterName,
mPendingInputEventCount);
//添加log打印关键信息
Log.i(">_<!!","enqueueInputEvent: event = " + event + " ,this = " + this);
if (processImmediately) {
doProcessInputEvents();
} else {
scheduleProcessInputEvents();
}
}
这里只需要根据添加的log查看两个参数即可,event会打印出来 KeyEvent的action和keyCode,我们需要看下这里的action和keyCode是否有紊乱的情况,如果输入和get到的不对应,那还是需要driver来协调。后面打印出来的this就是此ViewRootImpl对象,具体内容可以看它的toString方法。
我们只需要在最终的log中观察这句是否打印出来,如果打印出来了,说明input事件已经成功发送到应用端了,跳过下面步骤,直接检查Step5,如果没打印这段log,再看Step2
Step2. 查看inputDispatcher是否有收到input event
/frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
bool InputDispatcher::dispatchKeyLocked(nsecs_t currentTime, std::shared_ptr<KeyEntry> entry,
DropReason* dropReason, nsecs_t* nextWakeupTime) {
// Preprocessing.
if (!entry->dispatchInProgress) {
// 这个是AOSP的log机制,不用再另外添加log
logOutboundKeyDetails("dispatchKey - ", *entry);
}
void InputDispatcher::logOutboundKeyDetails(const char* prefix, const KeyEntry& entry) {
//if (DEBUG_OUTBOUND_EVENT_DETAILS) {
if (true) {
ALOGD("%seventTime=%" PRId64 ", deviceId=%d, source=0x%x, displayId=%" PRId32 ", "
"policyFlags=0x%x, action=0x%x, flags=0x%x, keyCode=0x%x, scanCode=0x%x, "
"metaState=0x%x, repeatCount=%d, downTime=%" PRId64,
prefix, entry.eventTime, entry.deviceId, entry.source, entry.displayId,
entry.policyFlags, entry.action, entry.flags, entry.keyCode, entry.scanCode,
entry.metaState, entry.repeatCount, entry.downTime);
}
}
这里AOSP的log已经添加的很全面了,我们只需要手动将打印条件置为true即可。这段log中同样可以对应上action和keyCode,不过c++代码打印出来的是十六进制,但是也和上面java code中打印出来的字符串是一一对应的。如果我们最终可以搜索到这段log,说明inputDispatcher已经收到input event了,那么直接快进到Step4检查inputDispatcher状态是否正常。如果没有查看到这句log,再看Step3
Step3. 查看inputreader线程里面是否有keycode
/frameworks/native/services/inputflinger/reader/mapper/KeyboardInputMapper.cpp
int32_t usageCode) {
int32_t keyCode;
int32_t keyMetaState;
uint32_t policyFlags;
if (getDeviceContext().mapKey(scanCode, usageCode, mMetaState, &keyCode, &keyMetaState,
&policyFlags)) {
keyCode = AKEYCODE_UNKNOWN;
keyMetaState = mMetaState;
policyFlags = 0;
}
if (mParameters.handlesKeyRepeat) {
policyFlags |= POLICY_FLAG_DISABLE_KEY_REPEAT;
}
NotifyKeyArgs args(getContext()->getNextId(), when, readTime, getDeviceId(), mSource,
getDisplayId(), policyFlags,
down ? AKEY_EVENT_ACTION_DOWN : AKEY_EVENT_ACTION_UP,
AKEY_EVENT_FLAG_FROM_SYSTEM, keyCode, scanCode, keyMetaState, downTime);
getListener().notifyKey(&args);
ALOGI("device: %s, keyCode=%d, scanCode=%d, eventTime = %lld, action=0x%x,duwnTime=%lld",getDeviceName().c_str(), keyCode, scanCode, args,eventTime, args.action. args.downTime);
}
KeyboardInputMapper.cpp 是在Android R之后添加的工具,如果是比较旧的版本,需要在InputReader.cpp中添加log。此处可以确定input event被发送到了inputReader了,这里的值就是从getEvent读取的,如果getEvent的值是对的,但这里没有打印log,就需要打印cpp文件的callstack,看看是流程中哪一步出错。
Step4. 检查inputDispatcher的状态是否正常
可以通过adb命令来查看inputDispatcher的状态
adb shell dumpsys input
/frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
void InputDispatcher::dumpDispatchStateLocked(std::string& dump) {
dump += StringPrintf(INDENT "DispatchEnabled: %s\n", toString(mDispatchEnabled));
dump += StringPrintf(INDENT "DispatchFrozen: %s\n", toString(mDispatchFrozen));
dump += StringPrintf(INDENT "InputFilterEnabled: %s\n", toString(mInputFilterEnabled));
dump += StringPrintf(INDENT "FocusedDisplayId: %" PRId32 "\n", mFocusedDisplayId);
DispatcherEnabled 必须为1,并且DispatcherFrozen 必须为0,如果是inputDispatcher状态有问题,需要在代码中查看哪些地方有修改inputDispatcher的状态mDispatchEnabled,mDispatchFrozen,找到将修改状态的地方来分析问题。如果打印出来的FocusedDisplayId或FocusedApplications不符合预期,那就是display or WMS相关问题,与input流程没有关系。
Step5. 查看最终input消费event的是哪个页面
/frameworks/base/core/java/android/view/View.java
public boolean dispatchKeyEvent(KeyEvent event) {
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onKeyEvent(event, 0);
}
Log.i(">_<!!","dispatchKeyEvent event:" + event + " to :" + v);
// Give any attached key listener a first crack at the event.
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnKeyListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnKeyListener.onKey(this, event.getKeyCode(), event)) {
//表明input被消费了
Log.i(">_<!!","Event:" + event+ " handle in: " + v
+ " ,ListenerInfo = " + li.toString());
return true;
}
if (event.dispatch(this, mAttachInfo != null
? mAttachInfo.mKeyDispatchState : null, this)) {
return true;
}
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
return false;
}
这里的log可以表明input event正在按照view的层级依次dispatch并最终被哪个view消费,如果这个view并不是所期望的view,那么就需要查看为什么消费到这个view上面了,是layout区域有透明边界?还是期望的view并不存在,可能性就很多,细节可以再深思下。如果这里的view是符合期望的,那么问题就回到应用层了,看应用层对此input事件的响应是否有异常。
来源:https://juejin.cn/post/7168870464301826055


猜你喜欢
- 1. 背景从JDK1.5开始,Java支持个数可变的形参,类似:public class ParamDemo { public static
- 背景:SpringMVC如何响应json格式的数据?技术实现方式1:在Controller使用@RestController注解方式2:在C
- 【诞生背景】最近在做某配置中心的时候,配置中心采用properties格式进行配置的(如下图)。而我们工程的项目配置文件是yml格式的(如下
- 1、什么是Mybatis?MyBatis是一个优秀的持久层框架,是一个半ORM(对象关系映射)框架,它对jdbc的操作数据库的过程进行封装,
- 本文实例为大家分享了Java实现多任务执行助手的具体代码,供大家参考,具体内容如下1.多线程执行任务类package com.visy.th
- 上一节我们了解了Lock接口的一些简单的说明,知道Lock锁的常用形式,那么这节我们正式开始进入JUC锁(java.util.concurr
- 简介:顺序一致性内存模型是一个理论参考模型,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照。1、数据竞争和顺序一致性当
- Java线程同步属于Java多线程与并发编程的核心点,需要重点掌握,下面我就来详解Java线程同步的4种主要的实现方式什么是Java线程同步
- 却被编译器提示说:警告 1“System.Configuration.ConfigurationSettings.AppSettings”已
- 点击窗体任意位置移动窗体:需要添加命名空间:using System.Runtime.InteropServices;private con
- 话说RecyclerView已经面市很久,也在很多应用中得到广泛的使用,在整个开发者圈子里面也拥有很不错的口碑,那说明RecyclerVie
- 本教程为大家分享了学籍管理系统的具体java代码,供大家参考,具体内容如下1.需求分析 1.1系统功能设计 (1)能够查询学生的基本信息,如
- HttpServletResponse.sendRedirect与RequestDispatcher.forward方法都可以实
- 一、token与cookie相比较的优势1、支持跨域访问,将token置于请求头中,而cookie是不支持跨域访问的;2、无状态化,服务端无
- SpringBoot2.3.1版本源码一、SpringBoot启动的时候加载主配置类,通过@EnableAutoConfiguration注解
- 前言在日常开发过程中,静态变量和 静态方法 是我们常见的用法,Java中相信大家并不陌生了,那么在 Kotlin 中该如何使用呢
- java沙箱环境测试支付宝支付接口?准备工作,登陆支付宝开放平台,进入沙箱环境开放平台链接:https://developers.alipa
- 业务场景:调用同步接口获取当前全部有效账户,数据库已存在部分账户信息,因此需要筛选同步接口中已存在本地的帐户。调用接口获取的数据集合List
- 一、File流概念 JAVA中针对文件的读写操作设置了一系列的流,其
- spring boot 秉承约定优于配置,spring boot在静态资源的处理上就已经默认做了处理。1.默认资源映射映射”/**”的路径到