Androidタッチイベントの原理(InputManagerService)を10分で理解

17251 ワード

指のタッチスクリーンからMotionEventがActivityやViewに送られるまで、いったい何を経験したのだろうか.Androidでタッチした事件はいったいどうやって来たのだろうか.ソースはどこですか?本文は直感的に1つの全体の流れを説明して、甚だしきに至っては理解を求めないで、ただ理解を求めます.
Androidタッチイベントモデル
タッチイベントは必ず先にキャプチャしてからウィンドウに伝わるので、まずスレッドが絶えずスクリーンを傍受し、タッチイベントがあればイベントをキャプチャしなければならない.次に、複数のAPPの複数のインタフェースがユーザーに表示される可能性があるため、このイベントがそのウィンドウに通知されるかどうかを決定しなければならない.最後に、ターゲットウィンドウがどのようにイベントを消費するかという問題です.
InputManagerServiceはAndroidが様々なユーザー操作を処理するために抽象的なサービスであり、自身はBinderサービスエンティティと見なすことができ、SystemServerプロセスの開始時にインスタンス化され、ServiceManagerに登録されているが、このサービスは対外的には主にいくつかの入力装置の情報を提供する役割を果たし、Binderサービスとしての役割は比較的小さい.
private void startOtherServices() {
        ...
        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);
       ...
       }

InputManagerServiceとWindowManagerServiceはほぼ同時に追加されており、両者はほぼ相生関係であることもある程度説明できるが、タッチイベントの処理も確かに2つのサービスに関連している.最良の証拠はWindowManagerServiceがInputManagerServiceの参照を直接握る必要があることであり、上記の処理モデルと照らし合わせると、InputManagerServiceは主にタッチイベントの収集を担当し、WindowManagerServiceはターゲットウィンドウの検索を担当します.次に、InputManagerServiceがタッチイベントの収集をどのように完了するかを見てみましょう.
タッチイベントのキャプチャ方法
InputManagerServiceでは、タッチイベントを読み取るためのスレッドが個別に開きます.
NativeInputManager::NativeInputManager(jobject contextObj,
        jobject serviceObj, const sp& looper) :
        mLooper(looper), mInteractive(true) {
  	 ...
    sp eventHub = new EventHub();
    mInputManager = new InputManager(eventHub, this, this);
}

ここにEventHubがあります.主にLinuxのinotifyとepollメカニズムを利用して、デバイスイベントを傍受します.デバイスの挿抜や各種タッチ、ボタンイベントなどを含めて、異なるデバイスのハブと見なすことができます.主に/dev/inputディレクトリの下のデバイスノードに向いています.例えば/dev/input/event 0のイベントは入力イベントです.EventHubのgetEventsを使用すると、イベントをリスニングして取得できます.
new InputManagerでは、新しいInputReaderオブジェクトとInputReader Thread Loopスレッドが作成されます.このloopスレッドの主な役割は、EventHubのgetEventsからInputイベントを取得することです.
InputManager::InputManager(
        const sp& eventHub,
        const sp& readerPolicy,
        const sp& dispatcherPolicy) {
    
    mDispatcher = new InputDispatcher(dispatcherPolicy);
    
    mReader = new InputReader(eventHub, readerPolicy, mDispatcher);
    initialize();
}

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

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

void InputReader::loopOnce() {
	    int32_t oldGeneration;
	    int32_t timeoutMillis;
	    bool inputDevicesChanged = false;
	    Vector inputDevices;
	    {  
	  ...
	    size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);
	   ....
	       processEventsLocked(mEventBuffer, count);
	   ...
	   
	    mQueuedListener->flush();
	}

上記のフローにより,入力イベントを読み取ることができ,processEventsLockedを経てRawEventに初歩的にカプセル化され,最後に通知を送り,メッセージの配布を要求する.以上でイベント読み込みの問題は解決したが,以下ではイベントの配布に重点を置いてみる.
事件の配布
InputManagerを新規作成する際には、イベント読取スレッドを作成するだけでなく、イベント配信スレッドを作成します.直接読取スレッドに配信することもできますが、時間がかかることは間違いありません.イベントのタイムリーな読み取りには不利です.そのため、イベントの読み取りが完了したら、直接配信スレッドに通知します.スレッドを配信して処理してください.これにより、スレッドの読み取りがより迅速になり、イベントの紛失を防ぐことができます.そのため、InputManagerのモデルは次のようになります.
InputReaderのmQueuedListenerは実はInputDispatcherオブジェクトなので、mQueuedListener->flush()はInputDispatcherイベントの読み取りが完了したことを通知し、イベントを配布することができます.InputDispatcher Threadは典型的なLooperスレッドで、nativeのLooperに基づいてHanlderメッセージ処理モデルを実現し、Inputイベントが来たら処理イベントを起動させ、処理が終わった後、睡眠待ちを続け、簡略化コードは以下の通りである.
bool InputDispatcherThread::threadLoop() {
    mDispatcher->dispatchOnce();
    return true;
}

void InputDispatcher::dispatchOnce() {
    nsecs_t nextWakeupTime = LONG_LONG_MAX;
    {  
      
        if (!haveCommandsLocked()) {
            dispatchOnceInnerLocked(&nextWakeupTime);
        }
       ...
    } 
    nsecs_t currentTime = now();
    int timeoutMillis = toMillisecondTimeoutDelay(currentTime, nextWakeupTime);
    
    mLooper->pollOnce(timeoutMillis);
}

以上が配布スレッドのモデルであり、dispatchOnceInnerLockedは具体的な配布処理ロジックであり、ここではその分岐点を見て、イベントに触れます.
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
	    ...
    case EventEntry::TYPE_MOTION: {
        MotionEntry* typedEntry = static_cast(mPendingEvent);
        ...
        done = dispatchMotionLocked(currentTime, typedEntry,
                &dropReason, nextWakeupTime);
        break;
    }

bool InputDispatcher::dispatchMotionLocked(
        nsecs_t currentTime, MotionEntry* entry, DropReason* dropReason, nsecs_t* nextWakeupTime) {
    ...     
    Vector inputTargets;
    bool conflictingPointerActions = false;
    int32_t injectionResult;
    if (isPointerEvent) {
    
        injectionResult = findTouchedWindowTargetsLocked(currentTime,
                entry, inputTargets, nextWakeupTime, &conflictingPointerActions);
    } else {
        injectionResult = findFocusedWindowTargetsLocked(currentTime,
                entry, inputTargets, nextWakeupTime);
    }
    ...
    
    dispatchEventLocked(currentTime, entry, inputTargets);
    return true;
}

以上のコードから分かるように,タッチイベントに対してはまずfindTouchedWindowTargetsLockedによってターゲットWindowが見つかり,さらにdispatchEventLockedによってターゲットウィンドウにメッセージが送信され,次にターゲットウィンドウがどのように見つかり,このウィンドウリストがどのように維持されているかを見る.
タッチイベントのターゲットウィンドウを見つける方法
Androidシステムは同時に複数の画面をサポートすることができ、各画面は1つのDisplayContentオブジェクトとして抽象化され、内部では現在の画面のすべてのウィンドウを記録するためにWindowListリストオブジェクトを維持し、ステータスバー、ナビゲーションバー、アプリケーションウィンドウ、サブウィンドウなどを含む.タッチイベントについては、可視ウィンドウに関心を持ち、adb shell dumpsys SurfaceFlingerで可視ウィンドウの組織形式を見てみましょう.
では、タッチイベントに対応するウィンドウをどのように見つけるのか、ステータスバー、ナビゲーションバー、アプリケーションウィンドウなのか、このときDisplayContentのWindowListが機能し、DisplayContentはすべてのウィンドウの情報を握っているので、タッチイベントの位置や窓口の属性によってイベントをどのウィンドウに送信するかを決定することができ、もちろん、その細部は一言よりも複雑で、ウィンドウの状態、透明性、スクリーン分割などの情報と関係があります.以下、簡単に見て、主観的な理解の流れを達成すればいいのです.
int32_t InputDispatcher::findTouchedWindowTargetsLocked(nsecs_t currentTime,
        const MotionEntry* entry, Vector& inputTargets, nsecs_t* nextWakeupTime,
        bool* outConflictingPointerActions) {
        ...
        sp newTouchedWindowHandle;
        bool isTouchModal = false;
        
        size_t numWindows = mWindowHandles.size();
        for (size_t i = 0; i < numWindows; i++) {
            sp windowHandle = mWindowHandles.itemAt(i);
            const InputWindowInfo* windowInfo = windowHandle->getInfo();
            if (windowInfo->displayId != displayId) {
                continue; // wrong display
            }
            int32_t flags = windowInfo->layoutParamsFlags;
            if (windowInfo->visible) {
                if (! (flags & InputWindowInfo::FLAG_NOT_TOUCHABLE)) {
                    isTouchModal = (flags & (InputWindowInfo::FLAG_NOT_FOCUSABLE
                            | InputWindowInfo::FLAG_NOT_TOUCH_MODAL)) == 0;
	     
                    if (isTouchModal || windowInfo->touchableRegionContainsPoint(x, y)) {
                        newTouchedWindowHandle = windowHandle;
                        break; // found touched window, exit window loop
                    }
                }
              ...

mWindowHandlesはすべてのウィンドウを表しています.findTouchedWindowTargetsLockedはmWindowHandlesからターゲットウィンドウを見つけることです.ルールが複雑すぎて、とにかくクリック位置やウィンドウZ orderなどの特性によって確定し、興味があれば自分で分析することができます.しかし、ここで関心を持つ必要があるのはmWindowHandlesです.それはどのように来たのか、またウィンドウが削除されたときにどのように最新のものを維持しているのか.ここでWindowManagerServiceとのインタラクションについて質問ですが、mWindowHandlesの値はInputDispatcher::setInputWindowsで設定されています.
void InputDispatcher::setInputWindows(const Vector >& inputWindowHandles) {
        ...
        mWindowHandles = inputWindowHandles;
       ...

誰がこの関数を呼び出しますか?本当の入り口はWindowManagerServiceのInputMonitorがInputDispatcher::setInputWindowsを呼び出します.このタイミングは主にウィンドウの変更削除などの論理に関連しています.addWindowを例に挙げます.
WindowManagerServiceとInputManagerServiceが相補的である理由は、上記のプロセスから理解できます.ここでは、ターゲットウィンドウをどのように見つけるか、次にターゲットウィンドウにイベントを送信するかの問題が解決しました.
ターゲットウィンドウにイベントを送信する方法
ターゲットウィンドウが見つかり、イベントもカプセル化され、残りはターゲットウィンドウに通知されますが、最も明らかな問題は、現在すべての論理がSystemServerプロセスであり、通知するウィンドウがAPP側にあるユーザープロセスである場合、どのように通知するかということです.無意識にBinder通信を思い浮かべるかもしれませんが、BinderはAndroidで最も多くのIPC手段を使用していますが、Inputイベント処理ではBinderではありません.高バージョンではSocketの通信方式が採用されていますが、古いバージョンではPipeパイプの方式が採用されています.
void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,
        EventEntry* eventEntry, const Vector& inputTargets) {
    pokeUserActivityLocked(eventEntry);
    for (size_t i = 0; i < inputTargets.size(); i++) {
        const InputTarget& inputTarget = inputTargets.itemAt(i);
        ssize_t connectionIndex = getConnectionIndexLocked(inputTarget.inputChannel);
        if (connectionIndex >= 0) {
            sp connection = mConnectionsByFd.valueAt(connectionIndex);
            prepareDispatchCycleLocked(currentTime, connection, eventEntry, &inputTarget);
        } else {
        }
    }
}

コードを層ごとに下を見ると、最後にInputChannelのsendMessage関数が呼び出され、最もsocketを通じてAPP側に送信されることがわかります(Socketはどのように来たのかは後で分析します).
このSocketはどうやって来ましたか?あるいは両端通信のペアSocketはどうやって来たのでしょうか.実際にはWindowManagerServiceにも関連しています.APP側がWMSにウィンドウの追加を要求すると、Inputチャネルの作成に伴い、ウィンドウの追加は必ずViewRootImplのsetView関数を呼び出します.
ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
				...
            requestLayout();
            if ((mWindowAttributes.inputFeatures
                    & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
                 
                mInputChannel = new InputChannel();
            }
            try {
                mOrigWindowType = mWindowAttributes.type;
                mAttachInfo.mRecomputeGlobalAttributes = true;
                collectViewAttributes();
                
                res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                        getHostVisibility(), mDisplay.getDisplayId(),
                        mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                        mAttachInfo.mOutsets, mInputChannel);
            }...
            
            if (mInputChannel != null) {
                if (mInputQueueCallback != null) {
                    mInputQueue = new InputQueue();
                    mInputQueueCallback.onInputQueueCreated(mInputQueue);
                }
                mInputEventReceiver = new WindowInputEventReceiver(mInputChannel,
                        Looper.myLooper());
            }

IWindowSessionでaidl定義ではInputChannelはoutタイプであり,すなわちサービス側が充填する必要があるが,次にサービス側WMSがどのように充填するかを見る.
public int addWindow(Session session, IWindow client, int seq,
        WindowManager.LayoutParams attrs, int viewVisibility, int displayId,
        Rect outContentInsets, Rect outStableInsets, Rect outOutsets,
        InputChannel outInputChannel) {            
		  ...
        if (outInputChannel != null && (attrs.inputFeatures
                & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
            String name = win.makeInputChannelName();
            
            InputChannel[] inputChannels = InputChannel.openInputChannelPair(name);
            
            win.setInputChannel(inputChannels[0]);
            
            inputChannels[1].transferTo(outInputChannel);
            
            mInputManager.registerInputChannel(win.mInputChannel, win.mInputWindowHandle);
        }

WMSはまずsocketpairをフルデュプレクスチャネルとして作成し、ClientとServerのInputChannelにそれぞれ埋め込む.その後、InputManagerに現在のウィンドウIDにInput通信チャネルをバインドさせることで、どのウィンドウがどのチャネルで通信されているかを知ることができます.最後にBinderを介してoutInputChannelをAPP側に転送します.次はSocketPairの作成コードです.
status_t InputChannel::openInputChannelPair(const String8& name,
        sp& outServerChannel, sp& outClientChannel) {
    int sockets[2];
    if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) {
        status_t result = -errno;
        ...
        return result;
    }

    int bufferSize = SOCKET_BUFFER_SIZE;
    setsockopt(sockets[0], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
    setsockopt(sockets[0], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
    setsockopt(sockets[1], SOL_SOCKET, SO_SNDBUF, &bufferSize, sizeof(bufferSize));
    setsockopt(sockets[1], SOL_SOCKET, SO_RCVBUF, &bufferSize, sizeof(bufferSize));
	
    String8 serverChannelName = name;
    serverChannelName.append(" (server)");
    outServerChannel = new InputChannel(serverChannelName, sockets[0]);
	 
    String8 clientChannelName = name;
    clientChannelName.append(" (client)");
    outClientChannel = new InputChannel(clientChannelName, sockets[1]);
    return OK;
}

ここでsocketpairの作成とアクセスは実際にはファイル記述子を借りるか、WMSはBinder通信を利用してAPP側にファイル記述子fdを返信する必要がある.この部分はBinderの知識を参考にすることができるだけで、主にカーネルレベルで2つのプロセスfdの変換を実現し、ウィンドウの追加に成功した後、socketpairは作成され、APP側に渡されたが、チャネルは完全に確立されていない.アクティブな傍受が必要であるため、メッセージが来るには通知が必要であるため、まずチャネルモデルを見てみましょう.
APP側のメッセージを傍受する手段は,socketをLooperスレッドのepoll配列に追加し,メッセージが来るとLooperスレッドが起動し,イベント内容を取得することであり,コード的にはWindowInputEventReceiverの作成に伴って通信チャネルのオープンが完了する.
情報が来て、Looperはfdによって対応するリスナーを見つけます:NativeInputEventReceiver、そしてhandleEventを呼び出して対応するイベントを処理します
int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {
   ...
    if (events & ALOOPER_EVENT_INPUT) {
        JNIEnv* env = AndroidRuntime::getJNIEnv();
        status_t status = consumeEvents(env, false /*consumeBatches*/, -1, NULL);
        mMessageQueue->raiseAndClearException(env, "handleReceiveCallback");
        return status == OK || status == NO_MEMORY ? 1 : 0;
    }
  ...

その後、イベントがさらに読み込まれ、Javaレイヤオブジェクトとしてカプセル化され、Javaレイヤに渡され、対応するコールバック処理が行われます.
status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,  
        bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {  
        ...
    for (;;) {  
        uint32_t seq;  
        InputEvent* inputEvent;  
        
        status_t status = mInputConsumer.consume(&mInputEventFactory,  
                consumeBatches, frameTime, &seq, &inputEvent);  
        ...
        
      case AINPUT_EVENT_TYPE_MOTION: {
        MotionEvent* motionEvent = static_cast(inputEvent);
        if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) {
            *outConsumedBatch = true;
        }
        inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
        break;
        } 
        
	   if (inputEventObj) {
	                env->CallVoidMethod(receiverObj.get(),
	                        gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
	                env->DeleteLocalRef(inputEventObj);
	            }

最後にタッチイベントがinputEventにカプセル化され、InputEventReceiverのdispatchInputEvent(WindowInputEventReceiver)で処理され、ここでは一般的なJavaワールドに戻ります.
ターゲットウィンドウでのイベント処理
最後にイベントの処理の流れを簡単に見て、ActivityやDialogなどはどのようにTouchイベントを獲得したのでしょうか.どのように処理しますか?率直に言えば、リスニングイベントをViewRootImplのrootViewに渡し、イベントの消費を自分で担当させ、最後にどのViewに消費されたのかは具体的な実現次第だが、ActivityとDialogのDecorViewについてはViewのイベント割当関数dispatchTouchEventを書き換え、CallBackオブジェクト処理にイベント処理を渡し、ViewおよびView Groupの消費については、ビュー自身の論理です.
まとめ
すべてのプロセスをモジュールに直列に接続します.プロセスは大体次のようになります.
  • 画面
  • をクリック
  • InputManagerServiceのReadスレッドはイベントをキャプチャし、前処理後にDispatcherスレッド
  • に送信する
  • Dispatcherターゲットウィンドウ
  • を見つける
  • は、Socketを介して、イベントをターゲットウィンドウ
  • に送信する.
  • APP端末が起動された
  • ターゲットウィンドウ処理イベント
  • を見つける
    著者:本を読むカタツムリは10分でAndroidタッチイベントの原理(InputManagerService)を知る
    参考までにご指摘ください