Android 輸入事件一擼到底之源頭活水(1)

前言

Android 不只是展示靜態頁面,更多的是與用戶的交互。用戶通過觸摸屏幕與App互動,提供了更好的用戶體驗。而在App層我們需要接收屏幕的觸摸事件進行相應的邏輯操作,本系列文章將分析App層輸入事件兜兜轉轉的一生。
本系列分為三篇文章講述:

1、Android 輸入事件一擼到底之源頭活水(1)
2、Android 輸入事件一擼到底之DecorView攔路虎(2)
3、Android 輸入事件一擼到底之View接盤俠(3

image.png

通過本篇文章,你將了解到:

1、輸入事件從哪分發到App層
2、輸入事件分發責任鏈
3、Touch/Key事件處理
4、Root View 接收事件

輸入事件從哪分發到App層

輸入事件從底層輸入

屏幕觸摸產生的事件由底層處理,最后分發到對應的Window,我們需要找到底層與Window的橋梁。找到InputEventReceiver類:

InputEventReceiver.java
    private void dispatchInputEvent(int seq, InputEvent event) {
        mSeqMap.put(event.getSequenceNumber(), seq);
        //處理輸入事件
        onInputEvent(event);
    }

底層封裝輸入事件為:InputEvent。
InputEventReceiver 為抽象類,其子類為ViewRootImpl 內部類WindowInputEventReceiver:

WindowInputEventReceiver.java
    @Override
    public void onInputEvent(InputEvent event) {
        ...
        if (processedEvents != null) {
            ...
        } else {
            //加入輸入隊列
            enqueueInputEvent(event, this, 0, true);
        }
    }

onInputEvent 為重寫InputEventReceiver 方法。

App與底層建立聯系

上面分析了底層輸入事件最終傳送給了ViewRootImpl 里的WindowInputEventReceiver,底層怎么就知道傳給了它呢?
來看看ViewRootImpl類:

ViewRootImpl.java
    InputChannel mInputChannel;
    WindowInputEventReceiver mInputEventReceiver;
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        ...
        if ((mWindowAttributes.inputFeatures
                & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
            //建立輸入通道
            mInputChannel = new InputChannel();
        }
        try {
            ...
            //InputChannel與WindowManagerService建立聯系,也就是以后輸入事件發送給這個Window
            res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                    getHostVisibility(), mDisplay.getDisplayId(), mTmpFrame,
                    mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                    mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel,
                    mTempInsets);
            setFrame(mTmpFrame);
        } catch (RemoteException e) {
           ...
        }
        if (mInputChannel != null) {
            if (mInputQueueCallback != null) {
                mInputQueue = new InputQueue();
                mInputQueueCallback.onInputQueueCreated(mInputQueue);
            }
            //將輸入通道與接收端綁定起來
            //mInputEventReceiver 作為接收端,底層數據通過mInputChannel 發送接收端
            //當前Looper為主線程Looper,Looper里有MessageQueue
            mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                    Looper.myLooper());
        }
    }

來看看mWindowSession.addToDisplay(xx)方法:

Session.java
    @Override
    public int addToDisplay(IWindow window, int seq, WindowManager.LayoutParams attrs,
                            int viewVisibility, int displayId, Rect outFrame, Rect outContentInsets,
                            Rect outStableInsets, Rect outOutsets,
                            DisplayCutout.ParcelableWrapper outDisplayCutout, InputChannel outInputChannel,
                            InsetsState outInsetsState) {
        //mService 為WindowManagerService 對象
        return mService.addWindow(this, window, seq, attrs, viewVisibility, displayId, outFrame,
                outContentInsets, outStableInsets, outOutsets, outDisplayCutout, outInputChannel,
                outInsetsState);
    }

再來看看WindowManagerService addWindow 方法:

WindowManagerService.java
    public int addWindow(xx) {
        ...
        final boolean openInputChannels = (outInputChannel != null
                && (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);
        if  (openInputChannels) {
            win.openInputChannel(outInputChannel);
        }
        ...
    }

最終調用:

WindowState.java
    void openInputChannel(InputChannel outInputChannel) {
        ...
        //注冊輸入通道
        mWmService.mInputManager.registerInputChannel(mInputChannel, mClient.asBinder());
    }

注冊上層傳遞過來的channel,這樣子該channel與Window建立了聯系。
既然建立了聯系,再來看看如何channel如何出傳遞數據給上層呢?
來看InputEventReceiver構造函數:

InputEventReceiver.java
    public InputEventReceiver(InputChannel inputChannel, Looper looper) {
        ...
        mInputChannel = inputChannel;
        mMessageQueue = looper.getQueue();
        //將MessageQueue傳遞給底層
        //當底層有數據時將會發送到該隊列里
        //最終消息循環取出執行
        //調用InputEventReceiver 的dispatchInputEvent方法
        //因此執行 dispatchInputEvent 方法時已經在主線程
        mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
                inputChannel, mMessageQueue);

        mCloseGuard.open("dispose");
    }

Looper相關請移步:Android事件驅動Handler-Message-Looper解析

輸入事件分發責任鏈

既然輸入事件已經分發到App對應的Window上,那么來看看如何處理這些事件的,從WindowInputEventReceiver onInputEvent(xx)方法看起。該方法調用了ViewRootImpl 的enqueueInputEvent(xx)方法:

ViewRootImpl.java
    void enqueueInputEvent(InputEvent event,
                           InputEventReceiver receiver, int flags, boolean processImmediately) {
        //將InputEvent構造為QueuedInputEvent 對象
        //QueuedInputEvent 實際上就是封裝了InputEvent的鏈表,這里當做有頭指針和尾指針的隊列
        QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
        QueuedInputEvent last = mPendingInputEventTail;
        if (last == null) {
            //隊列為空,那么隊頭指針和隊尾指針指向同一個節點
            mPendingInputEventHead = q;
            mPendingInputEventTail = q;
        } else {
            //將節點插入隊尾
            last.mNext = q;
            //移動隊尾指針
            mPendingInputEventTail = q;
        }
        //隊列里節點個數計數+1
        mPendingInputEventCount += 1;
        if (processImmediately) {
            //立即處理
            doProcessInputEvents();
        } else {
            //延時處理,通過Handler發送
            scheduleProcessInputEvents();
        }
    }

    void doProcessInputEvents() {
        //while 循環 處理隊列中所有的節點
        while (mPendingInputEventHead != null) {
            //取出隊頭節點,并移動隊頭指針
            QueuedInputEvent q = mPendingInputEventHead;
            mPendingInputEventHead = q.mNext;
            if (mPendingInputEventHead == null) {
                mPendingInputEventTail = null;
            }
            q.mNext = null;

            mPendingInputEventCount -= 1;
            ...
            //繼續傳遞該節點
            deliverInputEvent(q);
        }
        ...
    }

    private void deliverInputEvent(QueuedInputEvent q) {
        ...
        //輸入責任鏈,此處運用了設計模式之一的:責任鏈
        //簡單說就是定義了一個接收者處理鏈,該鏈節點有先后順序,并且前一節點持有
        //下一個節點的引用,當前一個節點不處理請求
        //那么會分發給下一個節點
        InputStage stage;
        //確定輸入鏈的開始位置,也就是從鏈中某個節點開始
        if (q.shouldSendToSynthesizer()) {
            stage = mSyntheticInputStage;
        } else {
            //touch事件走mFirstPostImeInputStage
            //key事件走mFirstInputStage
            stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
        }
        ...
        if (stage != null) {
            handleWindowFocusChanged();
            //責任鏈處理事件
            stage.deliver(q);
        } else {
            finishInputEvent(q);
        }
    }

上面提到了責任鏈,輸入事件交給了責任鏈處理,那么責任鏈在哪建立的,它有哪些節點呢?
首先來看看InputStage,該類為抽象類:

    abstract class InputStage {
        //記錄下一個節點
        private final InputStage mNext;
        //節點處理的狀態
        //需要轉發給下一個節點
        protected static final int FORWARD = 0;
        //當前節點已處理完成
        protected static final int FINISH_HANDLED = 1;
        //當前節點標記完成,但未處理
        protected static final int FINISH_NOT_HANDLED = 2;
        public InputStage(InputStage next) {
            //每次構造時都指定其后一個節點
            mNext = next;
        }
        //一些處理方法,有些被子類重寫
    }

繼續來分析InputStage 子類,還是回頭看看ViewRootImpl setView(xx)方法:

    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                ...
                // Set up the input pipeline.
                CharSequence counterSuffix = attrs.getTitle();
                //定義了責任鏈節點
                mSyntheticInputStage = new SyntheticInputStage();
                //輸入法之后的View處理,下一個節點指向mSyntheticInputStage
                InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
                //輸入法之后的本地處理,下一個節點指向viewPostImeStage
                InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
                        "aq:native-post-ime:" + counterSuffix);
                //早些的輸入法之后的處理,下一個節點指向nativePostImeStage
                InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
                //輸入法處理,下一個節點指向earlyPostImeStage
                InputStage imeStage = new ImeInputStage(earlyPostImeStage,
                        "aq:ime:" + counterSuffix);
                //輸入法之前的View處理,下一個節點指向imeStage
                InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
                //輸入法之前的本地處理,下一個節點指向viewPreImeStage
                InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
                        "aq:native-pre-ime:" + counterSuffix);
                
                //記錄節點到成員變量里
                mFirstInputStage = nativePreImeStage;
                mFirstPostImeInputStage = earlyPostImeStage;
            }
        }
    }

可以看出定義責任鏈時,其順序已經指定了,用圖表示:


image.png

責任鏈已經建立完畢,接下來看具體的責任鏈里節點處理。

Touch/Key事件處理

當前我們常接觸到的事件分為兩類,Touch(觸摸事件)、Key(按鍵事件),相對應的InputEvent.java 有兩個子類:


image.png

在確定責任鏈開始位置時:

stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;

Touch事件選擇EarlyPostImeInputStage
Key事件選擇NativePreImeInputStage

經過責任鏈節點流轉,最終交由責任鏈節點:ViewPostImeInputStage 處理

ViewPostImeInputStage
    protected int onProcess(QueuedInputEvent q) {
        //key 事件處理
        if (q.mEvent instanceof KeyEvent) {
            return processKeyEvent(q);
        } else {
            final int source = q.mEvent.getSource();
            if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
                //Touch 事件處理
                return processPointerEvent(q);
            } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
                return processTrackballEvent(q);
            } else {
                return processGenericMotionEvent(q);
            }
        }
    }

可以看出,Touch/Key 事件在此處分流了。
processKeyEvent(xx)

    private int processKeyEvent(QueuedInputEvent q) {
        final KeyEvent event = (KeyEvent)q.mEvent;
        ...
        //mView 為ViewRootImpl 持有的View,表示該Window的Root View
        //此處將KeyEvent 交給Root View處理
        if (mView.dispatchKeyEvent(event)) {
            return FINISH_HANDLED;
        }
        ...
    }

processPointerEvent(xx)

    private int processPointerEvent(QueuedInputEvent q) {
        //將InputEvent 轉為MotionEvent
        final MotionEvent event = (MotionEvent)q.mEvent;
        mAttachInfo.mUnbufferedDispatchRequested = false;
        mAttachInfo.mHandlingPointerEvent = true;
        //mView 為ViewRootImpl 持有的View,表示該Window的root View
        //此處將MotionEvent 交給root View處理
        boolean handled = mView.dispatchPointerEvent(event);
        ...
        return handled ? FINISH_HANDLED : FORWARD;
    }

不論是KeyEvent還是MotionEvent,都交給了Window Root View處理,分別調用了View dispatchKeyEvent、dispatchPointerEvent 方法,那Root View從哪來的呢?

Root View 接收事件

在之前的文章:Window/WindowManager 不可不知之事
提到過View需要添加到Window里才能展示出來,而添加View到Window的方法:

public void addView(View view, ViewGroup.LayoutParams params);

當使用WindowManager.addView(View view, ViewGroup.LayoutParams params)時,該view即是Window的Root View。
來看看一些常用的Root View
Activity Root View
Activity 使用DecorView 作為Root View
Dialog Root View
Dialog 使用DecorView 作為Root View
PopupWindow Root View
PopupWindow 使用PopupDecorView 作為Root View(PopupDecorView 為PopupWindow內部類)
Toast Root View
Toast 默認加載

View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);

v 作為其默認的Root View
當然也可以更改其默認的Root View:

toast.setView(View rootView)

更多Dialog/PopupWindow/Toast 相關請移步:Dialog PopupWindow Toast 你還有疑惑嗎

總結

通過上述分析可知,輸入事件從底層傳到App層,經過責任鏈處理,最后分發給Root View,這過程用圖表示:


image.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容