初探Android事件分发机制源码上之从硬件出发

首先在网上看了很多文章包括郭霖大神的文章,他们都完美分析了ViewGroup和View的事件分发。可是还是很疑惑:触摸事件ViewGroup是怎么捕获到的?大神们都说Activity,Window,ViewRoot等等这些东西都是参与了事件分发,他们是怎么参加的?是谁最先接收到的触摸事件的?我是非常不解的,因此咬牙查资料分析源码学习了一波,接下来我们来一探究竟!由于为了讲得详细也为了全面,所以篇幅略长。所以分为两个部分分析。本文为上半部分,用来专门分析事件从手机硬件源头传递到我们自己写的布局之前的过程。本文源码均来自API24。


首先我先总结一下附带上一张流程图来提前剧透一下:

当触摸事件发生时,手机硬件监测到后将事件交给ViewRootImpl,然后ViewRootImpl交给DecorView,然后DecorView通过Window将事件交给Activity,然后Activity将事件交给Window,Window交给DecorView,然后DecorView开始就交给我们定义的布局啦。


触摸事件分发流程图

(图中左边的Receiver,Handler,Stage会在下文中讲解)

接下来我们来仔细分析分析。


当我们手指触碰屏幕时,先是手机硬件会进行相应处理然后发出通知。而在源码中有一个叫做InputEventReceiver的接收器。顾名思义,输入事件接收器。这个东西在哪里用到了呢?找啊找,哈哈,发现在ViewRootImpl里用到了这个东西!
而ViewRootImpl是什么呢?简单说明一下:

他是一个用来连接Window和DecorView的纽带,也是它来触发完成View的包括measure、layout、draw绘制过程。它也起到向View分发一系列输入事件的作用,例如触摸,键盘事件等。

在ViewRootImpl中有一个WindowInputEventReceiver类继承自InputEventReceiver并且重写了onInputEvent()方法:

 final class WindowInputEventReceiver extends InputEventReceiver {
        public WindowInputEventReceiver(InputChannel inputChannel, Looper looper) {
            super(inputChannel, looper);
        }

        @Override
        public void onInputEvent(InputEvent event) {
            enqueueInputEvent(event, this, 0, true);
        }

那么这个类是在哪里实例化的呢?在ViewRoot的setView()方法中有这么一段代码:

    if (mInputChannel != null) {
                    if (mInputQueueCallback != null) {
                        mInputQueue = new InputQueue();
                        mInputQueueCallback.onInputQueueCreated(mInputQueue);
                    }
                    mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                            Looper.myLooper());
                }

正是在这里进行了实例化,因此在ViewRoot将Window加入到WindowManager的时候(setView()具体在何时调用读者自行查找,可以看我的另一篇Window相关文章,这里不再深究)会创建了一个接收器。当手机硬件发出InputEvent后会调用Receiver的onInputEvent()方法,而在这里就调用了enqueueInputEvent(event, this, 0, true)(见上面的WindowInputEventReceiver源码)。

可以看到这里调用了enqueueInputEvent()方法,顾名思义,插入输入事件。我们继续跟入查看:

  void enqueueInputEvent(InputEvent event,
            InputEventReceiver receiver, int flags, boolean processImmediately) {

     ....//其他代码

        if (processImmediately) {
            doProcessInputEvents();
        } else {
            scheduleProcessInputEvents();
        }
    }

继续贴出关键代码,首先对processImmediately进行了判断,如实为true,调用doProcessInputEvents()方法,如果为false,调用scheduleProcessInputEvents()方法,实际上scheduleProcessInputEvents()内部最终还是调用了doProcessInputEvents()方法,这里不深究,有兴趣的同志可以自行查看。
我们再跟进doProcessInputEvents()方法(跟得好累啊...):

void doProcessInputEvents() {
        ...//其他代码
            deliverInputEvent(q);
        ...//其他代码
    }

老规矩,我们贴出关键代码,在这里又调用了deliverInputEvent()方法,还是顾名思义,传递输入事件,我们继续跟进:


    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,这是什么呢?在这里我们看一下官方注释:

 /**
     * Base class for implementing a stage in the chain of responsibility
     * for processing input events.
     * <p>
     * Events are delivered to the stage by the {@link #deliver} method.  The stage
     * then has the choice of finishing the event or forwarding it to the next stage.
     * </p>
     */
    abstract class InputStage {

大概意思就是这就是用来处理一系列输入事件(例如触屏,键盘)的类,事件通过调用deliver方法来传递到这个类,这个类可以选择终止事件,可以选择处理。

回到刚才,在上面的上面的代码标注处调用了stage.deliver()方法,我们再进去看看:

   public final void deliver(QueuedInputEvent q) {
            if ((q.mFlags & QueuedInputEvent.FLAG_FINISHED) != 0) {
                forward(q);
            } else if (shouldDropInputEvent(q)) {
                finish(q, false);
            } else {
                apply(q, onProcess(q));
            }
        }

在上面代码中可以看到:他可以选择继续传递事件,也可以选择终止事件,而最后也可以调用了一个apply方法,我理解为表示处理事件。

而当一个InputEvent到来时,ViewRootImpl会寻找合适它的InputStage来处理。而ViewRootImpl中又定义了多个xxxInputStage类来继承自InputStage类,用来针对不同事件做不同处理。
对于点击事件来说,ViewPostImeInputStage可以处理它,因此调用ViewPostImeInputStage类中的onProcess方法。当onProcess被回调时,processKeyEvent、processPointerEvent、processTrackballEvent、processGenericMotionEvent至少有一个方法就会被调用,这些方法都是属于ViewPostImeInputStage的。onProgress方法如下:

  @Override
        protected int onProcess(QueuedInputEvent q) {
            if (q.mEvent instanceof KeyEvent) {
                return processKeyEvent(q);
            } else {
                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);
                }
            }
        }

这里就看到根据输入事件的不同调用不同的处理方法,我们跟进几个processxxxEvent方法进去都会发现会调用mView.dispatchxxxEvent()方法。而这些dispatchxxxEvent()方法就是分发事件机制的实现方法了。

好了说了那么久了终于撒花撒花撒.....啊呸,啥玩意儿啊?mView是个啥啊?你这坑爹啊。啥了半天啥Window啊什么之类的都没出现。。好好好,别说了,自己挖的坑自己慢慢填。。。我们再继续看看这个mView是啥(说实话很恶心这种变量名字,只能说比test名字好一点)。

第一反应是去ViewRootImpl的构造方法去看看,结果没有找到赋值,然后就只能不停的去找,找了一会儿,诶?找到了,哈哈。在ViewRootImpl的setView()方法中发现,先上源码:

   public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ...//其他代码
            }
              ...//其他代码
      }
      ...//其他代码
}

在这里进行了赋值,那么这个传入的view是啥呢?其实这个传入的View一般都是DecorView,因为ViewRoot是联系Window和根View的纽带,而根View都是DecorView。至于在哪里调用这个方法,有兴趣的同志可以自行(算了吧我去,都写到这儿了我一并写出来吧)。在我的另一篇记录里面初探Android中Window与DecorView中提到,将Window加入到WindowManager是在Activity的makeVisible()方法中调用了windowManager.addView(mDecor, getWindow().getAttributes()); 方法。而addView()方法源码如下(在WindowManagerImpl中查看):

    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

我们看到是调用了 mGlobal.addView()方法,在跟进去看看:

 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
        ...//其他代码

        ViewRootImpl root;
        View panelParentView = null;
        ...//其他代码

        root = new ViewRootImpl(view.getContext(), display);
        ...//其他代码
        root.setView(view, wparams, panelParentView);
        ...//其他代码
        }
    }

在这里看到,实例化了ViweRootImpl对象,调用了setView()方法。
!!!!看到没!我我我曹!终于看到这个方法了!在这里将view传入,而这个view就是传入的DecorView!
由此真相大白!事已至此我们就已经看到了ViewRoot在事件分发过程中的作用!起到了获取触摸事件,然后将事件传递给DecorView。

那么接下来很久回到之前我们说到的调用mView.dispatchxxxEvent()方法。那么接下来我们去DecorView去看看dispatchxxxEvent()方法,这里拿触摸事件(dispatchTouchEvent()方法)来分析,先上代码:

   @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();//getCallback()返回mCallback变量
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }

我们看到,在这里又调用了一个Window.Callback的dispatchTouchEvent()方法。沃德天呐,这又是啥(说实话我对这种变量名真是一览无遗无可奈何/(ㄒoㄒ)/~~)
又开始苦逼的找啊找,曹,发现在Window中没找到,然后仔细想了想,Window是在Activity的attach()方法中新建的,那么去看看呢?
结果一看,沃日啊,沃德天,我那么厉害?先上代码:

 final void attach(Context context, ActivityThread aThread,
                      Instrumentation instr, IBinder token, int ident,
                      Application application, Intent intent, ActivityInfo info,
                      CharSequence title, Activity parent, String id,
                      NonConfigurationInstances lastNonConfigurationInstances,
                      Configuration config, String referrer, IVoiceInteractor voiceInteractor,
                      Window window) {
        ...//其他代码
        mWindow.setCallback(this);
        ...//其他代码
    }

我们看到,在这里设置了CallBack,参数为this,意味着当前Activity。恍然大悟,原来CallBack就是Activity啊!那么之前的调用的cb.dispatchTouchEvent()方法就是Activity的方法。那么接下来我们就继续去Activity看看,先上Activity的dispatchTouchEvent()源码:

  public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();//内部实现为空
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

首先看到调用了window的superDispatchTouchEvent()方法。我们跟进PhoneWindow(Android内唯一的Winodw实现类)看看源码分析:

 @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

我们可以看到,这里调用了DecorView的superDispatchTouchEvent()方法,我们继续跟进去看看:

 public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }

这里又调用了父类的dispatchTouchEvent()方法,而DecorView是继承自FrameLayout的,也就是继承自ViewGroup的。因此dispatchTouchEvent()就是ViewGroup的方法。
至此我们就已经将事件分发从源头分析到了系统内部布局的DecorView。接下来可以说就是从我们自己写的布局开始传递,处理分发事件。

介于本文已经太长了,别说读,我写都写晕了。那么具体的ViewGroup和View的事件分发我将会再写一篇来进行记录。

终于完结撒花!!!!

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

推荐阅读更多精彩内容