Android系統原始碼分析-事件收集

Jensen95發表於2019-03-02

前言

之前專案中涉及到對於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;
}
複製程式碼
  • 通過epoll_wait,得到相應裝置檔案發生變化的事件
  • 根據事件型別獲取相應裝置id
  • 從相應裝置檔案中讀取內容,對讀出內容進行判斷,判斷是否有錯誤,是否可用
  • 判斷無誤,對事件內容進行包裝,加入到返回事件中

參考文章

用 inotify 監控 Linux 檔案系統事件 【Linux學習】epoll詳解

相關文章