Android系统源码分析-事件收集

前言

之前项目中涉及到对于Android事件的转发处理相关的需求,因此对于这部分的系统源码进行了分析,从写入设备文件到我们的应用层如何获取到事件,传递事件,消耗事件。整个传递机制源码进行了分析,以下为对于相关代码的梳理过程中的一些代码剖析记录。

针对事件的分析,这里以触摸屏事件为例,这也是我们最常用的一个事件处理,这里首先抛出我们应用层相关使用代码的例子,然后在来看事件管理的服务的启动到如何读取事件,事件的整个分发流程机制,如何一步步的走到我们的应用层中,对于事件的源码分析,本次分为两个部分,一个是内核层,一个是应用层。

事件收集分发概述

我们平时的开发中,对于应用层的处理,主要在于为View设置触摸监听器,然后从其中的回调函数中得到我们所需要的相关的触摸的数据。我们可以对View进行设置相应的触摸监听器。

@Override
public boolean onTouch(View v, MotionEvent event) {
    float x = event.getX();
    float y = event.getY();
    return true;
}

从这里,我们可以获取到触摸事件的相关信息。那么问题来了,这个事件是从哪里传递来的呢?也就是说这个函数'onTouch'是被谁调用的呢?事件是如何传递到该View的?带着这些问题,我们自低向上,从设备文件中事件信息的写入到事件在上层应用的分发做一个系统的分析。

事件收集流程

事件收集流程

InputManagerService的创建与启动

创建InputManagerService

对于Android Framework层的一些service,都是在SystemServer进程中创建的。和大多数的Service一样,InputManager它也是在SystemServer中创建。InputManager负责对于输入事件的相应处理,在SystemServer的startOtherService中,进行了一些Service的创建。(对于SystemServer启动相关在后续也会进行介绍们这里先着于InputManagerService相关。)

InputManager inputManager = new InputManagerService(context);
wm = WindowManagerService.main(context, inputManager,
                    mFactoryTestMode != FactoryTest.FACTORY_TEST_LOW_LEVEL,
                    !mFirstBoot, mOnlyCore);
ServiceManager.addService(Context.WINDOW_SERVICE, wm);
ServiceManager.addService(Context.INPUT_SERVICE, inputManager);
inputManager.setWindowManagerCallbacks(wm.getInputMonitor());
inputManager.start();

首先创建了一个InputManagerService的实例,然后将该服务加入到ServiceManger中,同时为其设置了窗口的输入监听器,然后调用该服务的start方法。这里我们从其构造函数开始,然后再看一下它start方法的实现。

public InputManagerService(Context context) {
    this.mContext = context;
    this.mHandler = new InputManagerHandler(DisplayThread.get().getLooper());

     mPtr = nativeInit(this, mContext, mHandler.getLooper().getQueue());

     ....

    LocalServices.addService(InputManagerInternal.class, new LocalService());
 }

首先创建了一个InputManagerHandler,同时传入了DisplayThread的looper,这里的InputManagerHandler为InputManager的一个内部类,其中进行了一些消息的处理,调用native方法,nativeInit。其native实现在framework/services/core/jni下的com_android_server_input_InputManagerService.cpp

创建NativeInputManager

其nativeInit的实现如下

static jlong nativeInit(JNIEnv* env, jclass /* clazz */,
        jobject serviceObj, jobject contextObj, jobject messageQueueObj) {
    sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
    if (messageQueue == NULL) {
        jniThrowRuntimeException(env, "MessageQueue is not initialized.");
        return 0;
    }

    NativeInputManager* im = new NativeInputManager(contextObj, serviceObj,
            messageQueue->getLooper());
    im->incStrong(0);
    return reinterpret_cast<jlong>(im);
}

获取传递的消息队列,然后根据其创建本地的NativeInputManager。

创建EventHub,InputManager

对于NativeInputManger实例的创建。其构造函数如下所示。

NativeInputManager::NativeInputManager(jobject contextObj,
        jobject serviceObj, const sp<Looper>& looper) :
        mLooper(looper), mInteractive(true) {
    JNIEnv* env = jniEnv();
     ...

    mInteractive = true;
    sp<EventHub> eventHub = new EventHub();
    mInputManager = new InputManager(eventHub, this, this);
}

创建了一个EventHub,同时利用EventHub来创建了一个InputManger实例。InputManger在framework/services/inputflinger下,

创建InputDispatcher,InputReader

InputManger的构造函数代码如下:

InputManager::InputManager(
        const sp<EventHubInterface>& eventHub,
        const sp<InputReaderPolicyInterface>& readerPolicy,
        const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {
    mDispatcher = new InputDispatcher(dispatcherPolicy);
    mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
    initialize();
}

通过构造函数,我们可以看到,这里创建了一个InputDispatcher根据传入的分发策略,然后创建了InputReader,传递了读的策略和Dispatcher还是有EventHub。接下来调用了initialize方法。

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

将上面创建的读和分发事件的核心类传入我们所创建的线程之中。到此,在我们的SystemServer中,对于InputManager的创建已经完成,接下来调用了InputManager的start方法,顾名思义,是对于这个InputManger服务的开始。代码中的相关实现。

开启InputReaderThread,InputDispatcherThread

核心调用

nativeStart(mPtr);
static void nativeStart(JNIEnv* env, jclass /* clazz */, jlong ptr) {
    NativeInputManager* im = reinterpret_cast<NativeInputManager*>(ptr);

    status_t result = im->getInputManager()->start();
}

nativeStart函数中调用了InputManager的start方法。该方法执行如下

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

至此,我们可以看到start方法所做的事情是启动了在之前创建InputManger的时候,创建的DispatcherThread和ReaderThread.

上述创建启动流程图。

InputManager服务启动流程图

InputManager是系统事件处理的核心,它使用了两个线程,一个是InputReaderThread,读和处理未加工的输入事件然后发送事件到由DispatcherThread管理的队列中。InputDispatcherThread等待着在队列上新来的事件,然后将它们分发给应用程序。而对于上层的类只是对这的一个包装。所以要对输入服务有个细致的了解,对于InputManager类的剖析至关重要。

当前InputManager状态

至此,我们可以看到InputManager开启两个线程,同时创建了Dispatcher核心类和InputReader核心类。

EventHub事件收集

在NativeInputManager的构造函数中,创建了EventHub,同时将其作为参数传递给InputManager,在InputManager构造函数中,创建InputRead的时候,传递了EventHub,EventHub的作用是将来源不同的各种信息,转化成为一种类型的信息,然后将这些信息提交到上层,给上层做处理。也就是说在输入设备中,各种类型的输入信息,通过EventHub进行一个处理之后,将信息转化为同一种类型的信息传递到上层。EventHub集合来自系统的所有输出事件,包括模拟的一些事件,此外,其自身创造一些加数据,来表明设备的添加或者删除。事件会被合成,当设备被添加或者删除的时候。

设备文件处理

设备文件也称为设备特定文件。设备文件用来为操作系统和用户提供它们代表的设备接口。所有的 Linux 设备文件均位于 /dev 目录下,是根 (/) 文件系统的一个组成部分,因为这些设备文件在操作系统启动过程中必须可以使用。
关于这些设备文件,要记住的一件重要的事情,就是它们大多不是设备驱动程序。更准确地描述来说,它们是设备驱动程序的门户。数据从应用程序或操作系统传递到设备文件,然后设备文件将它传递给设备驱动程序,驱动程序再将它发给物理设备。反向的数据通道也可以用,从物理设备通过设备驱动程序,再到设备文件,最后到达应用程序或其他设备。

设备文件和设备关系

设备文件分为字符设备文件和块设备文件

  • 字符设备
    无缓冲且只能顺序存取
  • 块设备
    有缓冲且可以随机(乱序)存取
    块设备文件,对访问系统硬件部件提供了缓存接口。它们提供了一种通过文件系统与设备驱动通信的方法。有关于块文件一个重要的性能就是它们能在指定时间内传输大块的数据和信息。

无论是哪种设备,在 /dev 目录下都有一个对应的文件(节点),并且每个设备文件都必须有主/次设备号,主设备号相同的设备是同类设备,使用同一个驱动程序(虽然目前的内核允许多个驱动共享一个主设备号,但绝大多数设备依然遵循一个驱动对应一个主设备号的原则)。

流的监控

在对源码分析之前,这里先讲一下epoll,当我们的应用程序要对多个输入流进行监控的时候,处理多个输入流来的数据,我们可以采取的一个方式是,对于这每一个流进行遍历,检测到有数据,读出。

while true {
  for i in stream[]; {
    if i has data
    read until unavailable
  }
}

如果有数据,则读取直到其中没有数据为止。该种方式也称为忙轮询,但是当输入流一直没有数据的时候,就是在空消耗CPU,因此产生了select,poll,epoll。select和poll相似,其实现为,当没有数据的时候阻塞,一旦有了数据,通过轮询的方式读取数据。

while true {
  select(streams[])
  for i in streams[] {
    if i has data
    read until unavailable
  }
}

但是当我们的流比较多的时候,对于轮询检测每一个输入流,也是比较消耗的,因此,epoll产生了,当没有数据的时候,岂会阻塞,但是当有数据的时候,epoll能够把哪一个流发生了怎样的事件发送给我们,这样我们对返回的流进行操作就是有意义的了。

EventHub的创建

先从EventHub的构造函数看起。

EventHub::EventHub(void) :
      mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD), mNextDeviceId(1), mControllerNumbers(),
        mOpeningDevices(0), mClosingDevices(0),
        mNeedToSendFinishedDeviceScan(false),
        mNeedToReopenDevices(false), mNeedToScanDevices(true),
        mPendingEventCount(0), mPendingEventIndex(0), mPendingINotify(false) {

    acquire_wake_lock(PARTIAL_WAKE_LOCK, WAKE_LOCK_ID);
    //创建一个epoll句柄
    mEpollFd = epoll_create(EPOLL_SIZE_HINT);

    mINotifyFd = inotify_init();

    //监视dev/input目录的变化删除和创建变化
    int result = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);

    struct epoll_event eventItem;
    memset(&eventItem, 0, sizeof(eventItem));
    eventItem.events = EPOLLIN;
    eventItem.data.u32 = EPOLL_ID_INOTIFY;

    //把inotify的句柄加入到epoll监测
    result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);

    //创建匿名管道
    int wakeFds[2];
    result = pipe(wakeFds);
    mWakeReadPipeFd = wakeFds[0];
    mWakeWritePipeFd = wakeFds[1];

    //将管道的读写端设置为非阻塞
    result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);
    result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);

    eventItem.data.u32 = EPOLL_ID_WAKE;

    //将管道的读端加入到epoll监测
    result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);

    int major, minor;
    getLinuxRelease(&major, &minor);
    // EPOLLWAKEUP was introduced in kerel 3.5
    mUsingEpollWakeup = major > 3 || (major == 3 && minor >= 5);
}

构造函数首先创建了epoll句柄,然后创建了inotify句柄,然后创建了一个匿名管道,并将匿名管道设置为非阻塞,inotify是Linux下的一个监控目录和文件变化的机制,这里监控了/dev/input目录,当这个目录发生变化,就表明有输入设备加入或者移除。至此,EventHub只是进行了一些监控操作的处理。而对于EventHub相关事件处理部分的调用则是在创建ReaderThread的时候。

ReaderThread是继承自Android的Thread实现。下面是一个创建Android中Native 线程的方式。

namespace android {  
  
class MyThread: public Thread {  
public:  
    MyThread();  
    //virtual ~MyThread();  
    //如果返回true,循环调用此函数,返回false下一次不会再调用此函数  
    virtual bool threadLoop();  
};  
}  
  • inotify
    mINotifyFd = inotify_init();

从 Linux 2.6.13 内核开始,Linux 就推出了 inotify,允许监控程序打开一个独立文件描述符,并针对事件集监控一个或者多个文件,例如打开、关闭、移动/重命名、删除、创建或者改变属性。inotify_init
是用于创建一个 inotify 实例的系统调用,并返回一个指向该实例的文件描述符。

  • 使用 inotify_init 打开一个文件描述符
  • 添加一个或者多个监控
  • 等待事件
  • 处理事件,然后返回并等待更多事件
  • 当监控不再活动时,或者接到某个信号之后,关闭文件描述符,清空,然后退出
    int result = inotify_add_watch(mINotifyFd, DEVICE_PATH, IN_DELETE | IN_CREATE);
  • inotify_add_watch

增加对文件或者目录的监控,并指定需要监控哪些事件。标志用于控制是否将事件添加到已有的监控中,是否只有路径代表一个目录才进行监控,是否要追踪符号链接,是否进行一次性监控,当首次事件出现后就停止监控。

获取事件
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
    RawEvent* event = buffer;
    size_t capacity = bufferSize;
     for(;;) {
        ....
      while (mPendingEventIndex < mPendingEventCount) {
         const struct epoll_event& eventItem = mPendingEventItems[mPendingEventIndex++];
        .....
       ssize_t deviceIndex = mDevices.indexOfKey(eventItem.data.u32);
     if (eventItem.events & EPOLLIN) {
         int32_t readSize = read(device->fd, readBuffer,
                        sizeof(struct input_event) * capacity);
        if (readSize == 0 || (readSize < 0 && errno == ENODEV)) {
           // 设备被移除,关闭设备
           deviceChanged = true;
           closeDeviceLocked(device);
         } else if (readSize < 0) {
             //无法获得事件
             if (errno != EAGAIN && errno != EINTR) {
                 ALOGW("could not get event (errno=%d)", errno);
             }
         } else if ((readSize % sizeof(struct input_event)) != 0) {
            //获得事件的大小非事件类型整数倍
            ALOGE("could not get event (wrong size: %d)", readSize);
       } else {
           int32_t deviceId = device->id == mBuiltInKeyboardId ? 0 : device->id;
          //计算读入了多少事件
           size_t count = size_t(readSize) / sizeof(struct input_event);
           for (size_t i = 0; i < count; i++) {
               struct input_event& iev = readBuffer[i];
               if (iev.type == EV_MSC) {
                 if (iev.code == MSC_ANDROID_TIME_SEC) {
                     device->timestampOverrideSec = iev.value;
                     continue;
                  } else if (iev.code == MSC_ANDROID_TIME_USEC) {
                     device->timestampOverrideUsec = iev.value;
                     continue;
                  }
               }
              //事件时间相关计算,时间的错误可能会导致ANR和一些bug。这里采取一系列的防范 
               .........
             event->deviceId = deviceId;
             event->type = iev.type;
             event->code = iev.code;
             event->value = iev.value;
             event += 1;
             capacity -= 1;
          }
        if (capacity == 0) {
          //每到我们计算完一个事件,capacity就会减1,如果为0。则表示  结果缓冲区已经满了,
      //需要重置开始读取时间的索引值,来读取下一个事件迭代                    
           mPendingEventIndex -= 1;
           break;
      }
 } 
    //表明读到事件了,跳出循环
    if (event != buffer || awoken) {
            break;
     }
     mPendingEventIndex = 0;
     int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);
       if (pollResult == 0) {
          mPendingEventCount = 0;
          break;
       }
      //判断是否有事件发生
       if (pollResult < 0) {
          mPendingEventCount = 0;
        } else {
            //产生的事件的数目
          mPendingEventCount = size_t(pollResult);
        }
    }
    //产生的事件数目
    return event - buffer;
}
  • 首次启动调用该方法的时候,将遍历所有目录,Device中,同时将其加入到mDevices中,同时将各目录加入到epoll监控中。
  • 从epoll中读出相应的事件流。
  • 根据事件类型获取相应设备id。
  • 从相应设备文件中读取内容,对读出内容进行判断,判断是否有错误,是否可用。
  • 判断无误,对事件内容进行包装,加入到返回事件中。

参考文章

用 inotify 监控 Linux 文件系统事件
【Linux学习】epoll详解

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