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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,233评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,013评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,030评论 0 241
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,827评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,221评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,542评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,814评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,513评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,225评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,497评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,998评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,342评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,986评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,812评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,560评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,461评论 2 266

推荐阅读更多精彩内容