8 Activity触摸事件传输机制介绍
当我们触摸屏幕的时候,程序会收到对应的触摸事件,这个事件是在app端去读取的吗?肯定不是,如果app能读取,那会乱套的,所以app不会有这个权限,系统按键的读取以及分发都是通过WindowManagerService来完成
在WMS中,它的管理单位是WindowState,当你点击屏幕时,它会根据Z-Order顺序找到top & focus WindowState来handle这个事件,然后再跨进程传给App端对应的Window, App端Window对应的代码主体就是ViewRootImpl
接下去我们来看看ViewRootImpl是如何接收WMS发来的事件以及发送到对应的Décor view的
8.1 触摸事件数据如何跨进程传输
触摸事件从WMS跨进程传给App端,跨进程通讯方式采用的是基于socketpair双工通讯,可能大家会问,Android进程间通讯不都是基于Binder来传输的吗?为什么不用binder?
让我们回顾下Binder的优势:
1) 支持RPC,也就是说我们可以很方便实现复杂的数据交互指令
2) 内存拷贝次数会比socket等方式少
但是考虑到触摸事件数据是非常小的,而且就是简单的数据传输,不需要RPC操作,这个时候如果采用binder,基本上就不存在什么优势,可能效率还赶不上socketpair双工通讯,因为数据量太小,内存拷贝次数减少的优势基本可以忽略,而且支持RPC还需要额外的开销
还有更重要的原因,就是socket Pair返回的是file descriptor,这样就意味着,可以共用主线程Looper对其file descriptor的状态进行监听,具体细节下面会介绍
socketPair是基于c++的,Android实现InputChannel对这部分代码进行封装
创建过程如下:
InputChannel[] inputChannels = InputChannel.openInputChannelPair(name); |
openInputChannelPair最终会调用native函数,通过jni调用c++代码创建socketpair,然后基于新创建的socket file descriptor对来创建InputChannel对并返回到Java层,由于
InputChannel是parcelable的,接下去只需要把其中一个InputChannel通过Binder传到另一进程,另外进程拿到后,二者就可以基于InputChannel进行数据共享了
但是InputChannel只是利用JNI基于C++对file descriptor和数据的读取操作进行封装,所以还需要封装一个类用于对InputChannel 对应的filedescriptor 进行监听,并在
file descriptor ready的时候,及时触发InputChannel的读取操作并将数据通过JNI回调到Java层(发送在WMS端,如果扩展开,篇幅太大,这边就不做介绍了)
这个类是WindowInputEventReceiver,它派生自InputEventReceiver,详细的下面介绍
接着看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(); res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,mInputChannel); } catch (RemoteException e) { …… } finally { if (restore) { attrs.restore(); } } …… if (mInputChannel != null) { if (mInputQueueCallback != null) { mInputQueue = new InputQueue(); mInputQueueCallback.onInputQueueCreated(mInputQueue); } mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper()); } …… // Set up the input pipeline. CharSequence counterSuffix = attrs.getTitle(); 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; mPendingInputEventQueueLengthCounterName = "aq:pending:" + counterSuffix; } } } |
首先调用new InputChannel创建InputChannel对象并保存到mInputChannel,不过目前这个对象是不包含实质的File Descriptor的
接着调用addToDisplay传入mInputChannel,WMS在创建WindowSate的同时会对应的创建InputChannelPair,然后将其中一个InputChannel保存到mInputChannel返回,至此,
mInputChannel才真正包含了跟WMS中对应WindowState中保存的InputChannel相关联的File Descriptor
接下去调用:
mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper()); |
WindowInputEventReceiver构造时传入两个参数,一个是InputChannel,另外一个是当前线程,也就是主线程的Looper
InputChannel包含要监听的File descriptor,那Looper当然就是用于File descriptor的监听了,
Looper原理是在构造的时候,创建epoll和pipe描述符,并且通过对pipe描述符的监听以及设置监听超时时间来触发从messagequeue中获取queue item数据并回调到handler,所以,通过Looper来监听并触发InputChannel读取没任何问题(详细可以看Looper的源码,这里不做过多介绍了)
那还有个疑问,为什么要用主线程的Looper,而不是新创建一个呢?大家记得ANR吗?
当你连续触摸屏幕的时候,如果按键事件超过一定时间没被处理,系统会弹出对话框,显示App无响应
翻译成代码逻辑就是,WMS往App当前显示的窗口对应的WindowState中包含的InputChannel写入了按键数据,然后由于App端的Looper在处理当次回调时存在耗时操作,从而导致Looper的下一次pollOnce被延后执行,进而导致App过晚监听到InputChannel的file descriptor的状态改变,影响了对InputChannel中按键数据的及时读取并下发
这就是采用主线程Looper的原因
数据读取到后,最终会通过Jni回调到Java类InputEventReceiver的如下函数:
@SuppressWarnings("unused") private void dispatchInputEvent(int seq, InputEvent event) { mSeqMap.put(event.getSequenceNumber(), seq); onInputEvent(event); } |
接着调用WindowInputEventReceiver的onInputEvent
最后用一句话总结下,WMS端通过WindowState关联的InputChannel发送按键数据后,App端的ViewRootImpl内的WindowInputEventReceiver实例对应的onInputEvent函数会被回调,参数即为按键数据
8.2 App收到触摸事件如何传到DecorView
在App端收到onInputEvent后,接下去数据的传递就是按函数顺序调用,接着直接基于代码来分析:
//WindowInputEventReceiver public void onInputEvent(InputEvent event) { enqueueInputEvent(event, this, 0, true); } |
接着调用enqueueInputEvent:
//ViewRootImpl void enqueueInputEvent(InputEvent event, InputEventReceiver receiver, int flags, boolean processImmediately) { 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(); } } |
将新event添加到InputEvent queue,由于processImmediately为true,接着调用
doProcessInputEvents:
//ViewRootImpl void doProcessInputEvents() { while (mPendingInputEventHead != null) { QueuedInputEvent q = mPendingInputEventHead; mPendingInputEventHead = q.mNext; if (mPendingInputEventHead == null) { mPendingInputEventTail = null; } q.mNext = null;
mPendingInputEventCount -= 1; ...... deliverInputEvent(q); } …… } |
循环从Input event queue中取出event然后调用deliverInputEvent:
//ViewRootImpl private void deliverInputEvent(QueuedInputEvent q) { …… InputStage stage; if (q.shouldSendToSynthesizer()) { stage = mSyntheticInputStage; } else { stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage; }
if (stage != null) { stage.deliver(q); } else { finishInputEvent(q); } } |
先介绍下InputStage,其实它是一个Input process chain,每一个InputStage构造时,需要传入其next process InputStage,接着在每一个InputStage的deliver被调用时,都有权决定是否要将event继续传递给next input stage
mFirstPostImeInputStage和mFirstInputStage这两个InputStagechain的是在setView函数执行到最后创建的,详细的可以回过头看上面的setView代码,这两个chain的last InputStage都是ViewPostImeInputStage,这里我们先忽略IME相关的一大堆InputStage,假定Input Event最终都传递到了ViewPostImeInputStage,接着看其onProcess函数:
// ViewPostImeInputStage protected int onProcess(QueuedInputEvent q) { if (q.mEvent instanceof KeyEvent) { return processKeyEvent(q); } else { // If delivering a new non-key event, make sure the window is // now allowed to start updating. handleDispatchDoneAnimating(); final int source = q.mEvent.getSource(); if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) { return processPointerEvent(q); } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) { return processTrackballEvent(q); } else { return processGenericMotionEvent(q); } } } |
接着看processPointerEvent:
private int processPointerEvent(QueuedInputEvent q) { final MotionEvent event = (MotionEvent)q.mEvent; …… boolean handled = mView.dispatchPointerEvent(event); …… return handled ? FINISH_HANDLED : FORWARD; } |
直接调用Décor View的dispatchPointerEvent,由于Décor View以及ViewGroup未实现该函数,所以默认跑到View.dispatchPointerEvent:
//View public final boolean dispatchPointerEvent(MotionEvent event) { if (event.isTouchEvent()) { return dispatchTouchEvent(event); } else { return dispatchGenericMotionEvent(event); } } |
接着调用DecorView的dispatchTouchEvent:
//PhoneWindow.DecorView public boolean dispatchTouchEvent(MotionEvent ev) { final Callback cb = getCallback(); return cb != null && !isDestroyed() && mFeatureId < 0 ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev); } |
先拿到PhoneWindow设置的callback,之前在介绍Activity初始化的时候说过,
在Activity.attach时创建PhoneWindow的时候会将Activity设置为PhoneWindow的callback,所以这里的cb肯定不为null,接着调用Activity的dispatchTouchEvent:
//Activity public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); } |
到这里可以看出:
1) Activity. dispatchTouchEvent是Activity事件分发的总入口,我们可以在自定义
Activity中重新实现该函数,即可达到对指定事件的预处理或者截获
2) 调用getWindow().superDispatchTouchEvent(ev)来实现事件在DecorView的分发
3) 如果DecorView没有处理这个事件,则调用Activity.onTouchEvent作默认处理
getWindow().superDispatchTouchEvent(ev)直接调用mDecor.superDispatchTouchEvent:
//PhoneWindow.DecorView public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); } |
直接调用super. dispatchTouchEvent,也就是FrameLayout. dispatchTouchEvent来开始事件在
View中的分发,接下去介绍这一块的分发规则
8.3 触摸事件在DecorView中的分发规则
Android的Touch Event主要分四种类型:
1) ACTION_DOWN
当用户手指按压屏幕会产生,这里就称它为前置目标锁定事件,当这个事件传递到
Decor View时,其必须要根据事件对应的坐标来锁定一个target view来处理后续事件
2) ACTION_UP
用户手指抬起时会产生该事件,也就是上面所说的手续事件之一
3) ACTION_MOVE
用户在屏幕移动手指时产生的事件,也是上面所说的后续事件之一
4) ACTION_CANCEL
当一个View在ACTION_DOWN时被判定为target view后,后续ACTION_UP和
ACTION_MOVE事件都会被发送到当前target view来处理,也就是说taretview在这个时候是独享当前事件的输入的,不过targetview的parent view,也就是说它爸爸,或者它爸爸的爸爸,反正比它大的直系ViewGroup都可以在其onInterceptTouchEvent被调用时返回true完成处理权的剥夺,剥夺完成后,当前targetview会收到
ACTION_CANCEL被告知你的事件处理权被取消了,然后刚刚完成剥夺的它爹或爷爷会被设置成新的target view,用以接收后续的ACTION_UP和ACTION_MOVE事件,还有就是这种剥夺是不可逆的,一旦完成对处理权的剥夺,就无法还回去
接下去用一个简单的列子来介绍Décor View在收到ACTION_DOWN事件时是如何锁定targetview的
先看图:
假定DecorView有一个child view叫ViewGroup1,然后ViewGroup1有两个child view分别叫child view1和child view2
我们假定用户手指按在child view2和child view1的重叠区域内,这个时候DecorView会收到类型为ACTION_DOWN的触摸事件,接下去将这个ACTION_DOWN分发下去用以锁定target view,流程是这样的:
1) Décor View会根据用户点击区域来判定点击在哪个子View上,这里当然是ViewGroup1
2) ViewGroup1同样的,也是通过点击区域来判定目标子View,不同的是,这里child View1和child view2都符合要求,那谁先来处理ACTION_DOWN事件,当然是谁在上面谁先处理,即后添加的child view会先享有处理权,也就说,ChildView2会先处理;如果Child View2返回true,那它就会被设置为target view,反之就继续传给Child View1处理,如果Child View1返回true,锁定结束,否则就只能继续传给其parentview(ViewGroup1)处理了,因为同级已经不存在符合要求的childview了,如果ViewGroup1也是返回false,那就继续往上传,直到找到处理该事件的View为止,如果到达Décor View了都没有被处理,那最终只能调用Activity.onTouchEvent做默认处理了
当一个child view通过ACTION_DOWN被设置为target view后,后续它这个target view被取消,只有两种情况:
1) 收到ACTION_UP事件,用户当次触摸结束
2) 上面说过,其parent ViewGroup在处理后续事件时,在onInterceptTouchEvent被调用时返回true完成target view的切换
所以,如果同级child view存在重叠区域,当用户点击这个重叠区域时,最上面的child view返回true告知其处理了这个ACTION_DOWN事件,那么重叠区域下面所有的childview都是无法收到任何后续事件的
我们都知道Android视图是一个树形结构,所以对于在这个树形结构中的每一个ViewGroup节点来说,它只要保存它的目标childview就可以了,这样数据就能从根节点(DecorView)一级一级的传到最终的target view
因此,从代码的角度,我们其实只需要分析ViewGroup是如何确定其direct target child view来处理数据的,就可以以此来推出整个View Tree的数据传递过程了
由于分发逻辑主要在ViewGroup.dispatchTouchEvent中,接下去就基于这个函数来分析,先分析ACTION_DOWN事件是如何确定directtarget child view的 :
//ViewGroup public boolean dispatchTouchEvent(MotionEvent ev) { …… boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); }
final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; } …… // Check for cancelation. final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
// Update list of touch targets for pointer down, if needed. final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { …… if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); // always 0 for down final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they // have become out of sync. removePointersFromTouchTargets(idBitsToAssign);
final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final float x = ev.getX(actionIndex); final float y = ev.getY(actionIndex);
final ArrayList<View> preorderedList = buildOrderedChildList(); final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled(); final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { …… if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; }
newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; }
resetCancelNextUpFlag(child); if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { …… mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } …… }
if (newTouchTarget == null && mFirstTouchTarget != null) { // Did not find a child to receive the event. // Assign the pointer to the least recently added target. newTouchTarget = mFirstTouchTarget; while (newTouchTarget.next != null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; } } }
// Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { …… } predecessor = target; target = next; } } …… return handled; } |
ViewGroup用一个链表保存target view,链表头保存到mFirstTouchTarget,其实只对于
ACTION_DOWN事件来说,链表只会存在一个数据,也就是target child view
在函数一开始,判定如果是ACTION_DOWN事件,则将重置状态,包括将清除target view链表,并将mFirstTouchTarget置空等
接着调用ViewGroup的onInterceptTouchEvent看其是否要截断ACTION_DOWN的传输,这里假定不截断,也就是返回false,intercepted为false
然后从后向前遍历所有child view,通过isTransformedTouchPointInView判断点击是否在这个child view内,如果在,则调用dispatchTransformedTouchEvent将事件传给该child view处理,如果返回true,说明被这个child view处理了,然后addTouchTarget将这个child view保存为
mFirstTouchTarget并跳出循环;如果返回false,则继续调用下一个child view按相同方式进行处理
接下去判断mFirstTouchTarget是否为null,如果为null,说明ViewGroup要么没有child view,要么所有的child view都没有处理ACTION_DOWN事件,接着调用ViewGroup自身的
dispatchTransformedTouchEvent进行处理
如果mFirstTouchTarget不为null,说明有child view处理了ACTION_DOWN事件,接着遍历
mFirstTouchTarget链表依次进行事件分发,由于ACTION_DOWN事件上面已经分发过,
这里alreadyDispatchedToNewTouchTarget和target == newTarget都为true,不做任何处理,直接将handled置为true
mFirstTouchTarget即为ACTION_DOWN最终锁定的target view,至于其他ACTION_UP和
ACTION_MOVE还有ACTION_CANCEL的处理都比较简单,大家基于上面的分析自行看源码吧,这里就不做分析了
至此,触摸事件的传输机制介绍完毕