Android 是一个有用户界面(GUI)的操作体系,在它诞生之初,便是为带有接触屏的手持设备预备的。作为供给给用户最重要的交互办法之一,了解接触体系是怎样作业的,对于实践的项目开发有着十分大的帮助

本篇是图形系列的第五篇文章,在之前的几篇文章中,咱们别离了解了 Android 体系[渲染/合成的底层原理]和[自定义 View / ViewGroup 的流程]

今日咱们来聊聊图形体系中,另一个老生常谈的论题:作业分发

和以往比较,今日的文章会略微有那么一点点不一样

咱们会从知道硬件驱动开端,自底向上,一步步的来了解,作业是怎样抵达的体系内核,内核又是怎样传递到运用,以及运用终究是怎样消费掉作业的

废话不多说,咱们直接进入正题,let’s go

前排提示:全文 1.5w 字,主张阅览时长 30 分钟

Android图形系统(五)番外篇:触摸事件详解

一、接触作业的来历(Linux Kernel)

Android 输入作业的类型,和分发的流程都比较杂乱,除了接触作业外,体系还有来自鼠标键盘音量键电源键等其他 Input 设备的作业需求处理

咱们日常开发接触比较多的是 ‘接触作业’,因而,本文首要评论的是 ‘接触作业’ 的分发流程,其他类型的输入作业顺带会提一嘴,不是本文的要点

本文总共分为三大部分:

榜首部分介绍 ‘接触作业的来历’ ,首要讲的是驱动上报原始作业,内核解析原始作业并保存到设备文件中,以供 Framework 读取分发

第二部分介绍 ‘接触作业的传递’ ,首要讲的是 InputManagerService 怎样把作业传递到运用进程,并分发给方针 Window

第三部分介绍 ‘接触作业的消费’ ,这是咱们运用开发者最了解的作业分发/阻拦的进程,首要讲的是 ViewGroup / View 的几个要害办法以及运用场景

在文章的最初,咱们先来聊聊榜首部分的内容,接触作业的来历

从硬件到内核

在《当咱们点击“微信”运用后,它是怎样显现出来的?》这篇文章中,为了搞清楚 Android 设备的绘图硬件是什么,咱们拆了一台 小米11 手机

今日,咱们持续来拆小米

Android图形系统(五)番外篇:触摸事件详解

图片来历:【集微拆评】小米10拆解:内部布局与iPhone类似,1亿像素主摄吸睛

如上图,这是拆解后 小米10 的内部布局,图中左面黄色箭头所指的部分,是接触屏的触控芯片

米10 触控芯片运用的是,来自意法半导体的 “FJABH“,这块芯片是用来干嘛的呢?

用来和 CPU 进行通讯的

Android图形系统(五)番外篇:触摸事件详解

图片是重绘版,参阅自:blog.csdn.net/qq_39797956…

咱们知道,当咱们按下接触屏后,屏幕的电压/电流大小会产生改变(不翻开评论接触屏作业原理

改变的电压/电流会被图中心的触控 IC 捕获,接着核算出接触方位的坐标值,经过IC总线(如上图)发送到主板上的 CPU

IC 是硬件之间常用的一种通讯协议,它规矩了什么表明起始、停止、应对和非应对等一系列信号

当然,作为运用开发,咱们无需关心他们的通讯细节

咱们只需求知道: “一旦接触屏的信号产生改变,触控芯片就能经过 IC 总线告诉到 CPU “。了解这一点就够了

好了,现在接触信号现已能被 CPU 读取了,接下来咱们看 CPU ,也便是操作体系怎样处理接触信号

内核创立设备文件

咱们都知道,Google 运用 Linux 作为 Android 体系的内核,办理着主板上的 内存网卡硬盘 等硬件设备,其间也包括 CPU

在上一末节中,接触屏现已和 CPU 树立了通讯。也便是说,操作体系能够读取接触屏发送过来的信号了

接下来的作业要点分为两个部分,一是拟定接触屏详细的上报规矩;二是想办法把设备产生的作业报告给运用程序

先来看设备的上报规矩,咱们以键盘作业举例,相同都是按下 ‘A‘ 按键

达尔优 键盘上报的是:0010

罗技 键盘上报的是:0001

同一个按键作业,两个键盘厂商上报的按键值却不相同,这显然是不可的。

所以,只要上一末节的通讯规矩(IC)还不够,咱们还需求拟定一个内容规矩,来规范各个厂家发送的数据内容

说到输入规范,这就不能不提 Linux 的 Input 子体系了

在 2001 年发布的 [2.4.0] 版别,Linux 首次加入了 Input 子体系的代码,为的便是将输入设备的共性笼统出来,拟定一致的输入规矩

首发版别只支撑 手柄鼠标键盘 这三种硬件,在随后 2002 年发布的 [2.5.25] 版别中,加入了对 接触屏 的支撑

这样一来,厂商只需求依照 Linux 拟定的规范,来上报按键值屏幕坐标等信息即可,上报规矩的问题就处理了

除了规范输入内容,Input 子体系还为运用程序供给了 操作/读取 输入设备的接口,来看结构图:

Android图形系统(五)番外篇:触摸事件详解

图片来历:自己画的

如图,Input 子体系分为三层:

  • 最下层:输入设备驱动层,drivers/input/xxx,这儿便是各大厂商需求遵从的协议规范,向内核层报告输入的内容

  • 中心层:输入中心层,input.c 归于这一层。这是 Linux 中心逻辑,用来办理设备增加、卸载等操作,作业供给给运用前的预备作业

  • 最上层:输入作业驱动层,到这儿硬件驱动现已笼统为设备文件了,对应 /dev/input/xxx ,硬件驱动发送的数据就保存在该途径下的各个设备文件中,等候运用读取

最下面的 Drivers 层,是 Linux 平台对各种输入设备的规范,各大厂商都需求去遵从该协议,否则 Linux 内核无法识别,设备也就无法正常作业

然后是中心的中心层,它是输入设备驱动的办理层,在输入结构中起着承上启下的效果:向下供给驱动层的接口,向上供给作业处理层的接口

咱们来看一眼 input.c 中的几个要害办法,也就大约知道它供给了哪些功用

/drivers/input/input.c
class input { //Linux input 结构的中心层,为驱动层供给设备注册和操作接口
  	/* 设备注册 */
    int input_register_device(input_dev *dev); // 注册一个 input 设备到内核
    void input_unregister_device(input_dev *dev); // 从内核注销掉一个 input 设备
  	/* 设备衔接 */
    int input_attach_handler(input_dev *dev, input_handler *handler);
  	void input_disconnect_device(input_dev *dev);
  	/* 作业上报 */
  	void input_handle_event(input_dev *dev, type, code, value);
  	/* 运用程序数据读取 */
  	int input_event_to_user(input_dev *dev, type, code, value);
}

从代码来看,中心层担任 设备的注册设备的衔接作业上报数据读取 这几件事,详细的完结逻辑咱们这儿不翻开评论,太长了。

在 Input 子体系的最上层(Handlers),作业驱动层担任的是,为用户拜访供给接口,将硬件驱动层发送的音讯报告给用户

咱们能够在 /dev/input/xxx 找到一切已加载成功的输入设备,它们便是作业驱动层创立的设备文件

Android图形系统(五)番外篇:触摸事件详解

图片来历:自己截的

如图所示,我手里的 Pixel 3 在 /dev/input/ 这个途径下,发现了4个输入设备,从 event0event3

咱们能够用 ‘ cat /proc/bus/input/devices ‘ 命令,查看每个输入设备的信息,我这台手机 event2 节点是接触屏的设备文件(name = fts ‘ 表明的触控驱动厂商是 ‘敦泰

接着,咱们还能够用 ‘getevent‘ 命令翻开这个设备文件,获取它发送的原始数据

Android图形系统(五)番外篇:触摸事件详解

图片来历:自己录的

你看,当咱们滑动屏幕时,终端窗口会不断的打印来自 event2 的接触作业音讯

好,现在接触作业现已能被运用程序读取了,咱们来简略总结下榜首部分 ‘接触作业的来历’ 的内容:

首要,按下接触屏后,触控芯片捕获到电压/电流的改变,核算出方位坐标后,经过 IC 总线汇报给 CPU

接着,咱们需求一致通讯内容的规矩,在 Linux 平台下,触控芯片需求完结 input.c 协议,作业依照规矩的协议上报给内核体系

终究,比及设备开机,内核加载接触屏驱动,再然后是设备的注册/衔接/上报的进程

到这儿,内核现已为咱们收集好接触屏的输入作业,并存放在了 eventX 设备文件中,Linux Input 子体系的使命现已完结

接下来咱们看 Android 体系的结构层(Framework)是怎样处理接触作业的

二、接触作业的传递(Android Framework)

上回书说到,接触屏上报的作业现已保存到 /dev/input/xxx 设备文件中,那么体系接下来的使命是:

读取接触作业,并封装成 MotionEvent / KeyEvent 方针,终究分发给正在运转的 APP 运用

读取“ 和 “分发” ,是 Android Framework 的主线使命

在接下来的章节中,咱们将首要围绕着这两件事翻开

ps:本章节的 ‘作业分发‘ 讨论的是,怎样将接触作业从设备文件传递到 APP 进程,和 View 的作业分发不是一回事儿,留意别搞混了

初识 InputManagerService

咱们都知道,在 Framework 中,输入作业是由 InputManagerService(后续简称IMS) 来办理

咱们又知道,InputManagerService 是 Java 层代码,不可能直接调用到 Linux 内核层的 Input 结构来获取输入作业

因而,IMS 必定需求 native 层的支撑,才干完结对作业的读取与分发

实践的 InputManagerService 完结总共分为三层

  1. native 层,这是 IMS 的中心层,担任 读取/分发 作业,EventHub.cppInputReader.cppInputDispatcher.cpp 三员大将都在这
  2. jni 层,首要是对 natvie 做转发,别的担任创立方针啥的,不怎样需求重视
  3. Java 层,首要担任通讯部分,和 WMS 同步窗口数据啦,和 APP 跨进程通讯啦等等

在这其间,只要 native 层略微有那么一点点杂乱,由于数据的读取和分发都产生在 native 层

好,接下来,咱们来知道 native 层的主力人员

留意,咱们本末节只重视 native 中各个角色有哪些办法功用,做了哪些作业

至于方针什么时分创立,运转在哪个进程,哪个线程,这个后边会讲到,不是本末节重视的要点

1、EventHub

EventHub 的效果是监听、读取 /dev/input 目录下产生的新作业,并封装成 RawEvent 结构体供其他人运用

文件在 frameworks/native/services/inputflinger/EventHub.cpp

咱们来看 EventHub 中的要害办法

//frameworks/native/services/inputflinger/EventHub.cpp
class EventHub {
    EventHub::EventHub(void)  {
        mEpollFd = epoll_create(EPOLL_SIZE_HINT); // 创立 epoll,用于监听设备文件是否有可读作业
        mINotifyFd = inotify_init(); // 创立 inotify ,用于监听文件体系是否改变,有改变阐明产生设备插拔
    }
    size_t EventHub::getEvents( timeoutMillis, buffer, bufferSize) {
        //getEvents() 是 IMS 的中心,该办法总共做了两件事
        // 1. 监听设备插拔动作,履行对应的设备的翻开/卸载操作,并生成 RawEvent 结构告诉调用者
        // 2. 监听输入设备文件的作业改变
        //假如没有任何作业产生,调用 epoll_wait() 函数履行等候
        return event - buffer; // 回来读取到的作业数量
    }
}

EventHub 在初始化时,会创立 mEpollFdmINotifyFd 两个 Fd,运用 INotify 机制监听设备增删作业,运用 Epoll 监听设备文件读写状况改变

然后是 getEvents() 办法,这个办法用来获取当前的设备作业,包括设备数量改变,和设备上报的作业,是 IMS 获取音讯的 “中心办法”

假如没有任何作业产生,getEvents() 会产生堵塞,直到有作业产生才回来

EventHub 整个进程如下图

Android图形系统(五)番外篇:触摸事件详解

图片来历:自己画的

2、InputReader

从类的命名就能看出来,InputReader 的职责是读取输入音讯

不过,它可不是只会读取作业,拿到原始作业今后,还需求解析作业,拆解为按键接触屏鼠标等,依据不同的输入,封装成不同的方针,提交到音讯行列等候分发

所以,原始作业的解析、转换,终究生成 KeyEventMotionEvent 方针,才是 InputReader 的首要作业

/frameworks/native/services/inputflinger/InputReader.cpp
class InputReader {
    class InputReaderThread : Thread { //内部类
        /*InputReaderThread 线程发动后,循环将不断地履行 InputReader#loopOnce()函数*/
        bool InputReaderThread::threadLoop() {
            mReader->loopOnce();
            return true;
        }
    }
    void InputReader::loopOnce(); // 中心办法,担任读取作业,解析作业
}

InputReader 类的中心办法是 InputReader#loopOnce() ,它担任读取作业和解析作业

loopOnce() 办法被 InputReaderThread 线程的 threadLoop() 所调用,InputReaderThread 是 InputReader 的内部线程类

和 Java 不同,在 C++ 中,咱们只需求将 threadLoop() 回来值设置为 true,当 InputReaderThread 线程发动后, 该办法就会被循环调用,不必手动写 while(true)

InputReaderThread 线程发动时机咱们后边会讲到,先回过头来看 InputReader#loopOnce() 的作业

/frameworks/native/services/inputflinger/InputReader.cpp
class InputReader {
    // 读取作业,解析作业
    void InputReader::loopOnce() {
        int count = mEventHub->getEvents(); // 读音讯,有音讯回来,没音讯堵塞到 epoll()。由于作业分发需求时刻,所以单次读取的作业可能是多个
        if(count) processEventsLocked();//解析原始作业、提交到行列等候分发
    }
}

要害代码只要两行

榜首行是调用了 EventHub#getEvents() 获取作业,咱们在上一节现已介绍过这个办法了,有音讯回来,没音讯堵塞到 epoll()

而且,由于履行作业分发需求时刻,在上一次分发还没有履行完毕之前,假如多个设备都产生了作业,或许一个设备发送了多次作业,都会造成数据的积压,getEvents() 回来的作业数量可能是多条

第二行代码 processEventsLocked() 是获取到作业今后,对原始作业进行解析,然后提交到行列等候分发

/frameworks/native/services/inputflinger/InputReader.cpp
class InputReader {
    void InputReader::processEventsLocked(rawEvents, count) {
        // 遍历一切作业,解析、分发
        for (const RawEvent* rawEvent = rawEvents; count;) {
            int32_t type = rawEvent->type; // 获取作业类型
            switch (type){ // 源码不包括此 switch 逻辑,这是 InputDevice 中的内容,为了便利了解我才搬了过来
                case EV_KEY; // 按键类型的作业。能够上报这类作业的设备有键盘、鼠标、手柄、手写板等悉数具有按钮的设备(包括手机上的实体按键)
                case EV_ABS; // 必定坐标类型的作业。这类作业描绘了在空间中的一个点,触控板、接触屏等运用必定坐标的输入设备能够上报这类作业
                case EV_REL; // 相对坐标类型的作业。这类作业描绘了作业在空间中相对于上次作业的偏移量。鼠标、轨迹球等依据游标指针的设备能够上报此类作业
                case EV_SW; // 开关类型的作业。这类作业描绘了若干固定状况之间的切换。手机上的静音形式开关按钮、形式切换拨盘等设备能够上报此类作业
                ...
            }
            // 咱们只专心 touch 接触作业
            dispatchTouches(when, policyFlags);
        }
    }
    void dispatchTouches(){
        // 判别是否仅仅单指作业,或是多指接触等等等
        // 解析完结后,调用 dispatchMotion() 分发
        dispatchMotion();
    }
    void dispatchMotion(){
        // 终究生成 NotifyMotionArgs 结构,交给 InputDispatcher 履行终究的分发
        NotifyMotionArgs args;
        InputDispatcher::notifyMotion(args);//提交到 InputDispatcher
    }
}

ps:为了便利了解,我把其他分支整合到主办法来,中心还省略了许多代码。所以主张不要对照源码看这篇文章,否则你可能会由于找不到某个办法回来骂我的~

解析作业的事务逻辑简直都放在了 processEventsLocked() 办法

processEventsLocked() 办法中,首要遍历作业调集,依据不同的作业类型,调用不同的解析办法

咱们只专心 touch 接触作业,也便是 dispatchTouches()

dispatchTouches() 办法中,依据接触点信息来决议,是单指仍是多指作业

解析完结后,调用 dispatchMotion() 生成 NotifyMotionArgs 方针,告诉 InputDispatcher#notifyMotion() 办法

InputDispatcher#notifyMotion() 办法被调用,也就代表着,一次读取作业,解析作业的进程就完毕了

接下来就看作业是怎样分发的了,InputReader 整个进程如下图

Android图形系统(五)番外篇:触摸事件详解

图片来历:自己画的

3、InputDispatcher

接下来的 InputDispatcher 应该不必再介绍了,便是用来分发输入作业的

上一末节完毕时放的图片,左面的 InputReader 向中心的 EventQueue 行列提交了作业音讯,我先来解释一下这是什么时分产生的

/frameworks/native/services/inputflinger/InputDispatcher.cpp
class InputDispatcher {
    void notifyMotion(const NotifyMotionArgs* args) {
        MotionEntry* newEntry = new MotionEntry(args);//封装成entry
        enqueueInboundEventLocked(newEntry);//入列一个节点,等候分发被履行,逻辑在 dispatchOnce()
    }
}

呐,看代码

上一末节终究一行调用的 InputDispatcher#notifyMotion() 办法,效果便是把作业音讯提交到音讯行列,等候分发

好,尾收完了咱们持续来看 InputDispatcher 类

到了 InputDispatcher 这儿,原始的输入作业现已被封装成 KeyMotion 等方针,InputDispatcher 的使命是:找到适宜的 Window,并把数据传递过去

/frameworks/native/services/inputflinger/InputDispatcher.cpp
class InputDispatcher {
    class InputDispatcherThread : Thread {
        bool InputDispatcherThread::threadLoop() {
            mDispatcher->dispatchOnce();
            return true;
        }
    }
    void dispatchOnce() {
        if(!queue.isEmpty()) dispatchOnceInnerLocked();//有音讯就履行分发
    }
}

首要来看 InputDispatcher 的 dispatchOnce() 主办法,和 InputReader 规划思路相同,InputDispatcher 也是内部有个线程类,然后循环调用 dispatchOnce() 履行分发

假如音讯行列中有音讯,调用 dispatchOnceInnerLocked() 履行分发

/frameworks/native/services/inputflinger/InputDispatcher.cpp
class InputDispatcher {
    void dispatchOnceInnerLocked() {
        mPendingEvent = queue.dequeue(); // 从派发行列取出一个作业,简略写法
        switch (mPendingEvent->type) { // 判别音讯的类型:配置更改、插拔音讯、key作业、接触作业等等
            case EventEntry::TYPE_MOTION:
                dispatchMotionLocked(); // 咱们只专心 接触作业
        }
    }
    void dispatchMotionLocked() {
        int32_t injectionResult = findTouchedWindowTargetsLocked(); // 为 Motion 作业寻找适宜的方针窗口
        if (injectionResult) dispatchEventLocked(); // 假如成功地找到了能够接纳作业的方针窗口,则经过dispatchEventLocked()函数完结实践的派发作业
    }
}

担任分发的 dispatchOnceInnerLocked() 办法需求处理不同输入类型的作业,咱们这儿仍是只重视接触音讯

假如是接触作业,那么接触音讯的分发是由 dispatchMotionLocked() 办法来完结的

dispatchMotionLocked() 接触音讯的分发,分为两步履行:

榜首步,为 Motion 作业寻找适宜的方针窗口,这个使命交给 findTouchedWindowTargetsLocked() 函数去完结

第二步,假如成功地找到了能够接纳作业的方针窗口,交给 dispatchEventLocked() 函数完结实践的派发作业

接下来咱们来看,findTouchedWindowTargetsLocked()dispatchEventLocked() 这两个办法的完结

/frameworks/native/services/inputflinger/InputDispatcher.cpp
class InputDispatcher {
    int findTouchedWindowTargetsLocked(){
        size_t numWindows = mWindowHandles.size(); // 获取窗口调集
        for (size_t i = 0; i < numWindows; i++);//早年向后遍历一切的window以找出接触的window,将满意条件的放入inputTargets,没找到回来榜首个前台window
    }
  	// 适宜的方针窗口被确定下来之后,便能够开端将实践的作业发送给窗口了
    void dispatchEventLocked(Vector<InputTarget>& inputTargets) {
        InputChannel channel = inputTarget.inputChannel;//删减过的流程
        channel->sendMessage(&msg);//给能够被接触的window发送跨进程音讯
    }
}

findTouchedWindowTargetsLocked() 办法的首要逻辑,是依据窗口的点击区域与作业产生的坐标点选取适宜的方针窗口

代码略微有点长,这儿就不翻开评论了,感兴趣的朋友能够阅览《Window Touchable Region》这篇文章

再来看担任分发的 dispatchEventLocked() 办法,代码完结也很简略,依据窗口查找该窗口对应的 channel ,然后经过 channel 跨进程把作业传递到 APP

到这儿,InputDispatcher 一切的分发作业就悉数完毕了,整个进程如下图

Android图形系统(五)番外篇:触摸事件详解

图片来历:自己画的

哎, 还没完,回头来看担任分发接触音讯的 InputDispatcher#dispatchMotionLocked() 函数

/frameworks/native/services/inputflinger/InputDispatcher.cpp
class InputDispatcher {
    void dispatchMotionLocked() {
        int32_t injectionResult = findTouchedWindowTargetsLocked(); // 为 Motion 作业寻找适宜的方针窗口
        if (injectionResult) dispatchEventLocked(); // 假如成功地找到了能够接纳作业的方针窗口,则经过dispatchEventLocked()函数完结实践的派发作业
    }
    int findTouchedWindowTargetsLocked(){
        size_t numWindows = mWindowHandles.size(); // 获取窗口调集
        for (size_t i = 0; i < numWindows; i++)
        ...
    }
    void dispatchEventLocked(Vector<InputTarget>& inputTargets) {
        InputChannel channel = inputTarget.inputChannel;
        channel->sendMessage(&msg);//给能够被接触的window发送跨进程音讯
    }
}

再看一遍源码,咱们来思考两个问题

  1. 担任查找窗口的 findTouchedWindowTargetsLocked() 办法中,mWindowHandles 所持有的窗口调集,是从哪里来的?

  2. 担任通讯的 dispatchEventLocked() 办法中,InputChannel 是什么?IMS 是什么时分和 APP 树立通讯的?

回想一下,在 InputDispatcher 之前,不论是 EventHub ,仍是 InputReader ,咱们一直都是在和 Linux 内核打交道,读取作业、解析作业啥的

但到了 InputDispatcher 这儿,突然和运用程序产生了联络

findTouchedWindowTargetsLocked() 的窗口调集从哪里来的?

dispatchEventLocked() 又是怎样把接触作业告诉到 APP 进程的?

带着这两个疑问,咱们开端进入 InputManagerService 的发动流程环节,这儿面一定有咱们要寻找的答案

发动 InputManagerService

咱们知道,InputManagerService 作为运转在 SystemServer 进程中的服务,发动次序是排在 Zygote 进程之后的

Java 虚拟机初始化完结后,再由 Zygote 进程 fork 而来

本末节咱们将要来盯梢 InputManagerService 的发动流程,中心会涉及到一些没那么重要的类(重要的都在上面介绍过了

比方,同为在 /frameworks/native/services/inputflinger 包下面的 InputManager 类,咱们在介绍 native 层成员的时分就没有带上它

由于 InputManager 仅仅担任办理 readerdispatcher 线程 ,没有事务逻辑,不是很重要

在整个 IMS 的发动流程中,咱们时刻要谨记,本节的要点是:

  • 了解 InputManagerService 大致的发动流程
  • 了解 InputDispatcher 的窗口调集从哪里来,以及 IMS 怎样树立跟 APP 通讯的?

搞清楚这两个要害问题, IMS 发动流程这 part 就能够翻篇了,千万别被堕入到源码中,很难出来的~

1、IMS 窗口调集从哪里来?

在前两节咱们了解到,InputManagerService 能够分为 native、jni、Java 三层

这三层大致的发动次序是:先从 Java 层的初始化开端,再调用到 jni 层,由 jni 拉起 native 的各个类,从而完结 native 部分的初始化,终究回来到 Java 层,IMS 开端为各个 APP 供给服务

咱们先来看 Java 层的初始化作业

/frameworks/base/services/java/com/android/server/SystemServer.java
class SystemServer {
    private void startOtherServices() {
        inputManager = new InputManagerService(context); // 创立 IMS 方针
        ...
        //将 InputMonitor 方针保存到 IMS 方针
        inputManager.setWindowManagerCallbacks(wm.getInputMonitor());
        inputManager.start();
    }
}

SystemServer#startOtherServices() 发动服务的办法中,首要创立了 InputManagerService 方针

然后,将 WindowManagerService 中的 InputMonitor 方针保存到 IMS 中

这是 InputMonitor 类是干嘛的?之前好像没见过

简略来说,它是衔接 WMS 和 IMS 的枢纽。WMS 经过 InputMonitor.java 持有了 IMS 的引证,当窗口信息产生改变后,经过 InputMonitor#updateInputWindowsLw() 办法,将新的窗口调集更新到 IMS 中,IMS 又将窗口调集同步到 InputDispatcher

咱们本末节的方针之一,是为了搞清楚 InputDispatcher 持有的窗口调集从哪里来?

现在,答案有了,是 WMS 经过调用 InputMonitor#updateInputWindowsLw() 函数,终究同步到 InputDispatcher 类中

简略说一下查找进程

先回到 InputDispatcher 源码,查找 mWindowHandles 要害字,很简单就能发现了 mWindowHandles 调集是在 setInputWindows() 函数中被赋值

/frameworks/native/services/inputflinger/InputDispatcher.cpp
class InputDispatcher {
    void InputDispatcher::setInputWindows(inputWindowHandles) {
        mWindowHandles = inputWindowHandles;
    }
}

那么 setInputWindows() 是谁调用的?接着查找 Framework ,成果如图

Android图形系统(五)番外篇:触摸事件详解

图片来历:aospxref.com/android-7.1…

咱们发现,Java 层的 IMS 也有一个 setInputWindows() 办法,并经过 JNI 指向了 native 中的 InputDispatcher#setInputWindows()

终究的调用动作就产生在 InputMonitor 类的 updateInputWindowsLw() 办法中!

好了,InputDispatcher 持有的窗口调集来历,这个疑问现已处理了。咱们持续来跟发动流程

/frameworks/base/services/java/com/android/server/SystemServer.java
class SystemServer {
    private void startOtherServices() {
        inputManager = new InputManagerService(context);
        inputManager.start();
    }
}
/frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
class InputManagerService {
    // 【step 1.0】初始化流程
    public InputManagerService(Context context) {
       mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
       LocalServices.addService(InputManagerInternal.class, new LocalService());
    }
    // 【step 2.0】 发动流程
    public void start() {
        nativeStart(mPtr); // 详见 【2.1】
        Watchdog.getInstance().addMonitor(this);
    }
}

在 SystemServer 创立完 InputManagerService 方针后,紧接着就调用了 inputManager#start() 发动了服务

所以咱们需求把发动流程分为两个步骤来看,一个是初始化流程做了什么,第二个才是发动流程

在 InputManagerService 的结构函数中,调用了 nativeInit() 履行了初始化作业,这是个 jni 办法,咱们一同去看看源码完结

2、IMS 的初始化作业

/frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
class NativeInputManager {
    static jlong nativeInit(env, jclass, serviceObj, contextObj, messageQueueObj) {
        NativeInputManager* im = new NativeInputManager(contextObj, serviceObj,messageQueue->getLooper()); 
    }
    NativeInputManager::NativeInputManager(contextObj, serviceObj, looper)  {
        sp<EventHub> eventHub = new EventHub(); // 创立 EventHub 方针
        mInputManager = new InputManager(eventHub, this, this); // 创立 InputManager 方针
    }
}

nativeInit() 办法中创立了 NativeInputManager 方针

在 NativeInputManager 的结构函数中,又创立了两个咱们了解的方针,EventHubInputManager

EventHub 在前面现已介绍过了,担任监听作业改变,并对外供给获取作业改变的接口

InputManager 也提到过,担任创立 Reader 和 Dispatcher 两线程,没什么逻辑

/frameworks/native/services/inputflinger/EventHub.cpp
class EventHub {
    EventHub::EventHub(void)  {
        mEpollFd = epoll_create(EPOLL_SIZE_HINT); // 创立 epoll,用于监听设备文件是否有可读作业
        mINotifyFd = inotify_init(); // 创立 inotify ,用于监听文件体系是否改变,有改变阐明产生设备插拔
    }
}
/frameworks/native/services/inputflinger/InputManager.cpp
class InputManager {
    InputManager::InputManager(eventHub, readerPolicy, dispatcherPolicy) {
        mDispatcher = new InputDispatcher(dispatcherPolicy);
        mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
        initialize(); 
    }
    void InputManager::initialize() {
        mReaderThread = new InputReaderThread(mReader); //创立线程 “InputReader”
        mDispatcherThread = new InputDispatcherThread(mDispatcher); //创立线程 ”InputDispatcher“
    }
}

呐,你看

EventHub 仅仅创立了 mEpollFdmINotifyFd 两个 Fd 方针

InputManager 也仅仅创立了 InputReaderInputDispatcher 两个方针,然后创立了 InputReaderThreadInputDispatcherThread 两个线程

好了,现在 IMS 底层的三员大将:EventHubInputReaderInputDispatcher 悉数成功创立,IMS 类的初始化作业就悉数完毕了

3、IMS 的发动流程

咱们把 SystemServer 的代码再拿出来看看,看看接下来应该做什么

/frameworks/base/services/java/com/android/server/SystemServer.java
class SystemServer {
    private void startOtherServices() {
        inputManager.start();
    }
}
/frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
class InputManagerService {
    public InputManagerService(Context context); // 初始化作业已完结 
    public void start() {
        nativeStart(mPtr); // 发动服务
        Watchdog.getInstance().addMonitor(this);
    }
}

初始化作业完结今后,紧接着调用了 start() 办法发动了服务

start() 内部调用了 nativeStart() 办法,这又是个 jni 函数,咱们持续向下跟

/frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
class NativeInputManager {
    static void nativeStart(env, jclass , ptr) {
      	getInputManager()->start(); // 调用 InputManager 的 start() 办法
    }
}
/frameworks/native/services/inputflinger/InputManager.cpp
class InputManager {
    status_t InputManager::start() {
        result = mDispatcherThread->run("InputDispatcher", PRIORITY_URGENT_DISPLAY); // 发动线程“InputReader”
        result = mReaderThread->run("InputReader", PRIORITY_URGENT_DISPLAY); // 发动线程”InputDispatcher“
        ...
        return OK;
    }
}

nativeStart() 内部调用了 InputManager#start() ,发动了 InputReaderThreadInputDispatcher 线程

这俩线程咱们现已见过了,发动后,InputReaderThread 循环读音讯,读到音讯解析,生成 Event 方针,提交到作业行列,等候分发

InputDispatcher 循环派发音讯,一直从作业行列中取 Event 音讯,派发给适宜的窗口

到这儿, IMS 的发动作业就悉数完毕了,整个进程如下图

Android图形系统(五)番外篇:触摸事件详解

图片来历:自己画的

IMS 发动流程略微有那么一点点长,完整的发动分析我放在了 GitHub ,点击[这儿]跳转查看

发动 APP 进程

在上一末节 InputManagerService 的发动流程中,咱们处理了“ InputDispatcher 的窗口调集从哪里来” 的问题

现在还剩余 “ APP 是怎样和 IMS 树立通讯的 ” 这个问题还没有处理

体系服务悉数预备就绪后,接下来是 APP 运用的发动流程,在盯梢发动运用进程的进程中,咱们会找到 “ APP 是怎样和 IMS 树立通讯的 ” 这个问题的答案

1、为 Activity 分配窗口

APP 的发动进程咱们应该都多少有点了解,和 AMS 通讯怎样创立进程这部分咱们就跳过了,本篇要点是接触作业,咱们直接快进到为 Activity 设置视图,分配窗口这部分内容

/frameworks/base/core/java/android/view/ViewRootImpl.java
class ViewRootImpl {
    InputChannel mInputChannel; // 保存的是 client 端的 socket
    void setView(View view){
        mInputChannel = new InputChannel(); // 创立了空的 InputChannel ,下面代码将会生成实在的 InputChannel
        Session.addToDisplay(view,mInputChannel);//向wms增加窗口
        if(mInputChannel!=null) mInputEventReceiver = new WindowInputEventReceiver(mInputChannel, Looper.myLooper());
    }
}

咱们在 Activity 设置的视图文件,终究会调用到 ViewRootImpl#setView() 办法

setView() 办法中,总共有三行要害代码

  1. 创立了归于该视图的 InputChannel 空方针,先不必管
  2. 向 WMS 增加窗口,并将刚刚创立的 InputChannel 方针一并传递过去,重要逻辑
  3. 创立了 WindowInputEventReceiver ,将该视图的 InputChannel 保存起来,也不必管

在这三行代码中,第2行是要害代码,第1行和第3行,以现有的信息没办法解释它们内部做了什么,比及后边有时机在介绍

好,咱们来看第2行代码产生了什么

/frameworks/base/services/core/java/com/android/server/wm/Session.java
class Session {
    void addToDisplay(InputChannel inputChannel){
        WindowManagerService.addWindow(inputChannel);
    }
}
/frameworks/base/services/core/java/com/android/server/wm/WindowManagerService.java
class WindowManagerService {
    int addWindow(InputChannel outInputChannel){
        WindowState win = new WindowState(); // 首次增加视图时创立,用于描绘一个window
        win.openInputChannel(outInputChannel); // 创立通讯的要害代码,翻开一对已衔接的 socket
    }
}

Session#addToDisplay() 办法仅仅做了转发,实践创立窗口的作业仍是由 WindowManagerService#addWindow() 来完结的

addWindow() 办法中,首要创立了 WindowState 方针,用于描绘该窗口信息

随后调用了 WindowState 方针中的 openInputChannel() 办法,它是创立通讯的要害代码,内部是创立了一对已衔接的 socket

咱们来要点重视 WindowState#openInputChannel() 办法

/frameworks/base/services/core/java/com/android/server/wm/WindowState.java
class WindowState {
    InputChannel mInputChannel;
    InputChannel mClientChannel;
    void openInputChannel(InputChannel outInputChannel) {
        InputChannel[] inputChannels = InputChannel.openInputChannelPair(); // 回来一对已衔接的 socket
        // 这是一对已衔接的管道,将 socket 两头别离保存到服务端和客户端即可进行通讯
        mInputChannel = inputChannels[0]; // 下标为0的传递给 IMS 服务端,服务端可经过该 socket 向窗口发送音讯
        mClientChannel = inputChannels[1]; // 下标为1的回传给 client 端
        // 1. Client 端 InputChanenl 调用 transforTo() 办法传给 ViewRootImpl 的 mInputChannel
        mClientChannel.transferTo(outInputChannel);
        // 2. Server 端 InputChannel 存在 WindowState 的 mInputChannel 变量
        InputManagerService.registerInputChannel(mInputChannel);
    }
}
/frameworks/native/libs/input/InputTransport.cpp
status_t InputChannel::openInputChannelPair(name,outServerChannel,outClientChannel) {
    int sockets[2] = socketpair(sockets);
    serverChannelName.append(" (server)");
    outServerChannel = new InputChannel(serverChannelName, sockets[0]);
    clientChannelName.append(" (client)");
    outClientChannel = new InputChannel(clientChannelName, sockets[1]);
    return OK;
}

WindowState#openInputChannel() 中,首要调用了 InputChannel#openInputChannelPair() 函数创立两个 InputChannel,其内部调用 Linux 的 socketpair() 函数

socketpair() 是 Linux 供给的一种进程间通讯的办法,跟 pipe() 函数是类似的。但 pipe() 创立的匿名管道是半双工的,而 socketpair() 能够认为是创立一个全双工的管道:

我向我持有的 socket 中写数据,你在你持有的 socket 中能够读到数据,反过来也一样,即两头都能够对自己持有的 socket 进行读写

在接触作业的传递中,Google 团队运用了 socketpair() 创立一对已衔接的 socket ,用于 IMS 和 APP 间跨进程通讯

其间,IMS 会持有名为 server 的一端(sockets[0])进行读写; APP 持有名为 client 的一端(sockets[1])进行读写

提个醒,咱们现在看的代码是:设置 Activity 视图时,经过 binder 通讯后,跑在了 system_server 进程中的 WMS 服务里

因而,咱们接下来的使命,是把 sockets[0]server 端,传递给 IMS ,让接触作业产生后, IMS 能够告诉到 APP

然后,把 sockets[1]client 端经过 binder 跨进程回传给 APP 进程保存,让 APP 能够接纳到来自 IMS 的音讯

好,开端干活

2、sockets[1] 回传给 APP

源码里是先将 Client 的回传给 APP 进程,那咱们就依照源码的次序来看,先把归于 APP 进程的 socket / InputChannel 回传过去

/frameworks/base/services/core/java/com/android/server/wm/WindowState.java
class WindowState {
    void openInputChannel(InputChannel outInputChannel) {
        // 1. Client 端 InputChanenl 调用 transforTo() 办法传给 ViewRootImpl 的 mInputChannel
        mClientChannel.transferTo(outInputChannel);
        // 2. Server 端 InputChannel 存在 WindowState 的 mInputChannel 变量
        InputManagerService.registerInputChannel(mInputChannel);
    }
}
/frameworks/base/core/java/android/view/ViewRootImpl.java
class ViewRootImpl {
    InputChannel mInputChannel; // 保存的是 client 端的 socket 
    void setView(View view){
        mInputChannel = new InputChannel(); // 创立了空的 InputChannel
      	try {
          	Session.addToDisplay(view,mInputChannel);
        } catch (RemoteException e) {
          	mInputChannel = null;
        }
        ...
        if(mInputChannel != null ) mInputEventReceiver = new InputEventReceiver(mInputChannel, Looper.myLooper()); 
    }
}

看代码,Session#addToDisplay() 是将咱们设置的视图,和刚刚创立的空的 mInputChannel 传递到 WindowManagerService

假如 WMS 成功增加了视图,没有产生反常,表明归于 APP 端的 socket / InputChannel 现已创立成功并经过 binder 跨进程传递回来了,此时 mInputChannel 变量就不是刚刚创立的空方针了,里边现已包括了 APP 端的 socket

监听这个 socket ,咱们能够收到来自 IMS 的音讯;往这个 socket 写数据,IMS 也能够收到咱们发送的音讯,美滋滋

假如 WMS 增加视图失败了,会抛出 RemoteException 远程衔接反常,mInputChannel 变量将被清空。

总之,只要 mInputChannel 变量不为空,就表明归于 APP 端的 socket 现已传回来了, WMS 和 APP 中心的传递进程咱们先不论

好,现在 APP 端的 InputChannel 现已回传成功,下一步的代码是创立了 InputEventReceiver 方针,并将 APP 端的 InputChannel 和 APP 端的 Looper 一同传递过去

一同来看 InputEventReceiver 的代码

/frameworks/base/core/java/android/view/InputEventReceiver.java
class InputEventReceiver {
    public InputEventReceiver(InputChannel inputChannel, Looper looper) {
        mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),inputChannel, looper.getQueue());
    }
}

java 层的 InputEventReceiver 仅仅个空壳子,实践的完结在 native 层,持续向下跟

/frameworks/base/core/jni/android_view_InputEventReceiver.cpp
class NativeInputEventReceiver {
    static jlong nativeInit(env, clazz, receiverWeak, inputChannelObj,  messageQueueObj) { // 简略写法
       int fd = inputChannelObj->getFd();
       messageQueueObj->getLooper()->addFd(fd, 0, events, this, NULL);
       ...
    }
}

代码我又合并过,要害代码就两行

榜首行是运用 inputChannelObj 方针获取里边的 socketfd

第二行是运用 messageQueueObj 方针获取里边的 Looper ,把上一步拿到的 socketfd 丢进去监听

代码尽管不多,但了解这两行代码,需求对 Handler 机制比较了解,包括 native 层

我来简略解释一下:

在《Android组件系列:再谈Handler机制(Native篇)》这篇文章中,咱们了解到:在 native 层相同具有一套 Looper 机制。这套 Looper 不光能够处理 native 层音讯,还支撑监听 自定义 fd,这是本末节的要点

Java 层的 MessageQueue 在初始化时,会调用 nativeInit() 办法,同步创立 naive 层的 NativeMessageQueue 方针,并将回来的 native 引证保存到 mPtr 变量中

留意看,咱们现在盯梢的 NativeInputEventReceiver 结构函数的入参,传递过来的参数一个是 InputChannel ,里边包括了归于 APP 端的 socket,还有一个参数是来自 Java 层的 MessageQueue 方针

那么,NativeInputEventReceiver#nativeInit() 办法里完结的作业是:运用 Java MessageQueue 的 mPtr 变量持有的 NativeMessageQueue 方针,找到 native Looper

然后,运用 native 层的 Looper 方针来监听一同传递过来的 InputChannel 中的 socketfd

这一段听起来可能会有点绕,暂时没了解某个点问题也不大,咱们只需求知道,在设置视图的时分,顺便创立了和 IMS 通讯的通道就行了

好了,APP 端的 socketsockets[1]) 回传作业算是完毕了,接下来咱们看怎样把 sockets[0] 传递给 IMS

3、sockets[0] 传递到 IMS

IMS 端的 socket 终究是由 InputDispatcher 来保管,由于分发作业时,是 InputDispatcher 直接运用 socket 告诉 APP 的

所以,咱们本末节的方针是:搞清楚 IMS 端的 socket 是怎样传递到 InputDispatcher 类中的?

IMS 传递的起点,依旧是在 WindowState#openInputChannel() 办法中:

/frameworks/base/services/core/java/com/android/server/wm/WindowState.java
class WindowState {
    void openInputChannel(InputChannel outInputChannel) {
        // 1. Client 端 InputChanenl 调用 transforTo() 办法传给 ViewRootImpl 的 mInputChannel
        mClientChannel.transferTo(outInputChannel);
        // 2. Server 端 InputChannel 存在 WindowState 的 mInputChannel 变量
        InputManagerService.registerInputChannel(mInputChannel);
    }
}
/frameworks/base/services/core/java/com/android/server/input/InputManagerService.java
class InputManagerService {
    void registerInputChannel(){
        nativeRegisterInputChannel(mPtr, inputChannel, inputWindowHandle, false);
    }
}

InputManagerService#registerInputChannel() 又是一个空壳,详细完结在 native ,持续向下跟

/frameworks/base/services/core/jni/com_android_server_input_InputManagerService.cpp
class NativeInputManager {
    void nativeRegisterInputChannel(){
        InputManager->getDispatcher()->registerInputChannel(inputChannel, inputWindowHandle, monitor);
    }
}
/frameworks/native/services/inputflinger/InputDispatcher.cpp
class InputDispatcher {
    status_t InputDispatcher::registerInputChannel(inputChannel,inputWindowHandle,monitor){
        // 将代表窗口通讯的 inputChannel ,以及代表窗口信息的 inputWindowHandle 封装成 Connection 方针
        sp<Connection> connection = new Connection(inputChannel, inputWindowHandle, monitor);
        mConnectionsByFd.add(fd, connection);
        ... // 省略 IMS 监听来自 client 的代码,这部分是处理 ANR 的逻辑
    }
}

NativeInputManager#nativeRegisterInputChannel() 随手又是一个转发

InputDispatcher#registerInputChannel() 办法中,经过一番长途跋涉,终究将 socket 注册到 InputDispatcher,一同被注册过来的还有代表该窗口信息的 inputWindowHandle

代表窗口通讯的 inputChannel ,以及代表窗口信息的 inputWindowHandle 被封装成 Connection 方针,保存到 mConnectionsByFd 调集中

至此,IMS 端的 socket 传递作业完毕,APP 和 IMS 能够愉快的通讯了

整个进程如下

Android图形系统(五)番外篇:触摸事件详解

图片来历:自己画的

接触作业抵达 ViewRootImpl

好了,万事俱备,只欠东风

现在 APP 进程起来了,APP 和 IMS 也成功树立了通讯,接下来咱们按下屏幕,看看接触作业是怎样抵达咱们了解的 View 的

/frameworks/base/core/jni/android_view_InputEventReceiver.cpp
class NativeInputEventReceiver {
    static jlong nativeInit(env, clazz, receiverWeak, inputChannelObj,  messageQueueObj) { // 简略写法
       int fd = inputChannelObj->getFd();
       messageQueueObj->getLooper()->addFd(fd, 0, events, this, NULL);
       ...
    }
}

在介绍 APP 端的 socket 回传一节中,终究一步停在了 NativeInputEventReceiver#nativeInit() 办法,运用 Looper 监听了 socketfd

现在咱们来聊聊:socketfd 有音讯时,APP 端会怎样处理,作业是怎样分发到根 View 的?

/frameworks/base/core/jni/android_view_InputEventReceiver.cpp
class NativeInputEventReceiver {
    int NativeInputEventReceiver::handleEvent( receiveFd, events, data) {
        switch (type) { // 简略写法,这是 consumeEvents() 办法中的逻辑
            case AINPUT_EVENT_TYPE_KEY: {
                inputEventObj = factory->createKeyEvent(); // 转换为按键类型作业 KeyEvent
            }
            case AINPUT_EVENT_TYPE_MOTION: {
                inputEventObj = factory->createMotionEvent(); // 转转为接触类型作业 MotionEvent
            }
        }
        env->CallVoidMethod(receiverObj.get(),dispatchInputEvent, seq, inputEventObj); // 回调到 Java
    }
}

APP 端的 socketfd 产生作业改变时,Looper 依据指定的回调地址,调用到 NativeInputEventReceiver#handleEvent() 办法

NativeInputEventReceiver#handleEvent() 办法中,依据作业类型,创立不同的作业方针

终究,运用 CallVoidMethod 回调 Java 办法,将作业传递到 Java 层的 InputEventReceiver#dispatchInputEvent() 办法

/frameworks/base/core/java/android/view/InputEventReceiver.java
abstract class InputEventReceiver {
    // Called from native code.
    private void dispatchInputEvent(int seq, InputEvent event) {
        onInputEvent(event);
    }
}
/frameworks/base/core/java/android/view/ViewRootImpl.java
class ViewRootImpl extends InputEventReceiver {
    class WindowInputEventReceiver extends InputEventReceiver {
        @Override
        public void onInputEvent(InputEvent event) {
            enqueueInputEvent(event, this, 0, true);
        }
    }
    void enqueueInputEvent(InputEvent event,InputEventReceiver receiver, int flags, boolean processImmediately) {
        ...
        // 履行音讯入列今后,接着还有一个比较杂乱的流水线进程,咱们这儿先不关心,直接来看 processPointerEvent() 办法
        // input 音讯抵达 ViewRootImpl 后,Google 运用职责链的形式,将输入作业拆分为 KeyEvent 和 MotionEvent 两种类型,做进一步的处理
        //
        if(event.getType == input) processPointerEvent(event);
        if(event.getType == key) processKeyEvent(event);
    }
    // 职责链形式,每个 InputStage 担任不同的功用,链中的某个 InputStage 的成果会影响对下一节点的履行,或停止持续分发等
    // 在 ViewRootImpl#setView() 函数中指定职责链的前后次序,这儿不翻开评论,请查看参阅资料列表中《这一次,带你完全弄懂 Android 作业分发机制(外/内层职责链)》
    class InputStage {
        // 在 ViewRootImpl 中有好几个同名 processPointerEvent() 办法, eventTarget 通常是 ViewRootImpl 保存的 DecorView 方针,也便是会调用到 View#dispatchPointerEvent() 办法
        private int processPointerEvent(QueuedInputEvent q) {
            MotionEvent event = (MotionEvent)q.mEvent;
            final View eventTarget =  mView; // 通常是 DecorView
            boolean handled = eventTarget.dispatchPointerEvent(event);
            ...
            return handled ? FINISH_HANDLED : FORWARD;
        }
        private int processKeyEvent(QueuedInputEvent q) {
            KeyEvent event = (KeyEvent)q.mEvent;
            mView.dispatchKeyEvent(event)
            final View eventTarget =  mView; // 通常是 DecorView
            boolean handled = eventTarget.dispatchPointerEvent(event);
            ...
            return handled ? FINISH_HANDLED : FORWARD;
        }
    }
}

InputEventReceiver 是笼统类,在 dispatchInputEvent() 办法中回调 onInputEvent() 办法。

而 ViewRootImpl 的内部类 WindowInputEventReceiver 完结了 InputEventReceiver 类,而且在 onInputEvent() 内部又调用了 enqueueInputEvent() 入列输入音讯

所以,接下来的分发逻辑悉数都产生在 ViewRootImpl#enqueueInputEvent() 办法中

胜利的曙光就在前方,持续冲

/frameworks/base/core/java/android/view/ViewRootImpl.java
class ViewRootImpl extends InputEventReceiver {
    void enqueueInputEvent(InputEvent event,InputEventReceiver receiver, int flags, boolean processImmediately) {
        if(event.getType == input) processPointerEvent(event);
        if(event.getType == key) processKeyEvent(event);
    }
    class InputStage {
        private int processPointerEvent(QueuedInputEvent q) {
            MotionEvent event = (MotionEvent)q.mEvent;
            final View eventTarget =  mView; // 通常是 DecorView
            boolean handled = eventTarget.dispatchPointerEvent(event);
            ...
            return handled ? FINISH_HANDLED : FORWARD;
        }
        private int processKeyEvent(QueuedInputEvent q);// 处理 key 作业,疏忽
    }
}

input 音讯抵达 ViewRootImpl 后,将输入作业拆分为 KeyEventMotionEvent 两种类型,做进一步的处理

Google 团队运用了职责链形式来处理作业音讯,每个 InputStage 担任不同的功用,链中的某个 InputStage 的成果会影响对下一节点的履行,或停止分发等

ViewRootImpl#setView() 函数中指定了职责链履行的前后次序,咱们这儿不翻开评论,感兴趣的同学能够查看参阅资料列表中《这一次,带你完全弄懂 Android 作业分发机制(外/内层职责链)》

为了省劲,咱们直接看 processPointerEvent() 处理接触作业的办法

processPointerEvent() 办法中,先是将 InputEvent 强转为 MotionEvent ,然后,调用 mViewdispatchPointerEvent() 办法履行分发

接触作业抵达 DecorView

了解 Window 创立流程的朋友必定知道,mView 便是 DecorView ,那这儿其实调用的是 DecorView#dispatchPointerEvent() 履行分发,接着来盯梢

/frameworks/base/core/java/android/view/View.java
class View {
    public final boolean dispatchPointerEvent(MotionEvent event) {
        if (event.isTouchEvent()) {
            return dispatchTouchEvent(event);
        } else {
            return dispatchGenericMotionEvent(event);
        }
    }
}

DecorView 承继自 FrameLayout ,FrameLayout 承继自 ViewGroup ,ViewGroup 承继自 View

View 的 dispatchPointerEvent()final 要害字润饰的,不允许子类重写

所以,调用 DecorView#dispatchPointerEvent() 实践的履行者是 View

View#dispatchPointerEvent() 办法中,首要判别是不是接触作业,那必定是啊

接着调用 dispatchTouchEvent() 办法履行分发

dispatchTouchEvent() 没有被 final 润饰,能够被重写,所以咱们现在回到 DecorView 的 dispatchTouchEvent() 办法中

/frameworks/base/core/java/com/android/internal/policy/DecorView.java
class DecorView extends FrameLayout {
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Window.Callback cb = mWindow.getCallback(); // 给 Activity 和 Dialog 阻拦作业的时机
        return cb != null ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }
}

DecorView#dispatchTouchEvent() 办法中,先是判别了 mWindow 持有的 Callback 是否为空

这儿临时补充两个小细节

  1. Window.Callback 是个接口,而 Activity 和 Dialog 都完结了这个接口
  2. DecorView 持有的 mWindow 的赋值途径是这样的:PhoneWindow#setContentView() -> installDecor() -> generateDecor() -> DecorView#setWindow(),不翻开评论了

接着看代码,假如 mWindow 的 Callback 不为空,则优先调用 Callback 的 dispatchTouchEvent() 函数履行分发

咱们以 Activity 来举例

/frameworks/base/core/java/android/app/Activity.java
class Activity {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    public boolean onTouchEvent(MotionEvent event) {
        ...
    }
}
/frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
class PhoneWindow {
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
}
/frameworks/base/core/java/com/android/internal/policy/DecorView.java
class DecorView extends FrameLayout {
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
}

Activity 的 dispatchTouchEvent() 办法被调用后,先调用了 getWindow().superDispatchTouchEvent(ev) 履行分发,没人处理再调用本身的 onTouchEvent() 办法

getWindow().superDispatchTouchEvent() 办法兜兜转转一圈,仍是调用到 DecorView#superDispatchTouchEvent()

前面说过了,DecorView 承继自 FrameLayout ,FrameLayout 是没有重写 dispatchTouchEvent() 办法的,FrameLayout 承继自 ViewGroup ,ViewGroup 重写了 dispatchTouchEvent()

所以,终究履行分发的仍是 ViewGroup#dispatchTouchEvent() 办法

合着绕了一圈,Activity 是啥也没做是吧?

ummmm~ 是的

不过,我觉得这样的规划是在给 Activity / Dialog 阻拦作业的时机,究竟假如咱们在 Activity 中重写了 dispatchTouchEvent() 办法,是能够让整棵 View 树都接纳不到接触作业的

好,现在接触作业分发的起点到了咱们十分了解的 ViewGroup#dispatchTouchEvent() 这儿

接下来的一整章,咱们来复习 View / ViewGroup 作业分发的流程

三、接触作业的消费(Application)

在之前的两节内容中,一个 input 作业一路从硬件驱动,成功的抵达运用的 DecorView

咱们接下来的使命是,把这个作业分发给某个详细的 View 或许是 ViewGroup

正文开端前,咱们先来聊聊接触作业的本体:MotionEvent

MotionEvent 表明一个接触作业,里边包括了作业的类型,接触的方位信息等,对分发者来说,作业的类型十分重要,来简略知道一下

  • ACTION_DOWN: 按下屏幕
  • ACTION_MOVE:手指滑动
  • ACTION_UP:抬起手指,脱离屏幕
  • ACTION_CANCEL:非正常抬起,简直等同于 ACTION_UP ,通常是父视图阻拦
  • ACTION_POINTER_DOWN:多指接触,表明现已有一只手指按下时,另有一只手指再次按下
  • ACTION_POINTER_UP:多指接触,表明屏幕上现已有多个手指,抬起其间一只手指后触发的作业

几种常用接触的类型就这么多,终究两种是多指接触的状况下才会收到的作业类型,本文咱们不计划评论多指接触(包括 TouchTarget),所今后边会疏忽掉

在一次作业分发中,以每个 DOWN 作业为开端, UP / CANCEL 作业为停止,在 DOWN -> MOVE -> MOVE -> UP / CANCEL 整个进程看做是一个作业序列

ViewGroup 的消费、分发、阻拦与放行

在进入 ViewGroup 的源码之前,咱们先来思考一个问题:什么是作业分发?

咱们都知道,在 Android 图形体系中,首要绘图的使命是交给 View 去完结的,ViewGroup 是作为办理视图的容器,它的使命是依照一定的规矩摆放子 View,本身则基本不参与绘图操作

可是在接触作业的分发中,ViewGroup 对接触作业的情绪就变了,由于它和子 View 一样,都可能需求呼应接触作业

1、ViewGroup 四种场景

举个比如,有一个能够纵向滑动的 ViewGroup ,里边摆满了 TextView ,用户在滑动屏幕时,必定是期望展现更多内容的。那么这时分,就需求 ViewGroup 对子 View 从头布局摆放,将隐藏在屏幕底部的子 View 显现出来

Android图形系统(五)番外篇:触摸事件详解

图片来历:自己录的

上面的场景中,ViewGroup 必须要拿到用户滑动屏幕的数据,才干核算展现多少视图才算适宜

即,ViewGroup 需求消费接触作业

再举个比如,在一个不支撑滑动的 ViewGroup 中,有个居中显现的 Button 按钮

Android图形系统(五)番外篇:触摸事件详解

图片来历:自己录的

用户按下屏幕后,ViewGroup 必定比自己的子 View 要优先拿到接触作业

而 ViewGroup 本身又不需求这个作业,那么,它就需求依据接触方位去查找,有没有子 View 需求该作业

经过核算,假如发现用户按偏了,没点到 Button ,那就不论了,dispatchTouchEvent() 回来 false ,爱谁消费谁消费,反正我不要

假如发现用户按到了中心的 Button ,这时分 ViewGroup 就需求把这个作业交给 Button ,问询子 View 要不要消费

即, ViewGroup 需求分发作业

再再举个比如,在一个支撑纵向滑动的 ViewGroup 中,有个居中显现的 Button 按钮

现在,用户按下了屏幕中的 Button ,ViewGroup 觉得不是滑动作业,就把这个作业分发给了 Button

可是用户在按下 Button 后,接着又开端滑动屏幕了

Android图形系统(五)番外篇:触摸事件详解

图片来历:自己录的

此时的 ViewGroup 需求核算滑动距离,所以是需求这个接触作业的,那只能对不起 Button 了,ViewGroup 会把本来预备分发给 Button 的作业阻拦消费掉

即,ViewGroup 需求阻拦作业

别的,咱们还需求考虑一种极端状况:假如子 View 和 ViewGroup 都需求接触作业,应该怎样处理?

按正常的阻拦逻辑,ViewGroup 先拿到作业,自己又有消费的需求,必定是紧着自己用

可是,总会有场景需求将作业优先分发给子视图,比方:

在一个支撑纵向翻滚的 ViewGroup ,它的两个子 View 相同是支撑纵向翻滚的 ViewGroup ,两个子 View 别离都包括若干 TextView

现在的需求是:长按某一个 TextView 时,允许该 TextView 上下自在拖动

Android图形系统(五)番外篇:触摸事件详解

图片来历:自己录的

当用户长按 TextView 移动时,预期是移动这个 TextView,理论上这个移动作业应该被 TextView 的爸爸消费掉,由于需求从头布局制作;

但实践上是 TextView 的爷爷消费掉的,由于爷爷觉得用户是在滑动屏幕,要把屏幕下方更多的视图显现出来

ViewGroup 和 ViewGroup 中的子 View 都想要消费作业(滑动冲突),这时分该怎样办?

很简略,咱们需求创立一种机制,让 ViewGroup 知道子 View 也需求这个作业

咱们暂且把这套机制称之为 “恳求放行” 好了

ViewGroup 为子 View 敞开一个恳求放行的接口,当 ViewGroup 收到来自子 View 的放行恳求时,优先将作业分发给子 View,这样,问题就处理了

接触作业的消费阻拦放行分发,这四种状况简直覆盖了 ViewGroup 对接触作业处理(单指)的一切场景

好,现在 ViewGroup 的需求基本上了解清楚了,接下来咱们来看源码中, Google 是怎样进行代码规划的

2、作业的放行和阻拦

第二章完毕时,终究调用停在了 ViewGroup#dispatchTouchEvent() 办法,这也是整个 View / ViewGroup 作业分发的来历

/frameworks/base/core/java/android/view/ViewGroup.java
class ViewGroup extends View {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        int actionMasked = ev.getAction() & MotionEvent.ACTION_MASK;
        TouchTarget newTouchTarget = null;
        boolean intercepted;
      	boolean handled = false;
        if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
            // 查看子视图是否调用了 requestDisallowInterceptTouchEvent(true) 恳求放行
            boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
              intercepted = onInterceptTouchEvent(ev); // 子视图未恳求放行,问询 ViewGroup 本身是否需求消费
            } else {
              intercepted = false;
            }
        } else {
            intercepted = true; // 假如 mFirstTouchTarget 为空,而且作业类型不为 DOWN ,表明先前的作业也是 ViewGroup 自己消费的,无需履行分发,再次交给自己履行即可
        }
        return handled;
    }
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        //问询 ViewGroup 本身是否需求处理作业
        return false;
    }
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
      	if (disallowIntercept) {
          	mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
      	} else {
          	mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
      	}
  	}
}

ViewGroup 的几种场景咱们现已在上面介绍过了,接下来便是对照源码解释的进程,比较轻松

在作业分发的开端,先是判别了作业是否是 DOWN 类型,或许 mFirstTouchTarget 变量是否不为空(mFirstTouchTarget 记载的是消费上一次 DOWN 作业的是谁

满意条件则进入 “查看放行” 和 “查看是否要阻拦” 的逻辑

disallowIntercept 为 true ,表明子视图是否调用了 requestDisallowInterceptTouchEvent(true) 办法恳求放行,将 intercepted 标识置为false ,而且,本次作业将不问询 ViewGroup 本身是否要消费

ViewGroup#requestDisallowInterceptTouchEvent() 办法便是之前介绍过的,敞开给子 View 恳求放行的一套机制

假如子视图没有恳求放行,那么调用 onInterceptTouchEvent() 问询自己要不要阻拦消费,不需求消费作业会持续分发,咱们后边会讲到

假如 mFirstTouchTarget 为空,而且作业类型不为 DOWN ,表明先前的作业也是 ViewGroup 自己消费的,无需履行分发,再次交给自己履行即可,将 intercepted 变量置为 true

上面这段代码解释完了,咱们来总结一下得到的信息:

只要子 View 没有调用 requestDisallowInterceptTouchEvent() 办法恳求放行,ViewGroup 有权在任何状况下,经过调用 onInterceptTouchEvent() 回来 true 的办法,阻拦任一接触作业

咱们在承继 ViewGroup 今后,首要要重写 onInterceptTouchEvent() 办法,然后咱们依据自己的需求,判别要不要消费某个作业

true 表明本身需求消费,该作业将会被阻拦,并会在下一步发送到 ViewGroup 本身的 onTouchEvent() 办法中,不会持续向下分发

false 表明不消费,持续向下分发作业

ViewGroup 的放行机制和阻拦就完毕了,咱们持续看代码,下一步该履行作业的分发了

3、作业的分发

假如子视图没有恳求放行,ViewGroup 本身也不消费,那么 intercepted 标识为 false ,进入分发流程

/frameworks/base/core/java/android/view/ViewGroup.java
class ViewGroup extends View {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        ...
        // 子视图未恳求放行,ViewGroup 本身也不消费,进入分发流程
        if (!intercepted) {
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                for (int i = mChildrenCount - 1; i >= 0; i--) {
                    ...// 查看子 View 是否可接触,是否在接触区域内等等,进程略
                    // 找到适宜的子 View 后,调用 dispatchTransformedTouchEvent() 履行作业分发,假如回来 true,记载本次分发
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                        newTouchTarget = addTouchTarget(child, idBitsToAssign); // 生成一个新的 TouchTarget 方针,用于记载消费的 View
                        break;
                    }
                }
                // 没有新的需求接触作业的视图,那么,把链表尾部的 TouchTarget 拿出来,在下一步把作业分发给它
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                }
            }
        }
        return handled;
    }
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
        boolean handled;
        if (child == null) {
            handled = super.dispatchTouchEvent(event); // ViewGroup 承继自 View ,这儿调用的是 View#dispatchTouchEvent()
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        return handled;
    }
}

分发的进程是将一切符合条件的子 View 都问询一遍

在上面这段代码中,运用 for 循环遍历一切的子 View ,查看每个子 View 是否在接触区域,是否能够被接触等等

假如找到适宜的子 View ,调用 dispatchTransformedTouchEvent() 履行分发,并记载消费的成果

一切的子 View 遍历一遍后,假如发现没有消费的视图,而且,当前 mFirstTouchTarget 不为空,那直接将保留着上一次消费的 View 最近的一个 TouchTarget 拿出来,等候下一步履行

4、作业的消费

作业的消费代码比较简略:

/frameworks/base/core/java/android/view/ViewGroup.java
class ViewGroup extends View {
    public boolean dispatchTouchEvent(MotionEvent ev) {
      	...
        // 跑到这,假如 mFirstTouchTarget 仍是为空 ,表明这个作业 ViewGroup 本身要消费
        if (mFirstTouchTarget == null) {
            handled = dispatchTransformedTouchEvent(ev,canceled,null,TouchTarget.ALL_POINTER_IDS);
        } else {
            TouchTarget target = mFirstTouchTarget;
            // 遍历 TouchTarget 链表,履行作业分发,代码有去重操作,被我删了,即之前分发过的新增加的节点,不再履行分发
            while (target != null) {
                final TouchTarget next = target.next;
                if (dispatchTransformedTouchEvent(ev, cancelChild,target.child, target.pointerIdBits)) {
                    handled = true;
                }
                target = next;
            }
        }
        return handled;
    }
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
        boolean handled;
        if (child == null) {
            handled = super.dispatchTouchEvent(event); // ViewGroup 承继自 View ,这儿调用的是 View#dispatchTouchEvent()
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        return handled;
    }
}

假如 mFirstTouchTarget 为空 ,表明这个作业 ViewGroup 本身要消费,调用 dispatchTransformedTouchEvent() 办法

dispatchTransformedTouchEvent() 办法中,假如 child 参数为空,表明是 ViewGroup 要消费,那么调用 ViewGroup 的父类: View#dispatchTouchEvent()

dispatchTouchEvent() 办法终究把作业传递给 onTouchEvent() ,咱们立刻就能看到 View 的消费流程了

假如 mFirstTouchTarget 方针不为空,阐明有其他子 View 消费了作业(可能有多个),依旧调用 dispatchTransformedTouchEvent() 履行分发

好了,ViewGroup 的 消费分发阻拦放行,到这儿就完毕了,接下来咱们看 View#dispatchTouchEvent() 的流程

View 的消费

聊完了 ViewGroup 对接触作业的处理,咱们接着来聊 View 这边是怎样处理接触作业的

在上一节的 dispatchTransformedTouchEvent() 办法中,不管履行的是 super.dispatchTouchEvent(event) 办法,仍是 child.dispatchTouchEvent(event) 办法,终究都是调用到 View#dispatchTouchEvent()

View#dispatchTouchEvent() 逻辑比较少,重要代码只要一句,调用了 onTouchEvent() 消费作业,除此之外 View 的作业分发就没什么好聊的了

//frameworks/base/core/java/android/view/View.java
class View {
    public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = onTouchEvent(event);
        return result;
    }
    public boolean onTouchEvent(MotionEvent event) {
        return false;
    }
}

好了,View / ViewGroup 的作业分发到这儿就完毕了,当然咱们看到的是十分精简的版别了,只要三两个要害函数,而且简直没有任何细节

由于 View / ViewGroup 的作业分发比较简略,不像 Framework 的逻辑,绕来绕去的,作业分发的逻辑大部分时刻都在内部做跳转,感兴趣的朋友自己去跟一遍源码很快就了解清楚了

四、结语

本篇文章略微有点长,从硬件驱动,体系内核,到 Framework 都有涉及。其间,了解 IMS 的完结原理,APP 和 IMS 通讯的树立,以及 ViewGroup 的 dispatchTouchEvent() 办法的作业派发逻辑,是本篇文章比较重要的内容

在文章的终究,咱们用张大伟教师《深化了解Android 卷III》书中的一段话作为本文总结:

简略来说,内核将原始作业写入到设备文件中,InputReader 不断地经过 EventHub 将原始作业取出来,解析加工成 KeyEvent、MotionEvent 作业,然后交给 InputDispatcher

InputDispatcher 依据 WMS 供给的窗口信息将作业交给适宜的窗口

接着,窗口的 ViewRootImpl 方针再沿着控件树将作业派发给感兴趣的控件,控件对其收到的作业作出呼应,更新自己的画面、履行特定的动作

一切这些参与者以 IMS 为中心,构建了 Android 巨大而杂乱的输入体系。

好了,本篇文章到这儿就悉数完毕了。文中提到的硬件驱动和体系内核这两块暂时还不是我拿手的领域,所以假如各位大佬发现本文有写的不对的当地,还望及时指出,我会榜首时刻改正,感谢

别的,欢迎各位大佬给我留言,咱们一同来讨论技术问题

全文完

五、参阅资料

  • 《深化了解Android 卷III – 张大伟》
  • 电阻屏现已被智能手机抛弃,还有哪些运用场景?
  • 手机全贴合屏幕技术解析
  • 【Linux驱动】I2C子体系与接触屏驱动 – @hongZ
  • 【Linux驱动】input子体系与按键驱动 – @hongZ
  • Linux驱动开发|input子体系 – 安迪西
  • 从 0 开端学 Linux 驱动开发 – Hcamael
  • Android(Linux) 输入子体系解析 – Andy Lee
  • Android 怎样上报 Touchevent 给运用层 – 董刚
  • 这一次,带你完全弄懂 Android 作业分发机制(外/内层职责链) – 伤心的猪大肠
  • Android 接触作业分发机制(一)从内核到运用 悉数的开端 – 吴迪
  • Android 接触作业分发机制(二)原始作业音讯传递与分发的开端 – 吴迪
  • Android作业分发机制二:中心分发逻辑源码解析 – 一只修仙的猿
  • InputChannel and InputDispatcher in Android