前言

前段时间工作中遇到了一个问题,即在软键盘弹出后想监听back事件,但是在Activity中重写了对应的onKeyDown函数却怎么也监听不到,经过一阵Google之后才发现需要重写View的dispatchKeyEventPreIme函数才行。当时就觉得这个函数名字很熟悉,仔细思索一番以后才恍然大悟,当初看WMS源码的时候有过这方面的了解,现在却把它忘到了九霄云外,于是决定写这篇文章,权当记录。
InputManagerService
首先我们知道,不论是“键盘事件”还是“点击事件”,都是系统底层传给我们的,当然这里最底层的Linux Kernel我们不去讨论,我们的起点从Framework层开始。有看过Android Framework层源码的同学已经比较清楚,其中存在非常多的XXXManagerService,它们运行在system_server进程中,著名的如AMS(ActivityManagerService)和WMS(WindowManagerService)等等。这里和Android事件相关的Service就是InputManagerService,那么就先让我们看看它是如何进行工作的吧。
public void start() {
Slog.i(TAG, "Starting input manager");
nativeStart(mPtr);
........
}
看到这个nativeStart,是不是倒吸一口凉气,没错,是一个native方法。不过这也没办法,毕竟底层嘛,少不了和c打交道~
static void nativeStart(JNIEnv* env, jclass /* clazz */, jlong ptr) {
NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);
status_t result = im->getInputManager()->start();
if (result) {
jniThrowRuntimeException(env, "Input manager could not be started.");
}
}
可以看到,调用了InputManager的start方法。
status_t InputManager::start() {
status_t result = mDispatcherThread->run("InputDispatcher", PRIORITY_URGENT_DISPLAY);
if (result) {
ALOGE("Could not start InputDispatcher thread due to error %d.", result);
return result;
}
result = mReaderThread->run("InputReader", PRIORITY_URGENT_DISPLAY);
if (result) {
ALOGE("Could not start InputReader thread due to error %d.", result);
mDispatcherThread->requestExit();
return result;
}
return OK;
}
其中初始化了两个线程——ReaderThread和DispatcherThread。这两个线程的作用非常重要,前者接受来自设备的事件并且将其封装成上层看得懂的信息,后者负责把事件分发出去。可以说,我们上层的Activity或者是View的事件,都是来自于这两个线程。这里我不展开讲了,有兴趣的同学可以自行根据源码进行分析。有趣的是,DispatcherThread在轮询点击事件的过程中,采用的Looper的形式,可见Android中的源码真的是处处相关联,所以不要觉得某一部分的源码看了没用,说不定以后你就会用到了。
ViewRootImpl
从前一小节我们得知,设备的点击事件是通过InputManagerService来进行传递的,其中存在两个线程一个用于处理,一个用于分发,那么事件分发到哪里去呢?直接发到Activity或者View中吗?这显然是不合理的,所以Framework层中存在一个ViewRootImpl类,作为两者沟通的桥梁。需要注意的是,该类在老版本的源码中名为ViewRoot。
ViewRootImpl这个类是在Activity的resume生命周期中初始化的,调用了ViewRootImpl.setView函数,下面让我们看看这个函数做了什么。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
mView = view;
..........
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
//Attention here!!!
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mInputChannel = null;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
throw new RuntimeException("Adding window failed", e);
} finally {
if (restore) {
attrs.restore();
}
}
............
if (mInputChannel != null) {
if (mInputQueueCallback != null) {
mInputQueue = new InputQueue();
mInputQueueCallback.onInputQueueCreated(mInputQueue);
}
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
Looper.myLooper());
}
}
}
}
这个方法非常的长,我先截取了一小段,看我标注的[Attention here],创建了一个InputChannel的实例,并且通过mWindowSession.addToDisplay方法将其添加到了mWindowSession中。
final IWindowSession mWindowSession;
public ViewRootImpl(Context context, Display display) {
mContext = context;
mWindowSession = WindowManagerGlobal.getWindowSession();
......
}
public static IWindowSession getWindowSession() {
synchronized (WindowManagerGlobal.class) {
if (sWindowSession == null) {
try {
InputMethodManager imm = InputMethodManager.getInstance();
IWindowManager windowManager = getWindowManagerService();
sWindowSession = windowManager.openSession(
new IWindowSessionCallback.Stub() {
@Override
public void onAnimatorScaleChanged(float scale) {
ValueAnimator.setDurationScale(scale);
}
},
imm.getClient(), imm.getInputContext());
} catch (RemoteException e) {
Log.e(TAG, "Failed to open window session", e);
}
}
return sWindowSession;
}
}
mWindowSession是什么呢?通过上面的代码我们可以知道,mWindowSession就是WindowManagerService中的一个内部实例。getWindowManagerService拿到的事WindowManagerNative的proxy对象,所以由此我们可以知道,mWindowSession也是用来IPC的。
如果大家对上面一段话不是很了解,换句话说不了解Android的Binder机制的话,可以先去自行了结一下。
回到上面的setView函数,mWindowSession.addToDisplay方法肯定调用的是对应remote的addToDisplay方法,其中会调用WindowManagerService::addWindow方法去将InputChannel注册到WMS中。
看到这里大家可能会有疑问,第一小节说的是InputManagerService管理设备的事件,怎么到了这一小节就变成了和WindowManagerService打交道呢?秘密其实就在mWindowSession.addToDisplay方法中。
WindowManagerService
public int addWindow(Session session, IWindow client, int seq,
WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
InputChannel outInputChannel) {
..........
if (outInputChannel != null && (attrs.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
String name = win.makeInputChannelName();
InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
win.setInputChannel(inputChannels[0]);
inputChannels[1].transferTo(outInputChannel);
mInputManager.registerInputChannel(win.mInputChannel, win.mInputWindowHandle);
}
...........
}
可以看到在addWindow方法中,创建了一个InputChannel的数组,数组中有两个InputChannel,第一个是remote端的,通过 mInputManager.registerInputChannel方法讲其注册到InputManager中;第二个是native端的,通过inputChannels[1].transferTo(outInputChannel)方法,将其指向outInputChannel,而outInputChannel就是前面setView传过来的那个InputChannel,也就是ViewRootImpl里的。
通过这一段代码,我们知道,当Activity初始化的时候,我们就会在WMS中注册两个InputChannel,remote端的InputChannel注册到InputManager中,用于接受ReaderThread和DispatcherThread传递过来的信息,native端的InputChannel指向ViewRootImpl中的InputChannel,用于接受remote端的InputChannel传递过来的信息。
最后,回到ViewRootImpl的setView方法的最后,有这么一句:
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper());
WindowInputEventReceiver,就是我们最终接受事件的接收器了。
键盘事件的传递
下面让我们看看WindowInputEventReceiver做了什么。
final class WindowInputEventReceiver extends InputEventReceiver {
public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
super(inputChannel, looper);
}
@Override
public void onInputEvent(InputEvent event) {
enqueueInputEvent(event, this, 0, true);
}
@Override
public void onBatchedInputEventPending() {
if (mUnbufferedInputDispatch) {
super.onBatchedInputEventPending();
} else {
scheduleConsumeBatchedInput();
}
}
@Override
public void dispose() {
unscheduleConsumeBatchedInput();
super.dispose();
}
}
很简单,回调到onInputEvent函数的时候,就调用ViewRootImpl的enqueueInputEvent函数。
void enqueueInputEvent(InputEvent event,
InputEventReceiver receiver, int flags, boolean processImmediately) {
.........
if (processImmediately) {
doProcessInputEvents();
} else {
scheduleProcessInputEvents();
}
}
可以看到,如果需要立即处理该事件,就直接调用doProcessInputEvents函数,否则调用scheduleProcessInputEvents函数加入调度。
这里我们一切从简,直接看doProcessInputEvents函数。
void doProcessInputEvents() {
........
deliverInputEvent(q);
........
}
private void deliverInputEvent(QueuedInputEvent q) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
q.mEvent.getSequenceNumber());
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
}
InputStage stage;
if (q.shouldSendToSynthesizer()) {
stage = mSyntheticInputStage;
} else {
stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
}
if (stage != null) {
stage.deliver(q);
} else {
finishInputEvent(q);
}
}
可以看到deliverInputEvent函数中,存在一个很有意思的东西叫InputStage,通过一些标记位去确定到底是用哪个InputStage去处理。
这些InputStage是在哪里初始化的呢?显示是在setView函数啦。
mSyntheticInputStage = new SyntheticInputStage(); InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage); InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage, "aq:native-post-ime:" + counterSuffix); InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage); InputStage imeStage = new ImeInputStage(earlyPostImeStage, "aq:ime:" + counterSuffix); InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage); InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage, "aq:native-pre-ime:" + counterSuffix); mFirstInputStage = nativePreImeStage; mFirstPostImeInputStage = earlyPostImeStage;
可以看到初始化了如此多的InputStage。这些stage的调用顺序是严格控制的,Ime的意思是输入法,所以大家应该了解这些preIme和postIme是什么意思了吧?
从上面得知,最终会调用InputStage的deliver函数:
public final void deliver(QueuedInputEvent q) {
if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
forward(q);
} else if (shouldDropInputEvent(q)) {
finish(q, false);
} else {
apply(q, onProcess(q));
}
}
其apply方法被各个子类重写的,下面我们以ViewPreImeInputStage为例:
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
}
return FORWARD;
}
private int processKeyEvent(QueuedInputEvent q) {
final KeyEvent event = (KeyEvent)q.mEvent;
if (mView.dispatchKeyEventPreIme(event)) {
return FINISH_HANDLED;
}
return FORWARD;
}
可以看到,会调用View的dispatchKeyEventPreIme方法。看到这里,文章最开头的那个问题也就迎刃而解了,为什么在输入法弹出的情况下,监听Activity的onKeyDown没有用呢?因为该事件被输入法消耗了,对应的,就是说走到了imeStage这个InputStage中;那为什么重写View的dispatchKeyEventPreIme方法就可以呢?因为它是在ViewPreImeInputStage中被调用的,还没有轮到imeStage呢~
总结
通过这样的一篇分析,相信大家对Android中的事件分发已经有了一定的了解,希望本文的内容对大家的学习或者工作能带来一定的帮助,谢谢大家对的支持。
# android
# 事件传递流程
# 点击事件传递
# android事件处理机制
# 详解Android的两种事件处理机制
# Android View 事件分发机制详解
# Android事件的分发机制详解
# 详细分析Android中onTouch事件传递机制
# 老生常谈android中的事件传递和处理机制
# 的是
# 可以看到
# 是在
# 让我们
# 将其
# 重写
# 都是
# 我们可以
# 这两个
# 弹出
# 是一个
# 这一
# 还没有
# 我不
# 就在
# 看了
# 第一个
# 你就
# 一句
# 也就
相关文章:
如何使用Golang table-driven基准测试_多组数据测量函数效率
开心动漫网站制作软件下载,十分开心动画为何停播?
成都响应式网站开发,dw怎么把手机适应页面变成网页?
网站制作企业,网站的banner和导航栏是指什么?
香港服务器部署网站为何提示未备案?
建站之星安装步骤有哪些常见问题?
建站之星如何开启自定义404页面避免用户流失?
如何在橙子建站上传落地页?操作指南详解
如何用VPS主机快速搭建个人网站?
股票网站制作软件,网上股票怎么开户?
淘宝制作网站有哪些,淘宝网官网主页?
官网网站制作腾讯审核要多久,联想路由器newifi官网
建站之星好吗?新手能否轻松上手建站?
西安大型网站制作公司,西安招聘网站最好的是哪个?
最好的网站制作公司,网购哪个网站口碑最好,推荐几个?谢谢?
济南专业网站制作公司,济南信息工程学校怎么样?
北京的网站制作公司有哪些,哪个视频网站最好?
建站主机无法访问?如何排查域名与服务器问题
制作国外网站的软件,国外有哪些比较优质的网站推荐?
官网建站费用明细查询_企业建站套餐价格及收费标准指南
建站之星后台搭建步骤解析:模板选择与产品管理实操指南
专业商城网站制作公司有哪些,pi商城官网是哪个?
建站之星微信建站一键生成小程序+多端营销系统
武汉网站设计制作公司,武汉有哪些比较大的同城网站或论坛,就是里面都是武汉人的?
c++怎么使用类型萃取type_traits_c++ 模板元编程类型判断【方法】
如何选择建站程序?包含哪些必备功能与类型?
建站168自助建站系统:快速模板定制与SEO优化指南
哪家制作企业网站好,开办像阿里巴巴那样的网络公司和网站要怎么做?
潍坊网站制作公司有哪些,潍坊哪家招聘网站好?
上海网站制作网站建设公司,建筑电工证网上查询系统入口?
如何选择适配移动端的WAP自助建站平台?
建站为何优先选择香港服务器?
三星网站视频制作教程下载,三星w23网页如何全屏?
开封网站制作公司,网络用语开封是什么意思?
昆明高端网站制作公司,昆明公租房申请网上登录入口?
电脑免费海报制作网站推荐,招聘海报哪个网站多?
建站主机系统SEO优化与智能配置核心关键词操作指南
如何通过可视化优化提升建站效果?
山东网站制作公司有哪些,山东大源集团官网?
c# Task.ConfigureAwait(true) 在什么场景下是必须的
如何获取PHP WAP自助建站系统源码?
如何选择香港主机高效搭建外贸独立站?
如何在香港服务器上快速搭建免备案网站?
新网站制作渠道有哪些,跪求一个无线渠道比较强的小说网站,我要发表小说?
北京网站制作的公司有哪些,北京白云观官方网站?
如何在西部数码注册域名并快速搭建网站?
如何通过云梦建站系统实现SEO快速优化?
重庆网站制作公司哪家好,重庆中考招生办官方网站?
建站主机选哪种环境更利于SEO优化?
建站IDE高效指南:快速搭建+SEO优化+自适应模板全解析
*请认真填写需求信息,我们会在24小时内与您取得联系。