前言
聊到事件分发,很多朋友就会想到view的dispatchTouchEvent
,其实在此之前,Android还做了很多工作。
比如跨进程获取输入事件的方式?在dispatchTouchEvent
责任链之前还有一条InputStage
责任链?DecorView,PhoneWindow
之间的传递顺序?
包括事件分发过程中事件序列的处理方式?ViewGroup和View之间的协调?等等。
这一切,都要从你可爱的小拇指
说起...
当你的拇指触碰手机的那一刹那,手机就被你深深的影响了,没错,手机会收到你给他布置的任务。
这个任务可以是:
- 滑动界面任务
- 点击按钮任务
- 长按任务
等等,总之,你向手机传递了这个任务信息,接下来就是手机的处理任务时间。
我们可以假设手机系统就是一个大的公司(Android公司)
,而我们触摸手机的任务就是一个完整的项目需求,今天就和大家一起深入Android公司内部,打探事件分发的那些私密。
在此之前,我也列出了问题和大纲:
硬件部门和内核部门
首先,我的拇指找到了Android
公司,说出了自己的需求,比如:点击某个View并滑动到另外的位置。
Android
公司首先会派出硬件部门,和我的小拇指进行会谈,接收到我的需求之后,硬件部门生成简单的终端,并传递给内核部门。
内核部门将任务进行加工,生成了内部事件——event,并添加到公司内部的一个管理系统/dev/input/
目录下。
这样做的目的是把外来的需求转化成内部通用,都能看懂的任务。
任务处理部门(SystemServer进程)
当任务记录在公司管理系统上后,就会有专门的任务处理部门对这些任务进行处理,他们做的事情就是一直监听/dev/input/
目录,当发现有新的事件就会进行处理。
那这个任务处理部门到底是何方神圣呢?
不知道大家还记不记得在SystemServer
进程中启动了一系列系统有关的服务,比如AMS,PMS等等,其中还有一个不是很起眼的角色,叫做InputManagerService
。
这个服务就是用来负责与硬件通信,接受屏幕输入事件。
在其内部,会启动一个读线程,也就是InputReader
,它会从这个管理系统也就是/dev/input/
目录拿到任务,并且分发给InputDispatcher
线程,然后进行统一的事件分发调度。
分配给具体的项目组(InputChannel)
然后任务处理部门需要把任务交给 专业处理任务的项目组了,这就涉及到跨部门沟通了(跨进程通信)。
大家都知道跨部门沟通是个比较麻烦的事情,谁来完成这个事情呢?InputChannel
。
让我们回到ViewRootImpl
的setView
方法:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { //创建InputChannel mInputChannel = new InputChannel(); //通过Binder进入systemserver进程 res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets, mAttachInfo.mOutsets, mInputChannel); } }
在该方法中,创建了一个InputChannel
对象,并且通过Binder进入systemserver进程,最终形成socket的客户端。
这里涉及到socket通信的知识,比较重要的就是c层的socketpair
方法。
socketpair()函数用于创建一对无名的、相互连接的套接子。如果函数成功,则返回0,创建好的套接字分别是sv[0]和sv[1];这对套接字可以用于全双工通信,每一个套接字既可以读也可以写。
通过这个方法,就生成了socket通信的客户端和服务端:
- socket服务端保存到system_server中的WindowState的mInputChannel;
- socket客户端通过binder传回到远程进程的UI主线程ViewRootImpl的mInputChannel;
感兴趣的可以看看gityuan
对于input分析的博客,文末有链接。
所以小结一下就是,在App进程创建了一个对象InputChannel
,通过Binder机制传入了SystemServer
进程,也就是WindowManagerService
中。然后在WindowManagerService
中创建了一对套接字用于进程间通信,而传过来的InputChannel
就指向了socket
的客户端。
然后App进程的主线程就会监听这个socket客户端,当收到消息(输出事件)后,回调NativeInputEventReceiver.handleEvent()
方法,最终会走到InputEventReceiver.dispachInputEvent
方法。
dispachInputEvent
,处理输入事件,感觉离我们熟知的事件分发比较近了。
没错,到此,任务已经分配到了具体的项目组,也就是我们所使用的具体APP中了。
小组中任务第一次分发(InputStage)
当任务到达了项目组,首先组内会对这个任务进行分发,这里会涉及到第一次责任链分发模式
。
为什么强调是第一次呢?因为还没有到达我们熟知的view事件分发阶段,在此之前,还会有一次事件分类的责任链分发工作,也就是InputStage
处理事件分发。
//InputEventReceiver.java private void dispatchInputEvent(int seq, InputEvent event) { mSeqMap.put(event.getSequenceNumber(), seq); onInputEvent(event); } //ViewRootImpl.java ::WindowInputEventReceiver final class WindowInputEventReceiver extends InputEventReceiver { public void onInputEvent(InputEvent event) { enqueueInputEvent(event, this, 0, true); } } //ViewRootImpl.java void enqueueInputEvent(InputEvent event, InputEventReceiver receiver, int flags, boolean processImmediately) { adjustInputEventForCompatibility(event); QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags); QueuedInputEvent last = mPendingInputEventTail; if (last == null) { mPendingInputEventHead = q; mPendingInputEventTail = q; } else { last.mNext = q; mPendingInputEventTail = q; } mPendingInputEventCount += 1; if (processImmediately) { doProcessInputEvents(); } else { scheduleProcessInputEvents(); } }
兜兜转转,没想到还是到了ViewRootImpl这里,所以ViewRootImpl
不仅负责了界面的绘制,也负责了事件分发的部分处理工作。
这里的enqueueInputEvent
方法中,有涉及到一个QueuedInputEvent
类,这个类就是一个封装了InputEvent的事件类,然后经过赋值调用到doProcessInputEvents
方法:
void doProcessInputEvents() { // Deliver all pending input events in the queue. while (mPendingInputEventHead != null) { QueuedInputEvent q = mPendingInputEventHead; mPendingInputEventHead = q.mNext; deliverInputEvent(q); } } private void deliverInputEvent(QueuedInputEvent q) { InputStage stage; if (stage != null) { stage.deliver(q); } else { finishInputEvent(q); } } abstract class InputStage { private final InputStage mNext; public InputStage(InputStage next) { mNext = next; } public final void deliver(QueuedInputEvent q) { apply(q, onProcess(q)); }
到这里逻辑好像慢慢清晰了,QueuedInputEvent
是一种输入事件,InputStage
是处理输入事件的责任链,next字段则表示责任链的下一个InputStage
。
那InputStage
到底干了哪些事情呢?返回到ViewRootImpl
的setView方法再看看:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { synchronized (this) { // Set up the input pipeline. 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; } }
可以看到在setView方法中,就把这条输入事件处理的责任链拼接完成了,不同的InputStage子类,通过构造方法一个个串联起来了,那这些InputStage到底干了啥呢?
SyntheticInputStage
。综合处理事件阶段,比如处理导航面板、操作杆等事件。ViewPostImeInputStage
。视图输入处理阶段,比如按键、手指触摸等运动事件,我们熟知的view事件分发就发生在这个阶段。NativePostImeInputStage
。本地方法处理阶段,主要构建了可延迟的队列。EarlyPostImeInputStage
。输入法早期处理阶段。ImeInputStage
。输入法事件处理阶段,处理输入法字符。ViewPreImeInputStage
。视图预处理输入法事件阶段,调用视图view的dispatchKeyEventPreIme方法。NativePreImeInputStage
。本地方法预处理输入法事件阶段。
小结一下,事件到达应用端的主线程,会通过ViewRootImpl进行一系列InputStage来处理事件。这个阶段其实是对事件进行一些简单的分类处理,比如视图输入事件,输入法事件,导航面板事件等等。
事件分发完成后,会告知SystemServer
进程的InputDispatcher
线程,最终将该事件移除,完成此次事件的分发消费。
我们的view手指触摸事件就是发生在ViewPostImeInputStage
阶段了,具体来看看:
final class ViewPostImeInputStage extends InputStage { @Override protected int onProcess(QueuedInputEvent q) { if (q.mEvent instanceof KeyEvent) { return processKeyEvent(q); } else { final int source = q.mEvent.getSource(); if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { return processPointerEvent(q); } } } private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; boolean handled = mView.dispatchPointerEvent(event) return handled ? FINISH_HANDLED : FORWARD; } //View.java public final boolean dispatchPointerEvent(MotionEvent event) { if (event.isTouchEvent()) { return dispatchTouchEvent(event); } else { return dispatchGenericMotionEvent(event); } }
经过一系列分发,最终会执行到mView的dispatchTouchEvent
方法,而这个mView就是DecorView,同样是在setView中进行赋值的,就不细说了。
至此,终于到了我们熟悉的环节,dispatchTouchEvent
方法。
大佬之间的任务整理(DecorView)
确定了任务的分类,接下来就开始组内任务讨论整理了,这个阶段发生在几个大佬之间的谈话,这几个大佬分别是DecorView、PhoneWindow、Activity/Dialog
:
//DecorView.java @Override public boolean dispatchTouchEvent(MotionEvent ev) { //cb其实就是对应的Activity/Dialog final Window.Callback cb = mWindow.getCallback(); return cb != null && !mWindow.isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); } //Activity.java public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); } //PhoneWindow.java @Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); } //DecorView.java public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
可以看到,从DecorView开始,事件依次经过了Activity、PhoneWindow、DecorView
。
有点奇怪哈,为啥是这样一个顺序呢?而不是直接ViewRootImpl交给Activity,再交给顶层View——DecorView?而是转来转去,缘起和从呢?
- 首先,为什么ViewRootImpl不直接把事件交给Activity?
因为界面上不止Activity
一种形态呀,如果界面上存在Dialog
,而Dialog的Window属于子Window,是可以覆盖应用级Window的,所以总不能把事件直接交给Activity吧?都被覆盖了,所以这时候应该把事件交给Dialog。
为了方便,我们用到了DecorView
这个角色来充当分发的第一元素,由他来找到当前界面window的所持着,所以代码中也是找到mWindow.getCallback()
,其实也就是对应的Activity或者Dialog。
- 其次,交给Acitivity后,为什么不直接交给顶层View——DecorView开始分发事件呢?
因为Activity
和DecorView
之间并没有直接关系。DecorView怎么来的?通过setContentView被创建出来的,所以在Activity中是看不到DecorView身影的,DecorView的实例保存在PhoneWindow中,由Window所管理。
所以Activity
的事件肯定是交给Window来管理,之前也说过PhoneWindow的指责就是帮助Activity管理View,所以事件分发交给它也是它的职责所在。而PhoneWindow
的处理方式,就是交给顶层的DecorView
来处理了。
这样,一个事件分发的链条就形成了:
DecorView——>Activity——>PhoneWindow——>DecorView——>ViewGroup
接下来就是View层级之间的事件分发,内容有点多,没赶完,所以下期见了。
总结
拇指记者的探访还在继续...
参考
https://wanandroid.com/wenda/show/12119
http://gityuan.com/2016/12/31/input-ipc/