欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

深入解析Android中的事件传递

程序员文章站 2023-12-15 22:52:52
前言 前段时间工作中遇到了一个问题,即在软键盘弹出后想监听back事件,但是在activity中重写了对应的onkeydown函数却怎么也监听不到,经过一阵google之...

前言

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

上一篇:

下一篇: