美文网首页
一切从android的handler说起(五)之触摸事件模型

一切从android的handler说起(五)之触摸事件模型

作者: hxiao1983 | 来源:发表于2019-08-17 19:02 被阅读0次

阅读本文大概需要 5 分钟。

在弄清楚了handler消息机制原理后,小张显得异常高兴,感觉这块儿终于像一碗清水似的看到底了。

我无意间说了一句:别高兴得太早,你只清楚了一半!

小张听了有点发懵:一半?啥意思,不都非常清晰了吗?

我笑了笑,说道:现在你只知道UI线程是事件驱动模型,有事就干,没事就睡觉,那有没有想过事件都从何而来?为何我手指触摸UI上任何一个button都能很快做出反应呢?这个触摸事件是如何转为message扔到UI queue里去的呢?

小张瞬间愣住了,原来之前一直在聊的都是UI线程如何被message唤醒触发,从未考虑过message的源头是如何来的。

小张紧接着又补了一句:那肯定不是UI线程自己一直在盯着,而是另外一个东西,不然UI线程又该卡成翔了。

我继续问道:是的。那究竟是谁在负责这个输入事件的源头的采集呢?

小张只得缴械,直说不知道。

我又问道:如果是你来设计这个模型,你能大概给个想法吗?

小张想了一会儿说道:

1). 首先应该有一个线程在不断的监听屏幕,一旦有触摸事件,就将事件捕获;

2). 其次,还应该存在某种手段可以找到目标窗口,因为可能有多个APP的多个界面为用户可见,必须确定这个事件究竟通知那个窗口;

3). 最后才是目标窗口如何消费事件,也就是在这一步事件被包装成message(包含具体的触摸点x,y坐标)扔进queue中唤醒UI线程来处理。

我看了一下,说道:没错,你这个设计解耦很好,各负其职。其实Android系统也是这个设计。

第1)步里所说的负责监听触摸事件的是InputManagerService。

第2)步里所说的找到目标窗口的是WindowManagerService。它俩在Android操作系统启动时已经在System Server进程中被创建好,并被注册到了另一个ServiceManager进程当中。

小张不解的问道:ServiceManager?这个东东是干嘛的,为什么要有它呢?

我说道:Android系统中各种服务大概有几十种,AMS, PMS, WMS,等等。你想想如果各种隔离的服务之间要想相互通信的话,如果没有一个中间者,它们相互之间如何知道彼此的存在呢?

小张说道:哦...是不是ServiceManager负责注册和查询各种service,以便提供相互间的通信,就像网络中的DNS一样?

我说道:没错,这样各个service的功能就解耦了。其实这是一种常见的解耦手法,当大量模块之间要相互通信时,必然会产生一个中间协调者,也是顶级思维中的HUB思想。你想想你之前用过的都有哪些轮子使用的这种思想的?

小张想了想说道:有通信总线EventBus, 还有组件化中负责各个组件间的通信的ARoute。

我哈哈道:没错,聊着聊着咱把话题扯远了。我们还是回到刚才的话题,逐个看看这2个service都是如何分别干好自己职责的吧。

首先我们看看InputManagerService,其实系统在初始化InputManagerService时生成了2个线程:InputReaderThread和InputDispatcherThread,看名字就知道这2个线程一个是负责读取各种事件源,另外一个是负责把事件源派发给别人。

1InputManager::InputManager(

2        const sp<EventHubInterface>& eventHub,

3        const sp<InputReaderPolicyInterface>& readerPolicy,

4        const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {

5    <!--事件分发执行类-->

6    mDispatcher = new InputDispatcher(dispatcherPolicy);

7    <!--事件读取执行类-->

8    mReader = new InputReader(eventHub, readerPolicy, mDispatcher);

9    initialize();

10}

11

12void InputManager::initialize() {

13    mReaderThread = new InputReaderThread(mReader);

14    mDispatcherThread = new InputDispatcherThread(mDispatcher);

15}

16

17bool InputReaderThread::threadLoop() {

18    mReader->loopOnce();

19    return true;

20}

21

22void InputReader::loopOnce() {

23        int32_t oldGeneration;

24        int32_t timeoutMillis;

25        bool inputDevicesChanged = false;

26        Vector<InputDeviceInfo> inputDevices;

27        {  

28      ...<!--监听事件-->

29        size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);

30       ....<!--处理事件-->

31           processEventsLocked(mEventBuffer, count);

32       ...

33       <!--通知派发-->

34        mQueuedListener->flush();

35    }

InputReaderThread中的loop通过监听EventHub的getEvents获取Input事件,这样输入事件就可以被读取,并由processEventsLocked初步被封装成RawEvent,最后发通知,请求派发消息。以上就解决了事件读取问题。

下面我们重点来看一下事件的分发。

上面代码中的InputReader的mQueuedListener其实就是InputDispatcher对象,所以mQueuedListener->flush()就是通知InputDispatcher事件读取完毕,可以派发事件了。

InputDispatcherThread是一个典型Looper线程,基于native的Looper实现了Hanlder消息处理模型,如果有Input事件到来就被唤醒处理事件,处理完毕后继续睡眠等待,看下面的代码有种非常熟悉的感觉,有木有:

1bool InputDispatcherThread::threadLoop() {

2    mDispatcher->dispatchOnce();

3    return true;

4}

5

6void InputDispatcher::dispatchOnce() {

7    nsecs_t nextWakeupTime = LONG_LONG_MAX;

8    {  

9      <!--被唤醒 ,处理Input消息-->

10        if (!haveCommandsLocked()) {

11            dispatchOnceInnerLocked(&nextWakeupTime);

12        }

13       ...

14    } 

15    nsecs_t currentTime = now();

16    int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);

17    <!--睡眠等待input事件-->

18    mLooper->pollOnce(timeoutMillis);

19}

我们继续深入看看dispatchOnceInnerLocked都做了什么,这里以其中的触摸事件代码分支为例:

1void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {

2        ...

3    case EventEntry::TYPE_MOTION: {

4        MotionEntry* typedEntry = static_cast<MotionEntry*>(mPendingEvent);

5        ...

6        done = dispatchMotionLocked(currentTime, typedEntry,

7                &dropReason, nextWakeupTime);

8        break;

9    }

10

11bool InputDispatcher::dispatchMotionLocked(

12        nsecs_t currentTime, MotionEntry* entry, DropReason* dropReason, nsecs_t* nextWakeupTime) {

13    ...     

14    Vector<InputTarget> inputTargets;

15    bool conflictingPointerActions = false;

16    int32_t injectionResult;

17    if (isPointerEvent) {

18    <!--关键点1 找到目标Window-->

19        injectionResult = findTouchedWindowTargetsLocked(currentTime,

20                entry, inputTargets, nextWakeupTime, &conflictingPointerActions);

21    } else {

22        injectionResult = findFocusedWindowTargetsLocked(currentTime,

23                entry, inputTargets, nextWakeupTime);

24    }

25    ...

26    <!--关键点2  派发-->

27    dispatchEventLocked(currentTime, entry, inputTargets);

28    return true;

29}

从以上代码可以看出,对于触摸事件会首先通过findTouchedWindowTargetsLocked找到目标Window,进而通过dispatchEventLocked将消息发送到目标窗口。

到目前为止第1)步的职责我们已经很清楚了,接着就需要关注事件是如何被精确的命中到Android里某个Window窗口的,这也就是WindowManagerService干的活。

在Android系统中,每块屏幕被抽象成一个DisplayContent对象,内部维护一个WindowList列表对象,用来记录当前屏幕中的所有窗口,因此,可以根据触摸事件的位置及窗口的属性来确定将事件发送到哪个窗口。

目标窗口也找到了,接下来的问题是如何通知用户的App目标窗口,由于这是2个不同的进程,必然涉及到IPC通信。同学们可能下意识的会想到Binder通信,毕竟Binder在Android中是使用最多的IPC手段了,不过Input事件处理这里采用的却不是Binder:高版本的采用的都是Socket的通信方式,而比较旧的版本采用的是Pipe管道的方式。

我们就说socket这种方式,那么这个Socket是怎么来的呢?其实还是要牵扯到WindowManagerService,在APP端向WMS请求添加窗口的时候,会伴随着Input通道的创建,窗口的添加而创建(具体代码略过)。

而APP端的监听消息的手段是:将socket添加到Looper线程的epoll数组中去,一有消息到来Looper线程就会被唤醒,并获取事件内容。

这样一来,整个链条就打通了,一步一步通过事件的读取,派发,寻找目标窗口,IPC通知目标窗口,把触摸事件包装成message进入UI线程的message,激活UI线程的消息机制进行事件处理。

注:这篇文章绝大部分想法来源于“看书的小蜗牛”,我只是做了加工处理,并非100%原创。在此声明和感谢!

更详细的,请见https://www.jianshu.com/p/f05d6b05ba17

有热爱Android技术的同学,欢迎加微信公众号 xh18310039919。用诙谐的方式学习Android硬核知识点。

相关文章

网友评论

      本文标题:一切从android的handler说起(五)之触摸事件模型

      本文链接:https://www.haomeiwen.com/subject/tjvksctx.html