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

阅读本文大概需要 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硬核知识点。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容

  • 今天是大年三十,也是传统的中秋佳节。中国人都有个传统,在每年春节的这个时候,无论远离家乡多远,都要踏上回家的路,跟...
    A一心向上阅读 169评论 0 0
  • Logo形象与品牌年轻化 1,外界不少对华与华操盘的海底捞logo突变的评价,具体设计理念可否描述一下?对比以前的...
    华杉2009阅读 3,549评论 0 8
  • 【日精进打卡第45天】 【知~学习】 《六项精进》2遍 共125遍 《大学》2遍 共79遍 【经典名句分享】 人最...
    周明善阅读 123评论 0 0
  • 今天学习bpmf和aoeiuv带调拼读,很难,熊孩子很累,我更累,身累,心累。 心情特别不好。 课堂就是一个老师的...
    陌上花开hlx阅读 358评论 0 1