1. 程式人生 > >淺談Android之Activity觸控事件傳輸機制介紹

淺談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的處理都比較簡單,大家基於上面的分析自行看原始碼吧,這裡就不做分析了

至此,觸控事件的傳輸機制介紹完畢