拇指记者打探事件分发机制背后的秘密(上)

简介: 聊到事件分发,很多朋友就会想到view的dispatchTouchEvent,其实在此之前,Android还做了很多工作。

前言


聊到事件分发,很多朋友就会想到view的dispatchTouchEvent,其实在此之前,Android还做了很多工作。


比如跨进程获取输入事件的方式?在dispatchTouchEvent责任链之前还有一条InputStage责任链?DecorView,PhoneWindow之间的传递顺序?


包括事件分发过程中事件序列的处理方式?ViewGroup和View之间的协调?等等。

这一切,都要从你可爱的小拇指说起...


当你的拇指触碰手机的那一刹那,手机就被你深深的影响了,没错,手机会收到你给他布置的任务。


这个任务可以是:


  • 滑动界面任务
  • 点击按钮任务
  • 长按任务


等等,总之,你向手机传递了这个任务信息,接下来就是手机的处理任务时间。


我们可以假设手机系统就是一个大的公司(Android公司),而我们触摸手机的任务就是一个完整的项目需求,今天就和大家一起深入Android公司内部,打探事件分发的那些私密。


在此之前,我也列出了问题和大纲:


0.png


硬件部门和内核部门


首先,我的拇指找到了Android公司,说出了自己的需求,比如:点击某个View并滑动到另外的位置。


Android公司首先会派出硬件部门,和我的小拇指进行会谈,接收到我的需求之后,硬件部门生成简单的终端,并传递给内核部门。


内核部门将任务进行加工,生成了内部事件——event,并添加到公司内部的一个管理系统/dev/input/目录下。


这样做的目的是把外来的需求转化成内部通用,都能看懂的任务。


任务处理部门(SystemServer进程)


当任务记录在公司管理系统上后,就会有专门的任务处理部门对这些任务进行处理,他们做的事情就是一直监听/dev/input/目录,当发现有新的事件就会进行处理。


那这个任务处理部门到底是何方神圣呢?


不知道大家还记不记得在SystemServer进程中启动了一系列系统有关的服务,比如AMS,PMS等等,其中还有一个不是很起眼的角色,叫做InputManagerService


这个服务就是用来负责与硬件通信,接受屏幕输入事件。


在其内部,会启动一个读线程,也就是InputReader,它会从这个管理系统也就是/dev/input/目录拿到任务,并且分发给InputDispatcher线程,然后进行统一的事件分发调度。


分配给具体的项目组(InputChannel)


然后任务处理部门需要把任务交给 专业处理任务的项目组了,这就涉及到跨部门沟通了(跨进程通信)。


大家都知道跨部门沟通是个比较麻烦的事情,谁来完成这个事情呢?InputChannel

让我们回到ViewRootImplsetView方法:


public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
      //创建InputChannel
      mInputChannel = new InputChannel();
      //通过Binder进入systemserver进程
      res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                  getHostVisibility(), mDisplay.getDisplayId(),
                  mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                  mAttachInfo.mOutsets, mInputChannel);
    }
}


在该方法中,创建了一个InputChannel对象,并且通过Binder进入systemserver进程,最终形成socket的客户端。


这里涉及到socket通信的知识,比较重要的就是c层的socketpair方法。


socketpair()函数用于创建一对无名的、相互连接的套接子。如果函数成功,则返回0,创建好的套接字分别是sv[0]和sv[1];这对套接字可以用于全双工通信,每一个套接字既可以读也可以写。


通过这个方法,就生成了socket通信的客户端和服务端:


  • socket服务端保存到system_server中的WindowState的mInputChannel;
  • socket客户端通过binder传回到远程进程的UI主线程ViewRootImpl的mInputChannel;


感兴趣的可以看看gityuan对于input分析的博客,文末有链接。


所以小结一下就是,在App进程创建了一个对象InputChannel,通过Binder机制传入了SystemServer进程,也就是WindowManagerService中。然后在WindowManagerService中创建了一对套接字用于进程间通信,而传过来的InputChannel就指向了socket的客户端。


然后App进程的主线程就会监听这个socket客户端,当收到消息(输出事件)后,回调NativeInputEventReceiver.handleEvent()方法,最终会走到InputEventReceiver.dispachInputEvent方法。


dispachInputEvent,处理输入事件,感觉离我们熟知的事件分发比较近了。


没错,到此,任务已经分配到了具体的项目组,也就是我们所使用的具体APP中了。


小组中任务第一次分发(InputStage)


当任务到达了项目组,首先组内会对这个任务进行分发,这里会涉及到第一次责任链分发模式


为什么强调是第一次呢?因为还没有到达我们熟知的view事件分发阶段,在此之前,还会有一次事件分类的责任链分发工作,也就是InputStage处理事件分发。


//InputEventReceiver.java
private void dispatchInputEvent(int seq, InputEvent event) {
    mSeqMap.put(event.getSequenceNumber(), seq);
    onInputEvent(event); 
}
//ViewRootImpl.java ::WindowInputEventReceiver
final class WindowInputEventReceiver extends InputEventReceiver {
    public void onInputEvent(InputEvent event) {
       enqueueInputEvent(event, this, 0, true); 
    }
}
//ViewRootImpl.java
void enqueueInputEvent(InputEvent event,
        InputEventReceiver receiver, int flags, boolean processImmediately) {
    adjustInputEventForCompatibility(event);
    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();
    }
}


兜兜转转,没想到还是到了ViewRootImpl这里,所以ViewRootImpl不仅负责了界面的绘制,也负责了事件分发的部分处理工作。


这里的enqueueInputEvent方法中,有涉及到一个QueuedInputEvent类,这个类就是一个封装了InputEvent的事件类,然后经过赋值调用到doProcessInputEvents方法:


void doProcessInputEvents() {
        // Deliver all pending input events in the queue.
        while (mPendingInputEventHead != null) {
            QueuedInputEvent q = mPendingInputEventHead;
            mPendingInputEventHead = q.mNext;
            deliverInputEvent(q);
        }
    }
    private void deliverInputEvent(QueuedInputEvent q) {
        InputStage stage;
        if (stage != null) {
            stage.deliver(q);
        } else {
            finishInputEvent(q);
        }
    }
    abstract class InputStage {
        private final InputStage mNext;
        public InputStage(InputStage next) {
            mNext = next;
        }
        public final void deliver(QueuedInputEvent q) {
            apply(q, onProcess(q));
        }


到这里逻辑好像慢慢清晰了,QueuedInputEvent是一种输入事件,InputStage是处理输入事件的责任链,next字段则表示责任链的下一个InputStage


InputStage到底干了哪些事情呢?返回到ViewRootImpl的setView方法再看看:


public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
        // Set up the input pipeline.
        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;
         }
    }


可以看到在setView方法中,就把这条输入事件处理的责任链拼接完成了,不同的InputStage子类,通过构造方法一个个串联起来了,那这些InputStage到底干了啥呢?


  • SyntheticInputStage。综合处理事件阶段,比如处理导航面板、操作杆等事件。
  • ViewPostImeInputStage。视图输入处理阶段,比如按键、手指触摸等运动事件,我们熟知的view事件分发就发生在这个阶段。
  • NativePostImeInputStage。本地方法处理阶段,主要构建了可延迟的队列。
  • EarlyPostImeInputStage。输入法早期处理阶段。
  • ImeInputStage。输入法事件处理阶段,处理输入法字符。
  • ViewPreImeInputStage。视图预处理输入法事件阶段,调用视图view的dispatchKeyEventPreIme方法。
  • NativePreImeInputStage。本地方法预处理输入法事件阶段。


小结一下,事件到达应用端的主线程,会通过ViewRootImpl进行一系列InputStage来处理事件。这个阶段其实是对事件进行一些简单的分类处理,比如视图输入事件,输入法事件,导航面板事件等等。


事件分发完成后,会告知SystemServer进程的InputDispatcher线程,最终将该事件移除,完成此次事件的分发消费。


我们的view手指触摸事件就是发生在ViewPostImeInputStage阶段了,具体来看看:


final class ViewPostImeInputStage extends InputStage {
        @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);
                } 
            }
        }
    private int processPointerEvent(QueuedInputEvent q) {
            final MotionEvent event = (MotionEvent)q.mEvent;
            boolean handled = mView.dispatchPointerEvent(event)
            return handled ? FINISH_HANDLED : FORWARD;
        }
//View.java
    public final boolean dispatchPointerEvent(MotionEvent event) {
            if (event.isTouchEvent()) {
                return dispatchTouchEvent(event);
            } else {
                return dispatchGenericMotionEvent(event);
        }
    }


经过一系列分发,最终会执行到mView的dispatchTouchEvent方法,而这个mView就是DecorView,同样是在setView中进行赋值的,就不细说了。


至此,终于到了我们熟悉的环节,dispatchTouchEvent方法。


大佬之间的任务整理(DecorView)


确定了任务的分类,接下来就开始组内任务讨论整理了,这个阶段发生在几个大佬之间的谈话,这几个大佬分别是DecorView、PhoneWindow、Activity/Dialog


//DecorView.java
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        //cb其实就是对应的Activity/Dialog
        final Window.Callback cb = mWindow.getCallback();
        return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }
//Activity.java
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
//PhoneWindow.java
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
//DecorView.java
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }


可以看到,从DecorView开始,事件依次经过了Activity、PhoneWindow、DecorView


有点奇怪哈,为啥是这样一个顺序呢?而不是直接ViewRootImpl交给Activity,再交给顶层View——DecorView?而是转来转去,缘起和从呢?


  • 首先,为什么ViewRootImpl不直接把事件交给Activity?


因为界面上不止Activity一种形态呀,如果界面上存在Dialog,而Dialog的Window属于子Window,是可以覆盖应用级Window的,所以总不能把事件直接交给Activity吧?都被覆盖了,所以这时候应该把事件交给Dialog。


为了方便,我们用到了DecorView这个角色来充当分发的第一元素,由他来找到当前界面window的所持着,所以代码中也是找到mWindow.getCallback(),其实也就是对应的Activity或者Dialog。


  • 其次,交给Acitivity后,为什么不直接交给顶层View——DecorView开始分发事件呢?


因为ActivityDecorView之间并没有直接关系。DecorView怎么来的?通过setContentView被创建出来的,所以在Activity中是看不到DecorView身影的,DecorView的实例保存在PhoneWindow中,由Window所管理。


所以Activity的事件肯定是交给Window来管理,之前也说过PhoneWindow的指责就是帮助Activity管理View,所以事件分发交给它也是它的职责所在。而PhoneWindow的处理方式,就是交给顶层的DecorView来处理了。


这样,一个事件分发的链条就形成了:


DecorView——>Activity——>PhoneWindow——>DecorView——>ViewGroup

接下来就是View层级之间的事件分发,内容有点多,没赶完,所以下期见了。


总结


拇指记者的探访还在继续...


参考


https://wanandroid.com/wenda/show/12119

http://gityuan.com/2016/12/31/input-ipc/

https://juejin.cn/post/6844903926446161927

https://www.jianshu.com/p/b7f33f46d33c

目录
相关文章
|
6月前
|
Java 开发者
震惊!Java命名规范背后的惊天秘密,你真的了解吗?
【6月更文挑战第15天】Java命名规范提升代码可读性。标识符须以字母、下划线或$开头,避免数字开头。变量用camelCase,如`myVariable`;类与方法用PascalCase,如`MyClass`和`myMethod`。常量全大写加下划线,如`MAX_VALUE`。避免关键字冲突,确保可维护性。
48 10
任正非让寒气传递到每个人身上,互联网寒冬程序员该如何破冰?
连阿里、腾讯、百度、字节跳动、京东、美团等等互联网巨头也都在互联网寒冬的漩涡当中,都在大肆裁员,疯狂砍边缘业务,往社会大量输送人才!! 而公司裁掉的员工,一定是那些技术沉旧,不适应新技术发展,而且平日工作轻松,安逸,不思进取的员工。 作为程序员,在如此恶劣的环境之下,该如何去储备技能,才能破冰而出,得以生存呢?
|
人工智能 运维 算法
“开盖有奖”背后的攻防战:我卧底进500个黑产群、捡了3000个瓶盖
没想到,“开盖有奖”的活动被黑灰产、羊毛党们盯上了,不止一名消费者在网上发起投诉。
519 0
“开盖有奖”背后的攻防战:我卧底进500个黑产群、捡了3000个瓶盖
|
Android开发 容器
拇指记者打探事件分发机制背后的秘密(下)
上一期跟随拇指记者,发现了Android公司在指派具体的人之前的种种机制,今天就继续探索,看看任务具体的处理消费逻辑。
115 0
拇指记者打探事件分发机制背后的秘密(下)
|
机器学习/深度学习 人工智能 机器人
让AI“公民”之梦破碎的荣耀赵明,带给我们什么新希望?
2月4日,荣耀总裁赵明应邀做客央视《对话》栏目,与世界神经调控学会中国分会主席、首都医科大学三博脑科医院教授栾国明,知名文化学者、南京师范大学教授郦波,达闼科技创始人首席执行官、人工智能行业专家黄晓庆,汉森机器人公司创始人首席执行官、索菲亚的创造者大卫汉森等共话AI。
230 0
让AI“公民”之梦破碎的荣耀赵明,带给我们什么新希望?
|
机器学习/深度学习 搜索推荐
震撼!原来MIUI 9还藏着这些秘密!
小米除了做手机,还能做其他产品,小伙伴们都懂的~
271 0
震撼!原来MIUI 9还藏着这些秘密!
|
人工智能 小程序 自然语言处理
这场骗局像极了爱情,阿里小程序“一云多端”用法律武器捍卫爱情
王某通过婚恋平台认识了自称丁某的男士,并添加其社交账号,对方通过包装自己的社交形象,伪装成成功男士,并经常发送甜言蜜语俘获王某芳心。在确定男女朋友关系后,丁某便引诱王某去彩票平台帮忙充值刷流水,声称充值后随时可以提现。
7924 0
这场骗局像极了爱情,阿里小程序“一云多端”用法律武器捍卫爱情
|
人工智能 算法 机器人
阿里工程师养了只“二哈”,专治讨厌的骚扰电话
前两天3.15晚会上曝光了利用智能机器人,一天打4万个骚扰电话,从而赚取利润的黑色产业链。
4893 0