android的窗口机制分析------事件处理

    由于Android是linux内核的,所以它的事件处理也在linux的基础上完成的,因此本文我们从linux 内核往应用这个方向慢慢理清它的处理过程。

    linux内核提供了一个Input子系统来实现的,Input子系统会在/dev/input/路径下创建我们硬件输入设备的节点,一般情况下在我们的手机中这些节点是以eventXX来命名的,如event0,event1等等,但是如果是虚拟机的话,我们可以看到一个mice,这个mice代表鼠标设备,这是由于PC需要使用鼠标来模拟触屏。由于这些设备节点是硬件相关的,所以每款设备都是不尽相同的。看到了这些输入的设备节点,我们可能比较困惑这些eventXX到底代表什么含义呢,也就是说到底是什么样的设备创建了这个节点呢?我们可以从/proc/bus/input/devices中读出eventXX相关的硬件设备,这里具体的就不多说了,我们只需要知道android读取事件信息就是从/dev/input/目录下的设备节点中读取出来的,算是android事件处理的起源吧,可以让大家知道按键、触屏等事件是从哪里来的,不是我们的重点。

    首先,简而言之的介绍一下android事件传递的流程,按键,触屏等事件是经由WindowManagerService获取,并通过共享内存和管道的方式传递给ViewRoot,ViewRoot再dispatch给Application的View。当有事件从硬件设备输入时,system_server端在检测到事件发生时,通过管道(pipe)通知ViewRoot事件发生,此时ViewRoot再去的内存中读取这个事件信息。

    至于android在事件处理上为什么使用共享内存而不是直接使用Binder机制,我的猜测应该是google为了保证事件响应的实时性,因此在选择进程间传递事件的方式中,选择了高的共享内存的方式,由于共享内存在数据管理过程中基本不涉及到内存的数据拷贝,只是在进程读写时涉及到2次数据拷贝,这个是不可避免的数据拷贝,因此这种方式能够很好的保证系统对事件的响应,但是仅仅是共享内存是不够的,因为共享内存的通信方式并不能够通知对方有数据更新,因此android在事件处理过程中加入了另一种进程间通信方式管道(pipe),管道的效率不如共享内存高,会不会影响事件处理的实时性?没关系,每次system_serve通知ViewRoot只是向其传递一个字符,即轻巧有简单,一个字符的多次数据拷贝,我想google还是能够接受的。

    好的,了解了一些基本知识后,我们从底层往上层来分析事件的传递过程,这里为了下文便于理解,首先列出整个事件处理的结构图。

    

1. 事件处理系统的初始化过程

    前文讲到android的事件处理系统,这里称为事件传递系统更贴切一些,因为android事件系统中比较复杂就是其传递过程,下面我们就以事件传递系统来代替事件处理系统。android事件传递系统是以共享内存和管道的进程间通信方式来实现传递的,为了便于理解它的传递机制,事件传递系统的初始化工作的理解则会显得非常的重要。

    1.1 创建管道连接

    事件传递系统中的管道的主要作用是在有事件被存储到共享内存中时,system_server端通知ViewRoot去读取事件的通信机制。既然是ViewRoot和system_server之间建立管道通信,那么ViewRoot和WindowManagerService(负责事件传递,运行在system_server进程中)各需维护管道的一个文件描述符,其实ViewRoot和WindowManagerService不是各维护了一个管道的文件描述符,而是两个,当然了这两个描述符不属于同一管道,实际上也就是ViewRoot和WindowManagerService之间实现了全双工的管道通信。

    WindowManagerService--->ViewRoot方向的管道通信,表示WMS通知ViewRoot有新事件被写入到共享内存;

    ViewRoot-->WindowManagerService方向的管道通信,表示ViewRoot已经消化完共享内存中的新事件,特此通知WMS。

    ViewRoot和WindowManagerService的管道的文件描述符都是被存储在一个名为InputChannel的类中,这个InputChannel类是管道通信的载体。

    首先来看ViewRoot端的管道的建立。

    setView()@ViewRoot.java

                requestLayout();
                mInputChannel = new InputChannel();
                try {
                    res = sWindowSession.add(mWindow, mWindowAttributes,
                            getHostVisibility(), mAttachInfo.mContentInsets,
                            mInputChannel);
                } catch (RemoteException e) {

    在ViewRoot和WMS(WindowManagerService)建立起连接之前首先会创建一个InputChannel对象,同样的WMS端也会创建一个InputChannel对象,不过WMS的创建过程是在ViewRoot调用add()方法时调用的。InputChannel的构造不做任何操作,所以在ViewRoot中创建InputChannel时尚未初始化,它的初始化过程是在调用WMS方法add()时进行的,看到上面代码中将mInputChannel作为参数传递给WMS,目的就是为了初始化。下面转到WMS代码看看InputChannel的初始化过程。

    addWindow()@WindowManagerService.java

            if (outInputChannel != null) {
                String name = win.makeInputChannelName();
                InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
                win.mInputChannel = inputChannels[0];
                inputChannels[1].transferToBinderOutParameter(outInputChannel);
                
                mInputManager.registerInputChannel(win.mInputChannel);
            }

    outInputChannel为ViewRoot传递来的InputChannel对象,上述代码主要的工作其实就是创建一对InputChannel,这一对InputChannel中实现了一组全双工管道。 在创建InputChannel对的同时,会申请共享内存,并向2个InputChannel对象中各自保存一个共享内存的文件描述符。InputChannel创建完成后,会将其中一个的native InputChannel 赋值给outInputChannel,也就是对ViewRoot端InputChannel对象的初始化,这样随着ViewRoot和WMS两端的InputChannel对象的创建,事件传输系统的管道通信也就建立了起来。

    创建InputChannel pair的过程以及管道建立,共享内存申请的过程就不再列出它的代码了,请参考openInputChannelPair()@InputTransport.cpp。下图为ViewRoot和WMS两端创建InputChannel pair之后的结构。

    

    

    1.2 InputChannel的注册过程

    上一节介绍了InputChannel对象的创建过程,这个过程将管道通信建立了起来,但是我们需要清楚的一点是,一个管道通信只是对应一个Activity的事件处理,也就是当前系统中有多少个Activity就会有多少个全双工管道,那么系统需要一个管理者来管理以及调度每一个管道通信,因此我们在创建完InputChannel对象后,需要将其注册到这个管理者中去。

    明白了InputChannel对象需要注册的原因之后,我们再看ViewRoot和WMS端的InputChannel对象各自需要注册到哪里?其实也很好理解,两个InputChannel对象WMS端的是管道通信的sender, ViewRoot端的是Receiver(尽管创建的全双工,但是目前只使用到了它的一向的通信,另一方向的通信尚未使用),那么着两个InputChannel对象肯定需要被两个不同的管理者来管理。ViewRoot端的一般情况下会注册到一个NativeInputQueue对象中(这是一个Native的对象,而JAVA端的InputQueue类仅仅是提供了一些static方法与NativeInputQueue通信),只要当用到NativeActivity时,会是另外一种处理机制,这里我们不管它,NativeActivity毕竟很少用到;WMS端注册在InputManager对象中。其实从NativeInputQueue和InputManager的名字中也就能知道各自的功能了。

    1.2.1 注册到NativeInputQueue

    ViewRoot端InputChannel对象在向NativeInputQueue注册时,需要注册3个参数:

    1. 将InputChannel对象对应的Native InputChannel传递给NativeInputQueue;

    2. 将ViewRoot的成员变量InputHandler传递给NativeInputQueue,这个InputHandler则是事件的处理函数,传递它的作用主要是明确当前ViewRoot的事件处理函数;

    3. 还有一个很重要的参数需要传递给NativeInputQueue,那就是当前Application的主进程的MessageQueue。

    其实,android在实现事件传输时,很大程度上借用了线程Looper和MessageQueue的轮询(poll)机制,通过它的轮询机制来检测管道上是否有消息通知事件发生,借用Looper机制能够很大限度的保证事件能够第一时间被Application知晓, Looper这块会单独分析一下。

    在注册过程中,android会将InputChannel对象中保存的管道的文件描述符交给MessageQueue的native looper去监听,同时向native looper指示一个回调函数,一旦有事件发生,native looper就会检测到管道上的数据,同时会去调用指示的回调函数。这个回调函数为handleReceiveCallback()@android_view_InputQueue.cpp.

    当然了,NativeInputQueue对象,整个系统中只有这么一个,它为了负责管理这么多的Application的事件传递,android在NativeInputQueue类中定义了一个子类Connection,每个InputChannel对象在注册时都会创建一个自己的Connection对象。

    

    这一块的代码在registerInputChannel()@android_view_InputQueue.cpp

    1.2.2 注册到InputManager

    由于WMS端的对linux Input 系统的检测和ViewRoot对管道接收端的检测机制不同,前面分析过了,ViewRoot端很好的复用了Application 主线程的Looper轮询机制来实现对事件响应的实时性,而WMS尽管也有自己的Looper,WMS却没像ViewRoot一样复用自己的Looper机制,至于原因android的code上没有明确说明,我的猜测应该是WMS是整个系统的,不像ViewRoot一样每个Activity都有一套,为了不影响系统的整体性能,尽量不要去影响WMS。

    不采用Looper来轮询是否有事件发生,InputManager启动了2个进程来管理事件发生与传递,InputReaderThread和InputDispatcherThread,InputReaderThread进程负责轮询事件发生; InputDispatcherThread负责dispatch事件。为什么需要2个进程来管理,用一个会出现什么问题?很明显,如果用一个话,在轮询input系统event的时间间隔会变长,有可能丢失事件。

    虽然没有使用Looper来轮询事件的发生,但是InputDispatcher使用了native looper来轮询检查管道通信,这个管道通信表示InputQueue是否消化完成dispatch过去的事件。注意的是这个native looper并不是WMS线程的,而是线程InputDispatcher自定定义的,因此所有的轮询过程,需要InputDispatcher主动去调用,如

     mLooper->pollOnce(timeoutMillis);或者mLooper->wake();。而不像NativeInputQueue一样,完全不用操心对looper的操作。


    WMS在初始化时会创建这么一个InputManager实例,当然了,它也是系统唯一的。JAVA层的InputManager实例并没有实现太多的业务,真正实现Input Manager业务是Native的NativeInputManager实例,它在被创建时,建立起了整个WMS端事件传递系统的静态逻辑,如下图:

    

    NativeInputManager的整个业务的核心其实是InputReader和InputDispatcher两个模块,下面简单介绍一下这两个模块。

    A. InputReader

    InputReader从名称就可以看出主要任务是读事件,基本上它所有的业务都包含在了process()的函数中,

void InputReader::process(const RawEvent* rawEvent) {
    switch (rawEvent->type) {
    case EventHubInterface::DEVICE_ADDED:
        addDevice(rawEvent->deviceId);
        break;

    case EventHubInterface::DEVICE_REMOVED:
        removeDevice(rawEvent->deviceId);
        break;

    case EventHubInterface::FINISHED_DEVICE_SCAN:
        handleConfigurationChanged(rawEvent->when);
        break;

    default:
        consumeEvent(rawEvent);
        break;
    }
}

   process()函数的输入参数时EventHub模块提供的,

    1.当EventHub尚未打开input系统eventXX设备时,InputReader去向EventHub获取事件时,EventHub会首先去打开所有的设备,并将每个设备信息以RawEvent的形式返给InputReader,也就是process()中处理的EventHubInterface::DEVICE_ADDED类型,该过程会根据每个设备的deviceId去创建InputDevice,并根据设备的classes来创建对应的InputMapper。如上图所示。

    2.当所有的设备均被打开之后,InputReader去向EventHub获取事件时,EventHub回去轮询event节点,如果有事件,InputReader则会消化该事件consumeEvent(rawEvent);

    B. InputDispatcher

    数据传输管理的核心业务是在InputDispatcher中完成的,因此最终WMS端InputChannel对象会注册到InputDispatcher中,同样的由于整个系统中InputDispatcher实例只有一个,而WMS端InputChannel对象是和ViewRoot一一对应的,因此InputDispatcher类中也定义了一个内部类Connect来管理各自的InputChannel对象。不同于NativeInputQueue类中的Connect类,InputDispatcher中的Connect类的核心业务是由InputPublisher对象来实现的,该对象负责将发生的事件信息写入到共享内存。
相关代码在registerInputChannel()@InputDispatcher.cpp

2. 事件传递

    经过分析事件处理系统的初始化过程之后,我们已经对事件处理系统的整体架构有了一定程度的理解,那么下面的事件传递过程就会显得很easy了。

    2.1 InputReaderThread线程操作

     当input系统有事件发生时,会被InputReaderThread线程轮询到,InputReader会根据事件的device id来选择的InputDevice,然后再根据事件的类型来选择InputDevice中的InputMapper,InputMapper会将事件信息通知给InputDispatcher;

    目前adroid在InputReader中实现了5种设备类型的InputMapper,分别为滑盖/翻盖SwitchInputMapper、键盘KeyboardInputMapper、轨迹球TrackballInputMapper、多点触屏MultiTouchInputMapper以及单点触屏SingleTouchInputMapper。

设备类型

InputManager

EventType

Notify InputDispatcher

滑盖/翻盖

SwitchInputMapper

EV_SW

notifySwitch()

键盘

KeyboardInputMapper

EV_KEY

notifyKey()

轨迹球

TrackballInputMapper

EV_KEY, EV_REL,

EV_SYN

notifyMotion()

单点触屏

SingleTouchInputMapper

EV_KEY, EV_ABS,

EV_SYN

notifyMotion()

多点触屏

MultiTouchInputMapper

EV_ABS,

EV_SYN

notifyMotion()

    其中EV_REL为事件相对坐标,EV_ABS为绝对坐标,EV_SYN表示Motion的一系列动作结束。

    Notify InputDispatcher表示不同的事件通知InputDispatcher的函数调用,这几个函数虽然是被InputReaderThread调用的,单却是在InputDispatcher定义的。

    

    2.1.1 notifySwitch()

void InputDispatcher::notifySwitch(nsecs_t when, int32_t switchCode, int32_t switchValue,
        uint32_t policyFlags) {
#if DEBUG_INBOUND_EVENT_DETAILS
    LOGD("notifySwitch - switchCode=%d, switchValue=%d, policyFlags=0x%x",
            switchCode, switchValue, policyFlags);
#endif

    policyFlags |= POLICY_FLAG_TRUSTED;
    mPolicy->notifySwitch(when, switchCode, switchValue, policyFlags);
}

    Switch事件的处理是比较简单的,这是一个与Activity无关的事件,因此我们根本不需要将其dispatch到ViewRoot,所以在notifySwitch()方法中直接通知给PhoneWindowManager去处理即可。从上面的类图中我们其实可以发现mPolicy指向的就是NativeInputManager,

void NativeInputManager::notifySwitch(nsecs_t when, int32_t switchCode,
        int32_t switchValue, uint32_t policyFlags) {
#if DEBUG_INPUT_DISPATCHER_POLICY
    LOGD("notifySwitch - when=%lld, switchCode=%d, switchValue=%d, policyFlags=0x%x",
            when, switchCode, switchValue, policyFlags);
#endif

    JNIEnv* env = jniEnv();

    switch (switchCode) {
    case SW_LID:
        env->CallVoidMethod(mCallbacksObj, gCallbacksClassInfo.notifyLidSwitchChanged,
                when, switchValue == 0);
        checkAndClearExceptionFromCallback(env, "notifyLidSwitchChanged");
        break;
    }
}
    NativeInputManager的notifySwitch()最终会调用到notifySwitch()@PhoneWindowManager.java

    2.1.2 notifyKey()

void InputDispatcher::notifyKey(nsecs_t eventTime, int32_t deviceId, int32_t source,
        uint32_t policyFlags, int32_t action, int32_t flags,
        int32_t keyCode, int32_t scanCode, int32_t metaState, nsecs_t downTime) {
#if DEBUG_INBOUND_EVENT_DETAILS
    LOGD("notifyKey - eventTime=%lld, deviceId=0x%x, source=0x%x, policyFlags=0x%x, action=0x%x, "
            "flags=0x%x, keyCode=0x%x, scanCode=0x%x, metaState=0x%x, downTime=%lld",
            eventTime, deviceId, source, policyFlags, action, flags,
            keyCode, scanCode, metaState, downTime);
#endif
    if (! validateKeyEvent(action)) {
        return;
    }

    policyFlags |= POLICY_FLAG_TRUSTED;
    mPolicy->interceptKeyBeforeQueueing(eventTime, deviceId, action, /*byref*/ flags,
            keyCode, scanCode, /*byref*/ policyFlags);

    bool needWake;
    { // acquire lock
        AutoMutex _l(mLock);

        int32_t repeatCount = 0;
        KeyEntry* newEntry = mAllocator.obtainKeyEntry(eventTime,
                deviceId, source, policyFlags, action, flags, keyCode, scanCode,
                metaState, repeatCount, downTime);

        needWake = enqueueInboundEventLocked(newEntry);
    } // release lock

    if (needWake) {
        mLooper->wake();
    }
}
    InputDispatcher对KeyBoard事件的处理如上述代码所述,
    首先,InputDispatcher会截取这个按键事件,根据当前设备的状况来优先消化这个事件,这个过程当然是在将事件dispatch给ViewRoot之前。同样的就像notifySwitch()一样,最终该过程交由interceptKeyBeforeQueueing()@PhoneWindowManager.java来处理。interceptKeyBeforeQueueing()主要是对一些特殊案件的特殊处理,并判断该按键是够应该传递给ViewRoot。通过设置标志位policyFlags的值来判断是否给ViewRoot,例如policyFlags&POLICY_FLAG_PASS_TO_USER == 1 则应该传递给ViewRoot。
    interceptKeyBeforeQueueing()特殊处理主要是针对在锁屏或者屏幕不亮的情况的下收到特殊的键值,如音量键或者wake键。wake键是指能够点亮屏幕的键时的操作。
    其次,InputDispatcher再将该按键信息存储在一个队列中( enqueueInboundEventLocked()@InputDispatcher.cpp)。
    
    Queue mInboundQueue;

    2.1.3 notifyMotion()

    mPolicy->interceptGenericBeforeQueueing(eventTime, /*byref*/ policyFlags);
    首先,同样的,InputDispatcher会截取这个motion事件,不同的是motion事件的截取处理NativeInputManager完全有能力处理,所以并没有交给PhoneWindowManager来处理。查看代码interceptGenericBeforeQueueing()@com_android_server_InputManager.cpp.
    其次,InputDispatcher再将该motion事件信息存储在 mInboundQueue队列中( enqueueInboundEventLocked()@InputDispatcher.cpp)。

    2.2 InputDispatcherThread线程操作

    InputDispatcherThread线程的轮询过程dispatchOnce()-->dispatchOnceInnerLocked(), InputDispatcherThread线程不停的执行该操作,以达到轮询的目的,我们的研究重点也就放在这2个函数处理上。

    2.2.1 InputDispatcherThread基本流程

    InputDispatcherThread的主要操作是分两块同时进行的,

    一部分是对InputReader传递过来的事件进行dispatch前处理,比如确定focus window,特殊按键处理如HOME/ENDCALL等,在预处理完成 后,InputDispatcher会将事件存储到对应的focus window的outBoundQueue,这个outBoundQueue队列是InputDispatcher::Connection的成员函数,因此它是和ViewRoot相关的。

    一部分是对looper的轮询,这个轮询过程是检查NativeInputQueue是否处理完成上一个事件,如果NativeInputQueue处理完成事件,它就会向通过管道向InputDispatcher发送消息指示consume完成,只有NativeInputQueue consume完成一个事件,InputDispatcher才会向共享内存写入另一个事件。

    


    2.2.3 丢弃事件

    并不是所有的InputReader发送来的事件我们都需要传递给应用,比如上节讲到的翻盖/滑盖事件,除此之外的按键,触屏,轨迹球(后两者统一按motion事件处理),也会有部分的事件被丢弃,InputDispatcher总会根据一些规则来丢弃掉一部分事件,我们来分析以下哪些情况下我们需要丢弃掉部分事件?

    InputDispatcher.h中定义了一个包含有丢弃原因的枚举:

    enum DropReason {
        DROP_REASON_NOT_DROPPED = 0,
        DROP_REASON_POLICY = 1,
        DROP_REASON_APP_SWITCH = 2,
        DROP_REASON_DISABLED = 3,
    };
    1. DROP_REASON_NOT_DROPPED

     不需要丢弃

    2. DROP_REASON_POLICY

   设置为DROP_REASON_POLICY主要有两种情形:

    A. 在InputReader notify InputDispatcher之前,Policy会判断不需要传递给应用的事件。如上一节所述。

    B. 在InputDispatcher dispatch事件前,PhoneWindowManager使用方法interceptKeyBeforeDispatching()提前consume掉一些按键事件,如上面的流程图所示。

    interceptKeyBeforeDispatching()主要对HOME/MENU/SEARCH按键的特殊处理,如果此时能被consume掉,那么在InputDispatcher 中将被丢弃。

    3.DROP_REASON_APP_SWITCH

    当有App switch 按键如HOME/ENDCALL按键发生时,当InputReader向InputDispatcher 传递app switch按键时,会设置一个APP_SWITCH_TIMEOUT 0.5S的超时时间,当0.5s超时时,InputDispatcher 尚未dispatch到这个app switch按键时,InputDispatcher 将会丢弃掉mInboundQueue中所有处在app switch按键前的按键事件。这么做的目的是保证app switch按键能够确保被处理。此时被丢弃掉的按键会被置为DROP_REASON_APP_SWITCH。

    4. DROP_REASON_DISABLED

    这个标志表示当前的InputDispatcher 被disable掉了,不能dispatch任何事件,比如当系统休眠时或者正在关机时会用到。



你可能感兴趣的:(android的窗口机制分析------事件处理)