Android深入了解触摸事件(一)

929 阅读12分钟

前言

触摸事件已经是老生常谈的事情了,但是可能大部分童鞋都只是知道在ViewTree下的机制,但是事件怎么到ViewTree的,可能处于一种比较空白的状态,这系列文章我希望从InputManagerService开始了解事件是怎么一步一步的传递到View中。

本系列分上下部,上部主要围绕应用层,针对Framework之后的事件分发的这段流程来讲,下部分就是围绕IMS,WMS到InputChannel来讲。

这系列文章会帮助到大家理解成个事件从产生到消费的完整流程:

  1. 了解InputManagerService,了解这个服务到底是做什么的(因篇幅问题并且涉及到Framework,本章先不讲这个,下一篇会上)
  2. 了解ViewRootImpl是怎么接收到InputManagerService发送的事件的
  3. 了解Activity是怎么接收到事件,如何从DecorView开始分发事件到ViewTree
  4. 回顾ViewTree的事件分发机制

但是整体介绍我会通过倒序的方式来介绍,越难的东西放在后面,让大家能够从浅到深去了解事件分发的过程,首先我们先回顾一下View的触摸事件机制吧~

1. 回顾一下View的触摸事件机制

View层-触摸事件流程图.drawio.png

看完流程图之后,肯定多多少少唤醒了你对View触摸事件机制的记忆,但是没有具体代码感觉还是有点空虚,接下来我们就重新对View和ViewGroup,以及分发、拦截、消费三个方法来一次回顾

1.1 三个方法,两个角色

三个方法:

  1. dispatchTouchEvent(事件分发)
  2. onInterceptTouchEvent(事件拦截)
  3. onTouchEvent(事件消费)

两个角色:

  1. View
    • dispatchTouchEvent(事件分发)
    • onTouchEvent(事件消费)
  2. ViewGroup
    • dispatchTouchEvent(事件分发)
    • onInterceptTouchEvent(事件拦截)
    • onTouchEvent(事件消费)

我们可以看出来,两个角色的功能设计上还是多多少少有点区别的,View中不包含事件拦截,甚至都不关注事件分发,只关注事件是如何消费的,而ViewGroup则需要关注分发、拦截与消费。

接下来展示一下两个角色与对应方法的伪代码,帮助大家简单理解这两个角色具体的实现:

  • View
    • dispatchTouchEvent
    #View dispatchTouchEvent 伪代码
    public boolean dispatchTouchEvent(MotionEvent event) {
        //对于View来说,因为没有子View了,所以直接调用自己的onTouchEvent
        return onTouchEvent(event);
    }
    
    • onTouchEvent
    #View onTouchEvent 伪代码
    public boolean onTouchEvent(MotionEvent event) {
        //判断有没设置点击事件,有的话默认消费事件
        if(onClickListenr!=null){
            onClickListener.onClick(this);
            return true;
        }
        //判断有没设置Clickable标志位,有的话默认消费该事件
        return isClickable;
    }
    
  • ViewGroup
    • dispatchTouchEvent
    #ViewGroup dispatchTouchEvent 伪代码
    public boolean dispatchTouchEvent(MotionEvent event) {
        //先判断是否拦截时间,如果是的话则不分发事件,并且调用自己的onTouchEvent
        if(onInterceptEvent(event)){
            this.onTouchEvent(event);
            return false;
        }else {
            //如果自身不拦截事件,则开始递归在点击范围内的子控件,调用他们的dispatchTouchEvent分发事件
            for(child in legalChildren){
                //如果子类消费时间,则返回true,当前方法出栈
                if(child.dispatchTouchEvent(event)){
                    return true;
                }
            }
        }
        //如果自身不拦截,但是子类又不消费的时候,则调用自身onTouchEvent方法,决定是非消费
        return this.onTouchEvent();
    }
    
    • onInterceptEvent
    #ViewGroup onInterceptTouchEvent 伪代码
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (需要拦截) {
            return true;
        }
        return false;
    }
    
    • onTouchEvent(与View一致就不重复贴了)

Ok,那么流程图已经明确将大流程阐述清楚,也通过伪代码去说明了程序具体怎么执行的,当然这里屏蔽了很多细节,只关注事件的分发、拦截、消费这三件事来讲,相信结合伪代码来看流程图,很快就可以掌握到ViewTree的事件分发流程是怎么样的。

那么接下来我们抛出一个疑问,DecorView的事件究竟是谁传给它的呢?接下来我们狠狠解剖一下!往着进阶的知识点进发!

2. 应用层是如何接收触摸事件的?

思考了好久这里要怎么说,还不如直接一个断点,把堆栈打出来来的实际!

1687268859928.jpg

可能有些童鞋一看就直呼卧槽,不要紧,我们只是看看整个事件传递的过程是怎么样的,大部分基本都是负责传递的逻辑,最后说一下,不知道为啥DecorView的superDispatchTouchEvent就是断不了点,只能断在上一步PhoneWindow中。

ok,我们先看看有哪些类参与了传递事件到DecorView:

  1. PhoneWindow
  2. Activity
  3. DecorView(WTF?居然有自己)
  4. ViewRootImpl
  5. ViewRootImpl(相关内部类)
    1. ViewPostImeInputStage
    2. InputStage
    3. AsyncInputStage
    4. WindowInputEventReciver

根据以上的类这里会涉及几个前置的知识点要先说一下,直接说以上几个类童鞋们会有点懵逼。

我们要先搞清楚一个核心问题,View是到底是怎么渲染到屏幕的,只有View渲染到屏幕,在用户跟屏幕交互的时候,事件才能准确传递到对应的View里。

为了搞清楚上面的核心问题,我们要先简单的了解下面的问题。

  1. PhoneWindow是什么,跟Activity的关系是什么?
  2. DecorView是什么时候初始化的?
  3. ViewRootImpl又是什么,跟PhoneWindow的关系是什么?
  4. 了解了上面三点之后,我们会在ViewRootImpl初始化的过程中,了解到应用层是怎么接收到WMS传递的触摸事件。
  5. 最后知道了怎么注册之后,再了解事件是怎么一层一层往下传递的?

了解完上面4个问题之后,我们最后再开始分析第五点,也就是在应用层事件分析的源头到传递到DecorView的过程。

我们要带着以上4个疑问来阅读下面的内容,了解它们的关系,最终目的是了解我们应用层是怎么注册触摸事件的接收的。

2.1 简单介绍PhoneWindow是什么?

PhoneWindow是什么?

  • Window是一个抽象类,具体实现只有一个PhoneWindow类。
  • Window是一个抽象的概念,表示了一个窗口,是View Hierarchy(视图树)的容器;
  • 对Window的操作只能通过WindowManager,最终经IPC由WindowManagerService最终实现;
  • Window不直接管理View Hierarchy,而是通过ViewRootImpl;

我们再用一张图说明PhoneWindow与Activity的关系。

Activity渲染-布局组成.png

再简单说一下PhoneWindow是在Activity的什么时候初始化的

//这里实现了Window的Callback接口,让Activity拥有了事件监听回调方法
public class Activity implements Window.Callback{

    final void attach(Context context,...)
        //省略部分代码
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        //这个Callback其中一个关键就是定义了触摸事件方法
        mWindow.setCallback(this);
        //为PhoneWindow设置WM
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
      
    }

}

//我们再来看看这个Callback有哪些关键的方法
 public interface Callback {
 //让Activity拥有了接受事件分发的能力
  public boolean dispatchTouchEvent(MotionEvent event);
 }

通过上面代码我们可以得知,PhoneWindow在Activity的attach方法中进行了初始化,并且Activity作为Window.Callback的身份设置进去,而Window.Callback其中一个需要重写的方法是dispatchTouchEvent,那么可以说明,Activity的事件分发至少跟PhoneWindow有关系的。

2.2 DecorView在什么时候初始化的?

答案是从Activity的setContentView开始的,我们一步一步跟。

public class Activity{
    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }
}

这里调用了getWindow(),而getWindow其实获得的就是PhoneWindow,那么我们进入PhoneWindow的setContentView看看。

class PhoneWindow extends Window {
    @Override
    public void setContentView(int layoutResID) {
        //1.如果mContentParent为空,就初始化DecorView
        if (mContentParent == null) {
            installDecor();
        }
        //2.把我们的布局装载到decorView的content区域
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    
    private void installDecor() {
        //省略无关代码...
        //1.1如果DecorView为空就生成一个DecorView
        if (mDecor == null) {
            mDecor = generateDecor(-1);
        }
        //1.2如果ContentView为空就从DecordView中获取ContentView
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
        }
    }
}

简单说完了PhoneWindow与DecorView的初始化,总结了以下几点:

  1. 了解了Activity与PhoneWindow与DecorView的关系。
  2. Activity实现dispatchTouchEvent方法是由Window.Callback接口声明的,并且Activit将自己作为Callback传进去PhoneWindow中,这说明了Activity的触摸事件分发是经过了PhoneWindow。
  3. 了解了DecorView的初始化过程。

2.3 ViewRootImpl

ViewRootImpl是什么,是基于Activity与WindowsManagerService通信的中介组件,主要负责Activity与WindowsManager的通信,并且主要负责以下几点:

  1. 通过WindowManager向WindowManagerService注册窗口。
  2. 负责了控制整个布局的测量布局绘制,并且将窗口绘制的结果传输到WindowManager。
  3. 接收WindowManagerService的事件回调。
  4. 本篇核心:向WindowManager注册InputChannel,并且通过InputEventReceiver,接收Channel中的触摸事件

ok,那2,3点我们本篇暂时不关心,我们关心第一第四点具体的代码实现是怎么样的。

接着我们就看看ViewRootImpl的初始化,顺便搞清楚以下几件事

  1. ViewRootImpl是怎么注册InputChannel,并且怎么样用InputEventReceiver监听InputChannel,获得事件的。
  2. 搞清楚ViewRootImpl与PhoneWindow的关系。

ViewRootImpl的一切的根源在ActivityThread的handleResumeActivity中开始的。

public final class ActivityThread{
    @Override
    public void handleResumeActivity(ActivityClientRecord r) {
        final Activity a = r.activity;
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    //上面只是获取对象的逻辑,重点是下面这里会调用WindowManagerGlobal的addView
                    wm.addView(decor);
                } 
            }
        }  
    }
}
//我们接着wm.addView往下跟
public class WindowManagerGlobal{
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow, int userId) {
     
        ViewRootImpl root;
        synchronized (mLock) {
            //这个IWindowSession其实就是跟WMS通信用的
            IWindowSession windowlessSession = null;
            if (wparams.token != null) {
                for (int i = 0; i < mWindowlessRoots.size(); i++) {
                    ViewRootImpl maybeParent = mWindowlessRoots.get(i);
                    if (maybeParent.getWindowToken() == wparams.token) {
                        windowlessSession = maybeParent.getWindowSession();
                        break;
                    }
                }
            }
            //创建一个ViewRootImpl对象
            if (windowlessSession == null) {
                root = new ViewRootImpl(view.getContext(), display);
            } else {
                root = new ViewRootImpl(view.getContext(), display,
                        windowlessSession);
            }
            try {
                //调用其ViewRootImpl对象的setView函数
                root.setView(view, wparams, panelParentView, userId);
            } catch (RuntimeException e) {
                throw e;
            }
        }
    }
}

接着就是核心看看ViewRootImpl的setView方法,这里就是向AMS监听触摸事件的关键。

public final class ViewRootImpl{
    //关注该方法
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
            int userId) {
        synchronized (this) {
            if (mView == null) {
                //1.我去,终于看到你了,InputChannel,这个就是跟WMS通信触摸事件的对象
                InputChannel inputChannel = null;
                if ((mWindowAttributes.inputFeatures
                        & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                    //2.进行初始化
                    inputChannel = new InputChannel();
                }
                try {
                    //3.将inputChannel注册到WMS接收信息
                    res = mWindowSession.addToDisplayAsUser(mWindow,inputChannel,...省略部分参数);
                } catch (RemoteException e) {

                }
                if (inputChannel != null) {
                    if (mInputQueueCallback != null) {
                        mInputQueue = new InputQueue();
                        mInputQueueCallback.onInputQueueCreated(mInputQueue);
                    }
                    //4.通过WindowInputEventReceiver类来代理触摸事件通信的行为
                    mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
                            Looper.myLooper());
                }
            }
        }
    }
}

这里源码有点长,以上源码其实就是讲清楚了我们一开始断点堆栈里面,在应用层的接收到事件的开端WindowInputEventReceiver这个类,是怎么注册的!

3. 知道了注册流程后,就去了解事件怎么传递了!

我们重新看回这张图,我不会完整的讲完每个调用哈~只会说关键的传递,不然篇幅要爆炸了。

1687268859928.jpg

我们看看一开始的调用,InputEventReceiver,其实就是WindowINputEventReceiver的父类我们看看其一开始回调的方法。

public abstract class InputEventReceiver {

    //1.这里其实是调用了子类复写的onInputEvent
    private void dispatchInputEvent(int seq, InputEvent event) {
        mSeqMap.put(event.getSequenceNumber(), seq);
        //1.1我们往下跟
        onInputEvent(event);
    }
}

final class WindowInputEventReceiver extends InputEventReceiver {

        @Override
        public void onInputEvent(InputEvent event) {
            //...省略部分代码
            //2.这里其实是调用了外部类ViewRootImpl的方法
            enqueueInputEvent(event, this, 0, true);
        }

}
class ViewRootImpl{
    
    void enqueueInputEvent(InputEvent event){
        //...省略代码
        //3.是否立即执行
        if (processImmediately) {
            //3.1 立即执行
            doProcessInputEvents();
        } else {
            //3.2 延迟执行,最后还是会执行doProcessInputEvents方法
            scheduleProcessInputEvents();
        }
    }
}

接下来省略一部分传递流程,有兴趣的童鞋可以去看看源码,我直接用流程图来简化这一段描述

graph TB
    ViewRootImpl.doProcessInputEvents --> ViewRootImpl.deliverInputEvent --> ViewRootImpl.ViewPostImeInputStage#内部类#.onProcess --> ViewRootImpl.ViewPostImeInputStage#内部类#.processPointerEvent 

Ok,然后就到了有关DecorView这里了,我们看看ViewRootImpl.ViewPostImeInputStage#内部类#.processPointerEvent 具体实现。

#ViewRootImpl.ViewPostImeInputStage#内部类#.processPointerEvent
private int processPointerEvent(QueuedInputEvent q) {
    final MotionEvent event = (MotionEvent)q.mEvent;
    //...省略部分代码
    //执行decorView的dispatchPointerEvent,开始分发事件,返回值表示事件是否有view处理
    boolean handled = mView.dispatchPointerEvent(event);
    maybeUpdatePointerIcon(event);
    maybeUpdateTooltip(event);
    mAttachInfo.mHandlingPointerEvent = false;
    return handled ? FINISH_HANDLED : FORWARD;
}

接着看看DecorView代码

//事实上DecorView没实现这个方法,调的是父类View的方法
#View.dispatchPointerEvent
public final boolean dispatchPointerEvent(MotionEvent event) {
    if (event.isTouchEvent()) {//如果是触摸事件则是true
        return dispatchTouchEvent(event)
    } else {
        return dispatchGenericMotionEvent(event);
    }
}

接下来看看dispatchTouchEvent方法

#DecorView
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
   //这个mWindow是PhoneWindow,还记得我们讲PhoneWindow初始化流程吗,这个Callback其实就是Activity,
   final Window.Callback cb = mWindow.getCallback();
   //调用:Activity.dispatchTouchEvent
   return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

终于到了Activity接收事件了!我们回到熟悉的地方啦!

#Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    //调用PhoneWindow的superDispatchTouchEvent方法,实际上是开始让DecorView
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    //如果没有View消费,则调用Activity自己的onTouchEvent方法
    return onTouchEvent(ev);
}

快到尾声了,我们再看看PhoneWindow的superDispatchTouchEvent

#PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    //看吧,最后就是调用的DecorView的superDispatchTouchEvent
    return mDecor.superDispatchTouchEvent(event);
}

快结束了,事件来到了DecorView

#DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
    //结束了,去到ViewTree的流程了这里调用的是ViewGroup的dispatchTouchEvent
    return super.dispatchTouchEvent(event);
}

本次触摸事件快递到站,回到了第一章的ViewTree触摸事件流程!

#ViewGroup dispatchTouchEvent 伪代码
public boolean dispatchTouchEvent(MotionEvent event) {
    //先判断是否拦截时间,如果是的话则不分发事件,并且调用自己的onTouchEvent
    if(onInterceptEvent(event)){
        this.onTouchEvent(event);
        return false;
    }else {
        //如果自身不拦截事件,则开始递归在点击范围内的子控件,调用他们的dispatchTouchEvent分发事件
        for(child in legalChildren){
            //如果子类消费时间,则返回true,当前方法出栈
            if(child.dispatchTouchEvent(event)){
                return true;
            }
        }
    }
    //如果自身不拦截,但是子类又不消费的时候,则调用自身onTouchEvent方法,决定是非消费
    return this.onTouchEvent();
}

恭喜,结束了,但是只是结束了一半,本篇只是针对应用层的完整事件传递做一个简易版的流程分析,但是对WMS、对IMS甚至对InputChannel都没有展开来说,因为篇幅问题,我决定深入了解触摸事件这个文章分开两章来说,至于下一章什么时候写出来,那就要看大家的点赞和关注啦!

首先做一个总结,本篇文章说了以下几点:

  1. 在第一章中回顾了ViewTree的触摸事件机制。
  2. 在第二章中,我们了解了在Activity与PhoneWindow与DecorView与ViewRootImpl的关系,与他们初始化的过程,并且了解了应用层的事件是通过ViewRootImpl向WMS注册的。
  3. 在第三章中,我们了解事件是如何一步一步分发到Activity和DecorView,再从DecorView分发到ViewTree中的。

看到这里的童鞋估计也不容易了,不妨点个赞,收个藏,鼓励鼓励作者顺便鼓励鼓励自己看完这篇文章,那么有关Framework层的事件相关,就期待下一章吧!谢谢大家