深入解析Android中的事件传递
前言
前段时间工作中遇到了一个问题,即在软键盘弹出后想监听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中的事件分发已经有了一定的了解,希望本文的内容对大家的学习或者工作能带来一定的帮助,谢谢大家对的支持。