头条Android高级开发岗二面:按下手机的 Home 键,有哪些动作和事件发生?

缘起

后台有位小伙伴分享了一个头条的面试题:按下手机的 Home 键,有哪些动作和事件发生?

今天我们就来分析一下,本文源码基于 Android - 28。标准答案放在文末~

作者:Android面试官
链接:https://juejin.im/post/6844904193950498830

事件的分类

安卓系统中的事件,主要有以下几种:

  • 按键事件(KeyEvent) 由物理按键产生的事件,如:Home, Back, Volume Up, Volume Down, Camera 等。今天主要分析的就是这类事件。
  • 触摸事件(TouchEvent) 在屏幕上点击拖动,以及由它们组合的各种事件。
  • 鼠标事件(MouseEvent) 鼠标操作产生的事件
  • 轨迹球事件 (TrackBallEvent) 知道轨迹球的,怕不是要暴露年龄

安卓针对上面这些事件共性,提取了一个统一的抽象类 InputEvent 。InputEvent 提供了几个常用的抽象方法,比如 getDevice() 获得当前事件的“硬件源”,getEventTime() 获取事件发生的时间。

InputEvent 有两个子类:

  • KeyEvent 用于描述按键事件
  • MotionEvent 用来描述 Movement 类型的事件(通过 mouse, pen, finger, trackball 产生)。

而我们要监听这些事件一般也是通过对 View 设置相应的监听实现

setOnKeyListener(OnKeyListener)
setOnTouchListener(OnTouchListener)
...

或者也可以直接复写相关的方法

boolean onKeyDown(int keyCode, KeyEvent event)
boolean onTouchEvent(MotionEvent event)
....

事件处理的准备工作

事件处理设计的整体思路是驱动层会有一个消息队列来存放事件,会有一个 Reader 来不停的读取事件,一个 Dispatcher 来分发消息队列中的事件。Dispatcher 分发的事件最后会通过 jni 上报到 InputManagerService,然后通过接口最后传递给PhoneWindow,PhoneWindow 再根据不同的事件类型来做不同的处理。

我们先看一下 Reader、Dispatcher 是怎么来的。

SystemServer 在 startOtherServices() 方法中启动 InputMangerService

InputManagerService inputManager = new InputManagerService(context);
inputManager.setWindowManagerCallbacks(wm.getInputMonitor());
inputManager.start();
复制代码

接下来我们看一下 InputMangerService 的构造方法

public InputManagerService(Context context) {
    this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper()); 
    mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());
    ...
}

主要是调用 nativeInit 方法传入一个消息队列,nativeInit 方法是为了去初始化一些 native 对象。最终是为了 new 一个 native 层的 InputManager 。 调用链如(想要了解详情的同学可以在相应的源码类里查看):

   new InputManagerService() // InputManagerService.java
-> nativeInit(..., queue) // com_android_server_input_InputManagerService.cpp
-> new NativeInputManager(..., looper) // com_android_server_input_InputManagerService.cpp
-> new InputManager(eventHub, ...) //InputManager.cpp      

重点就在这个 InputManager 里

InputManager::InputManager(...) {
    mDispatcher = new InputDispatcher(dispatcherPolicy);
    mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
    initialize();
}

void InputManager::initialize() {
    mReaderThread = new InputReaderThread(mReader);
    mDispatcherThread = new InputDispatcherThread(mDispatcher);
}

我们可以看到,在 InputManager 里准备好了 mReader、mDispatcher,以及相关的两个线程,那么接下来当然就是把线程跑起来,好去读事件和分发事件。

启动事件读取和分发线程的调用链如下:

   SystemServer.startOtherServices()	
-> inputManagerService.start()
-> nativeStart() // com_android_server_input_InputManagerService.cpp
-> InputManager.start() // InputManager.cpp

最后的这个 start 方法很简单,就是把两个线程跑起来

status_t InputManager::start() {
    status_t result = mDispatcherThread->run("InputDispatcher", PRIORITY_URGENT_DISPLAY);
    result = mReaderThread->run("InputReader", PRIORITY_URGENT_DISPLAY);
}

至此,事件处理工作所需要的对象和线程都已经准备好了。前面那些代码看完记不住就算了,记住这句话:SystemServer 启动 IMS 时,会创建一个 native 的 InputManager 对象,这个 InputManager 会通过 mReader 不断读事件,再通过 mDispatcher 不断分发事件

接下来我们看下,事件是怎么读取和分发的。

事件的读取

InputReaderThread 等待按键消息到来,该 Thread 在 threadLoop 中无尽的调用 InputReader 的 loopOnce 方法。

bool InputReaderThread::threadLoop() {
    mReader->loopOnce();
    return true;
}

在 loopOnce 方法中会通过 EventHub 来获取事件,并放入 buffer 中:

void InputReader::loopOnce() {
    // EventHub 从驱动读取事件
    size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
    ...
    if (count) {
    	 // 获取的事件是 RawEvent,需要处理成 KeyEvent、MotionEvent 等各种类型等
       processEventsLocked(mEventBuffer, count);
    }
    // 将队列中事件刷给监听器,监听器实际上就是 InputDispatcher 事件分发器。
    mQueuedListener->flush();
}

InputDispatcher 收到事件后调用,如果是 KeyEvent 会调用 notifyKey,如果是 MotionEvent 则会调用 notifyMotion

void InputDispatcher::notifyKey(const NotifyKeyArgs* args) {
    KeyEvent event;
    event.initialize(args->deviceId, args->source, args->action,
            flags, keyCode, args->scanCode, metaState, 0,
            args->downTime, args->eventTime);

    // 通过 NatvieInputManager 在 Event 入队前做一些处理
    mPolicy->interceptKeyBeforeQueueing(&event, /*byref*/ policyFlags);

    ...
    KeyEntry* newEntry = new KeyEntry(args->eventTime, args->source,
                args->action, flags, keyCode,...)
    // 事件放入队尾
    needWake = enqueueInboundEventLocked(newEntry);
}

以上,便是 InputReader 获取到设备事件通知 InputDispatcher 并存放到事件队列中的流程。

事件的分发

下面将介绍 InputDispatcher 如何从事件队列中读取事件并分发出去。

首先在 InputDispatcherThread 的 threadLoop 中无尽的调用 dispatchOnce 方法

bool InputDispatcherThread::threadLoop() {
    mDispatcher->dispatchOnce();
    return true;
}

该方法两个功能:

  • 调用 dispatchOnceInnerLocked 分发事件;
  • 调用 runCommandsLockedInterruptible 来处理 CommandQueue 中的命令,出队并处理,直到队列为空。
void InputDispatcher::dispatchOnce() {
     if (!haveCommandsLocked()) {
         dispatchOnceInnerLocked(&nextWakeupTime);
     }
     if (runCommandsLockedInterruptible()) {
         nextWakeupTime = LONG_LONG_MIN;
     }
  ...
}

在 dispatchOnceInnerLocked 中会处理多种类型的事件,这里关注按键类型的(其他如触摸,设备重置等事件流程稍有区别)。一通调用后到 PhoneWindowManager , 终于回到 java 了,

	 InputDispatcher::dispatchOnceInnerLocked
-> dispatchKeyLocked  
-> doInterceptKeyBeforeDispatchingLockedInterruptible     
-> NativeInputManager.interceptKeyBeforeDispatching
-> PhoneWindowManager.interceptKeyBeforeDispatching  
...

// 以下 NativeInputManager.doInterceptKeyBeforeDispatchingLockedInterruptible 的部分代码 
nsecs_t delay = mPolicy->interceptKeyBeforeDispatching(commandEntry->inputWindowHandle,
            &event, entry->policyFlags);
if (delay < 0) {
  // Home 事件将被拦截
  entry->interceptKeyResult = KeyEntry::INTERCEPT_KEY_RESULT_SKIP;    
} 
// 未被拦截的继续处理分发,本篇暂不分析

PhoneWindowManager

话不多说,继续看代码,离胜利不远了!源码的注释写得很清楚,我这边就不翻译了。真的不是因为懒,是想让你们提高点英语阅读水平 。(-> <-)

public long interceptKeyBeforeDispatching(KeyEvent event, ...) {
 // First we always handle the home key here, so applications
 // can never break it, although if keyguard is on, we do let
 // it handle it, because that gives us the correct 5 second
 // timeout.
 if (keyCode == KeyEvent.KEYCODE_HOME) {
     // If we have released the home key, and didn't do anything else
     // while it was pressed, then it is time to go home!
     if (!down) {
         cancelPreloadRecentApps();
         mHomePressed = false;
         ... 
         // Delay handling home if a double-tap is possible.
         if (mDoubleTapOnHomeBehavior != DOUBLE_TAP_HOME_NOTHING) {
             mHomeDoubleTapPending = true;
             mHandler.postDelayed(mHomeDoubleTapTimeoutRunnable,
                     ViewConfiguration.getDoubleTapTimeout());
             return -1;
         }
         handleShortPressOnHome(); //短按

         //-1 调用处的事件结果就会赋值 INTERCEPT_KEY_RESULT_SKIP
         return -1;
     }
   	 ...
     // Remember that home is pressed and handle special actions.
     if (repeatCount == 0) {
         mHomePressed = true;
         if (mHomeDoubleTapPending) {
             handleDoubleTapOnHome();//双击
         } else if (mDoubleTapOnHomeBehavior == DOUBLE_TAP_HOME_RECENT_SYSTEM_UI) {
             preloadRecentApps();//最近 app
         }
     } else if ((event.getFlags() & KeyEvent.FLAG_LONG_PRESS) != 0) {
         if (!keyguardOn) {
             handleLongPressOnHome(event.getDeviceId());//长按
         }
     }
     return -1;
}
}

Home 的相关事件都在这处理啦。接下来我们就看一下 handleShortPressOnHome 短按 Home 进入 Luncher 是怎么实现的吧。

private void handleShortPressOnHome() {
    ...
    // Go home! 坚持下,看完我们就 Go Home!
    launchHomeFromHotKey();
}

Go Home!

void launchHomeFromHotKey(final boolean awakenFromDreams, final boolean respectKeyguard) {
    if (respectKeyguard) {
        // 处理一些锁屏的情况,可能直接 return 
    }
    // no keyguard stuff to worry about, just launch home!
    if (mRecentsVisible) {
    	  // 延时后台的打开 Activity 的操作,避免打扰用户的操作
        // 虽然方法名 stop 实际实现是延时 5s
        ActivityManager.getService().stopAppSwitches();
        // Hide Recents and notify it to launch Home
        hideRecentApps(false, true);
    } else {
        // Otherwise, just launch Home
        startDockOrHome(true /*fromHomeKey*/, awakenFromDreams);
    }
}

最后看一下跳转到 Home 的一些细节

 void startDockOrHome(boolean fromHomeKey, boolean awakenFromDreams) {
     ActivityManager.getService().stopAppSwitches();
     // 关闭系统弹窗,如输入法
     sendCloseSystemWindows(SYSTEM_DIALOG_REASON_HOME_KEY);

     Intent dock = createHomeDockIntent();
     if (dock != null) { // 开启应用抽屉
         startActivityAsUser(dock, UserHandle.CURRENT);
         return;
     }

     intent = mHomeIntent //省略部分逻辑
     // 开启 Home 页面
     startActivityAsUser(intent, UserHandle.CURRENT);
 }

怎么回答

面试官:按下手机的 Home 键,有哪些动作和事件发生

答:按下 Home 键后,底层驱动会获取这个事件, IMS 通过 Reader 读取驱动捕获的事件,再通过 Dispatcher 对事件进行分发。Dispatcher 分发事件前,PhoneWindowManager 会对 Home 和其它系统事件进行拦截处理,其中短按 Home 键的处理有:关闭相应的系统弹窗,延迟其它待打开的 Activity,最后使用 Intent 打开 Home 或者 Dock 页面。

文末:

这份资料我从春招开始,就会将各博客、论坛。网站上等优质的Android开发中高级面试题收集起来,然后全网寻找最优的解答方案。每一道面试题都是百分百的大厂面经真题+最优解答。包知识脉络 + 诸多细节。
节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
给文章留个小赞,就可以免费领取啦~

戳我领取:Android对线暴打面试指南超硬核Android面试知识笔记3000页Android开发者架构师核心知识笔记

《960全网最全Android开发笔记》

《379页Android开发面试宝典》

包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

如何使用它?
1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数


《507页Android开发相关源码解析》

只要是程序员,不管是Java还是Android,如果不去阅读源码,只看API文档,那就只是停留于皮毛,这对我们知识体系的建立和完备以及实战技术的提升都是不利的。

真正最能锻炼能力的便是直接去阅读源码,不仅限于阅读各大系统源码,还包括各种优秀的开源库。

腾讯、字节跳动、阿里、百度等BAT大厂 2019-2020面试真题解析

资料已经上传在我的GitHub,或者关注后私信我【666】即可领取(无偿)。

资料收集不易,如果大家喜欢这篇文章,或者对你有帮助不妨多多点赞转发关注哦。文章会持续更新的。绝对干货!!!

对文章有何见解,或者有何技术问题,都可以在评论区一起留言讨论,一定会回复的。
也欢迎大家来我的B站找我玩,各类Android架构师进阶技术难点的视频讲解,任君白嫖。
B站直通车:https://space.bilibili.com/484587989

©️2020 CSDN 皮肤主题: 深蓝海洋 设计师:CSDN官方博客 返回首页