淺談Android之Activity觸控事件傳輸機制介紹
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的處理都比較簡單,大家基於上面的分析自行看原始碼吧,這裡就不做分析了
至此,觸控事件的傳輸機制介紹完畢