Android 事件分发面试题

序、慢慢来才是最快的方法。

Android 2020年面试系列(02 — View事件分发)_view事件分发 2020-CSDN博客

1. Touch事件如何从屏幕到我们的App。

硬件与内核部分

当我们触摸屏幕或者按键操作时,首先触发的是硬件驱动
驱动收到事件后,将相应事件写入到输入设备节点,这便产生了最原生态的内核事件
当屏幕被触摸,Linux内核会将硬件产生的触摸事件包装为Event存到/dev/input/event[x]目录下

这样做的目的是将输入事件封装为通用的Event,供后续处理。

SystemServer部分

我们知道,当系统启动时,在SystemServer进程会启动一系列系统服务,如AMS,WMS等。

其中还有一个就是我们管理事件输入的InputManagerService。这个服务就是用来负责与硬件通信,接受屏幕输入事件。

跨进程通信传递给App

现在系统进程已经拿到输入事件了,但还需要传递给App进程,这就涉及到跨进程通信的部分
我们的App中的Window与InputManagerService之间的通信实际上使用的InputChannel
InputChannel是一个pipe,底层实际是通过socket进行通信。
我们知道在Activity启动时会调用ViewRootImpl.setView()
在ViewRootImpl.setView()过程中,也会同时注册InputChannel:

2.Touch事件到达App后怎么传递到对应页面

事件回传到ViewRootImpl

事件到达应用端的主线程,会通过ViewRootImpl进行一系列InputStage来处理事件。这个阶段其实是对事件进行一些简单的分类处理,比如视图输入事件,输入法事件,导航面板事件等等。
我们的View触摸事件就发生在ViewPostImeInputStage阶段

可以看到事件分发经过了:DecorView -> Activity -> PhoneWindow -> DecorView

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

主要是为了解耦
ViewRootImpl并不知道有Activity这种东西存在!它只是持有了DecorView。所以,不能直接把触摸事件送到Activity.dispatchTouchEvent()

  • 交给Acitivity后,为什么不直接交给DecorView开始分发事件呢?

因为Activity不知道有DecorView!但是,Activity持有PhoneWindow ,而PhoneWindow当然知道自己的窗口里有些什么了,所以能够把事件派发给DecorView。
在Android中,Activity并不知道自己的Window中有些什么,这样耦合性就很低了,Activity不需要知道Window中的具体内容

3. Touch事件到达对应页面后内部怎样分发

1. ViewGroup是否拦截事件

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean isConsume = false;
    if (isViewGroup) {
        if (onInterceptTouchEvent(event)) {
            isConsume = super.dispatchTouchEvent(event);
        } 
    } 
    return isConsume;
}

如果是ViewGroup,会先执行到onInterceptTouchEvent方法判断是否拦截,如果拦截,则执行父类View的dispatchTouchEvent方法。

如果ViewGroup不拦截,则会传递到子View

如果不拦截,ViewGroup内主要做以下几件事


1. 遍历当前ViewGroup的所有子View


2. 判断当前View是否在当前子View的坐标范围内,不在范围内不能接收事件,直接跳过


3. 利用dispatchTransformedTouchEvent,如果返回true,则通过addTouchTarget对mFirstTouchTarget赋值


4. dispatchTransformedTouchEvent做的主要就是两个事,如果child不为null,则事件分发到child,否则调用super.dispatchTouchEvent,并最终返回结果


5. mFirstTouchTarget是单链表结构,记录消费链,但是在单点触控的时候这个特性没有用上,只是一个普通的TouchTarget对象

2. 子View是否拦截

子View的diapatchTouchEvent逻辑比较简单

  • 如果设置了setOnTouchListener并且返回为true,那么onTouchEvent就不再执行

  • 否则执行onTouchEvent,我们常用的OnClickListenr就是在onTouchEvent里触发的

所以默认情况下会直接执行onTouchEvent,如果我们设置了setOnClickListener或者setLongClickListener,都会正常触发。

  • 如果子View消费事件会怎么样?

上面说了,如果子View消费事件,即dispatchTouchEvent方法返回true
表示这个事件我处理了,那么事件从此结束,ViewGroup的dispatchTouchEvent也返回true

  • 如果子View不消费事件会怎么样?

子View不拦截事件,那么mFirstTouchTarget就为null,退出循环后,调用了dispatchTransformedTouchEvent方法。

最后回到Activity的dispatchTouchEvent,也是直接返回true

如果ViewGroup与子View都不拦截会怎么样

如果ViewGroup与子View都不拦截,即mFirstTouchTarget == null,dispatchTouchEvent也返回false
再看看Activity的源码

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

答案很明显:会执行Activity的onTouchEvent方法

小结

  • 事件分发的本质就是一个递归方法,通过往下传递,调用dispatchTouchEvent方法,找到事件的处理者,这也就是项目中常见的责任链模式。

  • 在分发过程中,ViewGroup通过onInterceptTouchEvent判断是否拦截事件

  • 在分发过程中,View的默认通过onTouchEvent处理事件

  • 如果底层View不消费,则默认一步步往上执行父元素onTouchEvent方法。

  • 如果所有View的onTouchEvent方法都返回false,则最后会执行到Activity的onTouchEvent方法,事件分发也就结束了。

4.滑动冲突解决

常见的滑动冲突解决方法有两种:
1. 外部拦截法
2. 内部拦截法

外部拦截法

外部拦截法的原理很简单,就是通过我们上面分析的onInterceptTouchEvent进行。外部拦截法的模板代码如下:

内部拦截法

内部拦截法是将主动权交给子View,如果子View需要事件就直接消耗,否则交给父容器处理
内部拦截法主要通过requestDisallowInterceptTouchEvent方法控制

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        final boolean intercepted;
        //只有ActionDown或者mFirstTouchTarget为空时才会判断是否拦截
        if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
            } 
        } 
    }

如上所示,原理很简单
1. 子View通过requestDisallowInterceptTouchEvent控制mGroupFlags的值,从而控制disallowIntercept的值
2. disallowIntercept为true时就不会走到onInterceptTouchEvent,外部也就无法拦截了,当需要外部处理时,将disallowIntercept置为false即可

5.每一个事件的分发都需要递归吗?用户一次操作会产生大量的 UI 事件,频繁的递归遍历不会对性能有影响吗?

当然会有!所以 Android 为了避免每个事件都递归遍历,定义了一个 【事件序列】 的概念:将用户每一次触摸屏幕 --> 移动屏幕-->抬起手指称为一个事件序列。

一个事件序列必然包含 ACTION_DOWN,ACTION_MOVE,ACTION_UP 等多个事件。其中 ACTION_MOVE 数量不确定,ACTION_DOWN 和 ACTION_UP 数量则为 1

当接收到 ACTION_DOMN 事件时,意味着一次完成事件序列的开始。ViewGroup 会通过递归遍历找到 View 树中真正对事件进行消费的子 View,并将其保存。这之后接收到 ACTION_MOVE 和 ACTION_DOWN 事件时,则跳过递归遍历的过程,直接交给之前保存的消费者。当下一次 ACTION_DOWN 事件来临时重置整个过程。

6.CTION_CANCEL 事件是用来干嘛的?

如果有 View 能够消费事件,那么该事件序列所有的后续事件都会交给这个 View 处理。但如果不希望它处理全部的后续事件怎么办?比如手指点击一个 Button 然后滑出它的边界。在这个事件序列中,我只希望 Button 处理它边界内的 move 事件。对于边界外的 move 事件,虽然它们都在一个事件序列中,但理论上不应该再传递给 Button 了。

ACTION_CANCEL 就是用来解决这个问题的。当 Button 判断 move 事件已经超出 view 的边界时,会将自己的 mPrivateFlags 置为 cancel 状态。等下次事件分发来临,Button 的父 ViewGroup 会检测 Button 的 mPrivateFlags,如果为 cancel 则将之前保存的 mFirstTouchTarget(也就是指向 Button 的引用) 设为 null,并向 Button 发送一个 ACTION_CANCEL 事件,表示以后不会再接受事件了。

参考:

【面试官爸爸】唠唠Android事件分发? - 掘金

你可能感兴趣的:(android)