いいかげんあんどろでも勉強するかと 6 年遅れくらいで重い腰を上げかけている。気が重い。スマホとか知らないっすよ・・・。
あんどろ、というかスマホ固有の話題は色々あれど、その一つがタッチベースの UI なのは間違いない。そういえばタッチというのはどうやって実装されているんだろうか。それを一通り眺めれば、少しは気の重さが晴れるかもしれない。ということで今日はタッチイベントの実装を眺めてみたい。実装といっても静電容量だの電磁誘導だのではなくユーザー空間の話です。そして老人の勉強記録であり目新しい話はありません。間違ってたら教えてください。
参照するコードは何も考えず repo sync で降ってくる AOSP master。たぶんだいたい 4.4.x 相当(だよね?)
View#onTouchEvent()
あんどろプログラマからみたタッチイベントはふつう View#onTouchEvent() にやってくる MotionEvent だと理解している。ListView なんかも onTouchEvent() で色々やっているからこれはきっと正しい。
さっそく frameworks/base の View.java を見てみると、onTouchEvent() にはそれなりに長いデフォルト実装がある。(150 行くらい。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | |
たとえば Long press や Tap の判定なんかがさらっと書いてある。判定方法は Runnable を実装してそれをタイマーから呼び、状態の差を見るだけ。
こういうのをさらっとかけるプラットホームはいいなあ…とおもうのだった。(C++比。Swift 書いてる人は鼻で笑っといてください。)
それにしてもたくさんの責務をばりっと同じクラスに書いてしまうのは伝統的な Java ぽくない。 View.java だけで 1.7 万行くらいある…
ViewGroup
さて onTouch() はどこから呼ばれるのか。主なパスは二つある。
一つは View ツリーの親からやってくるパスで、親たる ViewGroup の ViewGroup#dispatchTransformedTouchEvent() から呼ばれる。このメソッドは ViewGroup::dispatchTouchEvent() から使われている。子の View のうちイベントの座標に重なるものにイベントを配信する。よくある親から子への event propagation。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | |
名前が “Transformed” なのは子 View のローカル座標系に位置を変換するからだけど、よくみると位置にオフセットを足すだけでなくだけでなく変換行列をかけている。View には回転やらスケールやらの行列をセットできるらしい。たぶんアニメーションのためだろう。イベントの衝突計算にもちゃんと反映されるんだな。Material Design なんかだと色々派手に動くのであんなものが実装できるのかと密かに怪しんでいたけれど、下地は案外ちゃんとしていた。当たり前かもしれませんが・・・。
ViewRootImpl
もう一つのパスは、同じクラスの View#dispatchTouchEvent() と View#dispatchPointerEvent() を介し ViewRootImpl から呼ばれるもの。
ViewRootImpl も 0.7 万行くらいあるそこそこ大きなクラスで、コメントによれば View ツリーとウィンドウシステム (WindowManager) をとりもつのが仕事らしい。名前から察するにこれがツリーのルートなのだろう。ただし ViewGroup が View を継承しているのに対し ViewRootImpl は継承していない。ツリーのルートというよりコンテナという方が実態に近い。そして ViewRootImpl::mView がルートのようだ。この値は Activity が表示されるときにどこかからセットされる。 MotionEvent を最初にうけとる View はこの mView。mView がセットされるまでの道のりは長いので省略。
なおクラス名から予期される Impl でない ViewRoot は見当たらない。昔のコードにはあるから、どこかでこの不思議な名前に変わったようだ。
InputStage
さて View#dispatchPointerEvent() および View#dispatchGenericMotionEvent() は ViewPostImeInputStage#processPointerEvent() から呼ばれる。 ViewPostImeInputState をはじめとする InputStage のサブクラスはみな ViewRootImpl の内部クラスで、タッチやキーボードなどの入力イベントを処理するための小さなフレームワークを構成している。
この InputStage フレームワークはいわゆる Chain of responsibility のパターン。一つのイベントを処理するために一連の stage 実装が参加し、自分が処理できないイベントを別の stage に先送りしたり、ちょっとタイミングや中身を書き換えて委譲したりする。まあ UI まわりで chain of responsibility ってよくあるよね。 Cocoa の responder chain とか。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | |
InputStage が面倒を見る入力イベントは KeyEvent (キーボード)と MotionEvent (タッチ)の二種類。Stage の実装は 6 種類(ViewPreImeInputStage, ImeInputStage, NativePostImeInputStage, EarlyPostImeInputStage, ViewPostImeInputStage, SyntheticInputStage) 。MotionEvent については委譲の果てに ViewPostImeInputStage が呼び出されて View に届く。
タッチ紀行の主役 MotionEvent だけを追いかけると InputStage のフレームワークはやりすぎに見える。でも KeyEvent のコードパスを調べると事情がわかる。KeyEvent は IME にリダイレクトされる必要がある。そして処理の結果は非同期に、別のプロセスから戻ってくる。そんな非同期性やメッセージングの複雑さを局所化するための仕組みなのだろう。
そのほか NDK 対応のためとみられる Native なんとかという stage もあるけど、NativeAcitivity のコードをひやかした印象だともう機能してないレガシーな印象。
QueuedInputEvent
本題に戻る。 InputStage へのイベントはどこからやってくるのだろう。読み進めると ViewRootImpl#doProcessInputEvents() が deliverEvent() 経由で InputStage を呼び出している。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
名前のとおり doProcessInputEvents() は複数のイベントを処理する。そのイベントは ViewRootImpl#mPendingInputEventHead という線形リストから取り出している。型は QueuedInputEvent.
名前の通り、このリストは intrusive なキューとして機能している。
イベントやメッセージの配信について調べるとき、どんなキューをいつ通過するか はわかりやすい道程になる。その一つ目が現れた。
このキューにはどこからイベントが詰め込まれるのか・・・というと、ViewRootImpl#enqueueInputEvent() なる大変わかりやすい名前のメソッドがあるのだった。
イベント配信について調べるとき気にする事がもう一つある。
その配信は同期的に処理される(同じコールスタックの中で即座に配信される)か、それとも非同期(タイマーやイベントループで先送りされる)か。非同期配信はコードの堅牢さを助ける一方、遅延の原因にもなる。MotionEvent みたいに反応時間が大切そうなものを非同期化していいの?
などと思いつつよく見ると、enqueueInputEvent() には processImmediately なんてパラメタがある。
1 2 3 4 5 6 7 8 9 10 11 12 | |
呼び出しが processImmediately なら即座に doProcessInputEvents() が呼ばれ、キューに詰めたばかりのイベントが同期的に掃き出される。そうでなければメインループにメッセージを投げ (scheduleProcessInputEvents())、非同期に doProcessInputEvents() を呼び出すよう指示する。つまり ViewRootImpl はキューをもっているが、それを同期的に掃き出すオプションを用意している。(そしてだいたいは同期的に処理している。)
WindowInputEventReceiver#onInputEvent()
enqueueInputEvent() はあちこちから呼ばれている。ただし、その多くはキーボードのイベントや、「合成」イベントを発行するためのもの。
SyntheticTrackballHandler や SyntheticTouchNavigationHandler といったクラスが、SyntheticInputStage から「合成された」 InputEvent を送り出す。たとえばトラックボール由来の MotionEvent をスクロールのための矢印キーのイベントに、MotionEvent 全般を十字キーイベントに変換/合成(synthesis)したりする。トラックボールのあんどろデバイスとかあるんかいな・・・。
こうした脇道はさておくと、内部クラスである WindowInputEventReceiver が実質上唯一の MotionEvent 送付元のようだ。processImmediately は true. 同期配信。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
クラス名から判断すると、この WindowInputEventReceiver およびスーパークラスの InputEventReceiver は MotionEvent などの入力イベントを処理するのに特化した専用の仕組みなのだろう。イベントを扱う他のコードは ViewRootImpl#mHandler という Handler オブジェクトを介するのが流儀に見える。わざわざ特別な WindowInputEventReceiver を使うのは不思議な気もする。性能上の事情があるのかもね。
InputEventReceiver, InputChannel, Looper
ViewRootImpl は InputEventReceiver を介して MotionEvent を受け取っているようだ、ということがわかった。
InputEventReceiver は Template Method パターンでサブクラスの onInputEvent() を呼びだし、InputEvent (MotionEventをふくむ) の到着を知らせる。でもいつどこからこれを呼び出すのだろう。ぱっと見ただけではよくわからない。 onInputEvent() を呼び出す dispatchInputEvent() は C++ 側から呼び出されるからだ。Java はこのへんで切り上げ、JNI のむこうにある C++ コードに駒を進めよう。
InputEventReciever.java に対応する JNI の実装は android_view_InputEventReceiver.cpp。このファイルは NativeInputEventReceiver という (C++) クラスを定義している。Java 側のクラス構造をおおまかにマップした C++ クラスを作るのはあんどろ JNI 実装のイディオムらしく、目についた JNI のコードはだいたい似たようなパターンに従っていた。オブジェクトモデルを Java 側に任せきる伝統的な Java スタイルとは違い、どちらかというとブラウザの C++ と JS の関係っぽい。
参考までに NativeInputEventReceiver の定義はこんなかんじ:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | |
jobject 型の mReceiverWeakGlobal が Java のオブジェクトをさしている。
Java 側はこんなの:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | |
native とマークされたメソッドが複数。また long な mReceiverPtr に C++ 側オブジェクトへのポインタを持っている。Finalizer があるのも JNI っぽい。こうやって C++ と Java のクラスをミラーする流儀なんだね。
さて一瞬 Java に戻ると、InputEventReceiver には共に働くクラスが二つある: InputChannel と Looper だ。 InputEventReceiver はこの二つのオブジェクトをコンストラクタの引数に受け取る。
InputChannel
InputChannel も Looper も C++ にミラーしたオブジェクトのある C++ backed なクラス。
InputChannel の JNI コード android_view_InputChannel.cpp は NativeInputChannel クラスを定義している。でもこのクラスはほとんどなにもせず、別のクラス android::InputChannel をラップしているだけ。android::InputChannel が Java 側 android.os.InputChannel の実体だと言える。InputChannel(Java) -> NativeInputChannel(C++) -> android::InputChannel(C++) と間接化が二段階ある。この冗長さはきっと、 Java クラスの実装を C++ で書くのではなく C++ のクラスを Java 側に公開するという形で物事がデザインされ、中間の JNI にしわ寄せが来た結果だろうな、などと想像した。まあどうでもいい。
C++ 版 InputChannel は InputTransport.h に定義されている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
コメントや定義からわかるように、InputChannel は UNIX ドメインソケットをカプセル化し、そのソケット上で InputMessage 構造体を送受信するもののようだ。InputEventReceiver はこの InputChannel を通じ、どこかから届くイベントを受け取る。
ブラウザに似ていると書いたけれど、実際にはだいぶ違う。ブラウザでは(今のところ) DOM なんかの実装を JS で書く事はない。ぜんぶ C++ にコードがあって JS はそれをラップするだけ。オブジェクトグラフも C++ 側にある。あんどろのこのへんのコードは割と Java 側にもコードがあり、オブジェクトグラフにしても Java 側と C++ 側の両方がそれぞれ自分に必要なものをもっている。
一見グラフの同期が大変そうだけれど、いま見ているのは実装の詳細である非公開なクラスな上にグラフはおおむね immutable 。だから多少冗長でも大丈夫、ということらしい。いずれにせよフレキシブルというかアドホックというか、面白いね。
などと周辺事情をおさらいしたところで本題の InputEventReceiver に戻ろう。
C++ 側のコードに目をやると、NativeInputEventReceiver は LooperCallback なるクラスを継承している。
1 2 3 4 5 | |
LooperCallback は Looper.h に定義されている。名前の通り Looper から通知を受け取るためのインターフェイス。引数にはファイルデスクリプタらしい整数値が渡されている。
このことから察しがつくように、NativeInputEventReceiver は Looper に自分自身を登録する。コードをみてみよう。
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
InputChannel のソケットデスクリプタを自分自身に紐づけ Looper::addFd() を呼び出している。
Looper
この Looper とは何だろう。
Java の世界、アプリケーションの側からみると、android.os.Looper はスレッドのイベントループを抽象化したオブジェクトだ。といっても公開された機能はすくなく、API はループの開始終了くらいしかない。
C++ の世界から見ると、android::Looper は要するに select() (または epoll) だ。ファイルデスクリプタを登録しておき、読み書きの準備ができた際にコールバックを受け取る。
GUI のイベントループは OS の多重化 IO と同期機構の上に組み立てられる。だから epoll とイベントループが同じ名前で抽象化されるのは自然といえば自然だ。そして GUI プログラミングが epoll で非同期サーバーを書くようなものなら、ブロックするコードを書いて怒られるのも無理はない。今更ながら襟首をただす。
あんどろをはじめとする多くの GUI ツールキットは、足下に隠された多重化 IO をアプリケーションから隠している。Java や NDK から直接 android::Looper にアクセスすることはできない。
一方 Mac OS/iOS の Run Loop は多重化 IO としてのイベントループをアプリケーションプログラマに公開している。アプリケーションは多重化したいチャネル(ポート)をメインループに追加できる。これはきっと足下の Mach という OS がメッセージパッシングを重視している現れだろう。意外なところに出自が見えて面白い。
届いたイベントの処理
また脇道にそれた。ここまでのあらすじを振り返ると…
ViewRootImplはInputEvent(MotionEventを含む) を受け取るためにInputEventReceiverを使う。このクラスはInputChannelが持つソケットデスクリプタをLooperに登録し、そのソケットに届いたバイト列をイベントに変換して利用者 (ViewRootImpl) に知らせる。Looperはイベントループの多重化 IO に参加する手段としてLooperCallbackを提供している。LooperCallbackを使うとメインスレッドのイベントループに便乗してソケットのデータを待つ事が出来る。自分でスレッドを持たなくてよい。
NativeInputEventReceiver がソケットに届いたデータをどうやって処理するか、少し覗いてみよう。エントリポイントは handleEvent().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
この handleEvent() は fd の準備ができると Looper から呼び出される。
その準備の結果、fd が読み出し可能 (ALOOPER_EVENT_INPUT) なら届いたデータを処理し(consumeEvents())、
書き出し可能 (ALOOPER_EVENT_OUTPUT) なら ACK を送り返す。特に何も面白くない…
まあ ACK(Finish オブジェクト) の送付があるのは面白いといえば面白い。イベント配信なんて一方向通信で良さそうなものだけれど、なにか事情があるんだろうね。
一歩進んでデータを読み出す conumeEvents() を眺めてみると…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | |
C++ のイベントを mInputConsumer.consume() でソケットから読み出し、それを Java のオブジェクトに変換して Java 側のレシーバ (InputEventReceiver) に通知していた。
Command-Query separation などと厳しく躾けられた身には厳しいコードですな…
InputConsumer と Event Batching
新たに登場した mInputConsumer は InputConsumer クラス。InputChannel を補助している。なぜこんな間接化が必要なのか。InputConsumer::consume() を覗いてみよう:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | |
InputChannel::receiveMessage() で InputMessage 型のオブジェクトを読み出す。そして InputEventFactoryInterface の助けを借り InputMessage を InputEvent に変換する。
型の変換以外にも見所はある。届いたメッセージを batch している。
一回のイベントループで届いた複数の InputMessage を単一の InputEvent にまとめる操作を、ここでは batch と呼んでいる。Batch されるのは特定のメッセージ、具体的には AMOTION_EVENT_ACTION_MOVE と AMOTION_EVENT_ACTION_HOVER_MOVE だけ。要するにまとめて届いた一連のタッチ軌道を一つの MotionEvent にまとめるのが batch 化だ。
Batch してできた軌跡は Java の世界にある MotionEvent から取り出せる。そういえばお絵描きアプリを作っている友人がこの話をしていたなあ。イベントの情報は捨てずオーバーヘッドを減らす batch はタッチならでは。面白い。デスクトップとマウス相手なら間引いちゃえばいいからね大概・・・。
なお MotionEvent も C++ backed なクラスだった。Input.h に定義がある。別に JNI なんて使わずコピーで実装しても良さそうな気がするけど、それはゆとり世代なおっさんの考えなのだろう。MotionEvent 周辺コードではメモリ節約への気配りが見られる。まず先に登場した InputEventFactoryInterface からしてサブクラスの名前が PreallocatedInputEventFactory と PooledInputEventFactory. アロケーションを細工するための factory だった。batch のコードにも工夫がある。たとえばまとめるタッチ点の数が多すぎてメモリ確保に失敗するとタッチ点を “再サンプリング” して点数を減らす。芸が細かい。
InputChannel::receiveMessage()
receiveMessage() はソケットからデータを読むと書いた。念のため確認しとこう。
1 2 3 4 5 6 7 8 | |
構造体を sizeof() して読むだけ。よしよし。素朴でいいよ。
InputChannel の対
ここまでは InputChannel のソケットに届いたデータが InputMessage, InputEvent と姿を変えつつ ViewRootImpl に届くところを見届けた。
ではそもそも InputChannel のソケットに届くデータはどこからやってくるのだろう。ViewRootImpl に戻って InputChannel ができる様子を調べよう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | |
setView() という巨大な関数に一連の初期化があった。まず空の InputChannel をインスタンス化し、それを mWindowSession.addToDisplay() に渡したあと WindowInputEventReceiver のコンストラクタに届けている。
InputChannel のコンストラクタは何もしない空関数だから、怪しいのは addToDisplay() だ。 mWindowSession はどんなオブジェクトなのだろう。
WindowSession と Binder
mWindowSession は IWindowSession インターフェイス型のフィールド。
あんどろの世界で I から始まる型は IPC 機構の Binder が AIDL ファイルから生成したプロキシだ。
この IWindowSession にも対応する IWindowSession.aidl がある。
つまり mWindowSession は IPC のプロキシで、実体はたぶん別のプロセスにある。いちおう変数の出所を確認すると…
1 2 3 4 5 6 7 | |
コンストラクタの冒頭でグローバルの方から来た様子がわかる。そして…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
IWindowSession のインスタンスは IWindowManager という別の binder proxy から openSession() で取り出していた。
名前から察するに、IWindowSession はアプリケーションと WindowManager の接続単位として振る舞い、
その WindowManager とデータをやり取りするのだろう。InputChannel もやりとりされるデータの一部というわけだ。
…という仮説を確認すべく WindowSession の実装を探してみよう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | |
InputChannel を mService に引き渡している。それっぽい。このコードはどこか別のプロセスで動いている(はず)なのを思い出してほしい。
Parcel とファイルデスクリプタ
WindowSession が binder のサービスなのはいいとして、一つ気になる事がある。
InputChannel は C++ のオブジェクトをラップしており、
そのオブジェクトはソケットのデスクリプタを持っていた。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
この fd, プロセスをまたいで送れるものなんだろうか。
Binder ではオブジェクトを Parcelという形式でバイト列に書き出す。
InputChannel の直列化コードを覗いてみよう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
parcel->writeDupFileDescriptor() なんて API を使っている。どうも Binder はふつうにファイルデスクリプタを送れるらしい。
私の記憶によれば、Linux で別プロセスに fd を送るには複雑怪奇なシステムコールが必要なはず。
Parcel のバイト列に埋もれた fd をどうやってその手のシステムコールにつないでいるのだろうか。
答えを求め Parcel.cpp を覗く。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | |
んー。構造体 flat_binder_object として適当なタグをつけた fd をつくり、それをバイト列に書き込んでいるだけ…のように見える…
その後 libs/binder/ のコードをしばらく眺めたものの、
結局そのバイト列は /dev/binder というファイルに ioctl() で渡されるだけだとわかった。
細工はこの /dev/binder にある。
Binder にはカーネルドライバがある。そしてそのドライバがカーネル空間の力でファイルデスクリプタを別プロセスに引き渡している。だから変なシステムコールに頼る必要もない。
(自分で変なシステムコールを実装しているとも言える。)
よく見ると先に登場した flat_binder_object もカーネルの中、binder.h に定義がある。
そういえば Binder はクロスプロセスなオブジェクトの寿命管理なんかもカーネルにやらせていてクール、みたいな話をどこかで聞いたことがある。 ファイルデスクリプタ受け渡しもそういうクールな何かの一部なのですね。
カーネルのドライバ binder.c をみると…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | |
ふつうにプロセスのデスクリプタテーブルをいじっているのだった。ドライバ書くの便利だな・・・。
なお素の Linux ではファイルデスクリプタを送受信するのに recvmsg()/sendmsg() という API を使うのだけれど、その事実は man を読んでも全然わからない。The Linux Programming Interface (日本語訳) というシステムコールマニア向け読み物には説明がある。が、知らないよそんなの・・・。Mac OS/Mach は Port という IPC の仕組みにけっこうな労力を割いており、その Port はプロセス間で難なく受け渡す事ができる。Mac OS X Internals とかにも説明があったはず。IPC には OS の個性が見える、かも。
前半の道のり
話がそれた。
ここまでの道のりを振り返ると:
Viewに届くMotionEventは親のViewGroupかViewRootImplから届く。ViewRootImplはInputStageフレームワークで配信前のイベントに細工をする。ただしMotionEventに大きな影響はない。ViewRootImplはInputEventReceiverを介しInputChannelのソケットからMotionEventを読み出す。- ソケットには
InputMessage構造体が書き込まれている。 Looperを使いメインスレッドのイベントループに便乗してソケットを監視。MotionEventには複数のInputMessageを batch する。
- ソケットには
InputChannelは Binder オブジェクトのIWindowSessionから取り出す。ソケットの反対側は別プロセス。
イベントやメッセージの配信を調べるときはキューの存在が一里塚になると先に書いた。
ここまでだと、まず ViewRootImpl が QueuedInputEvent というキューを持っていた。
ただし MotionEvent がこのキューに長くとどまる事はなく、だいたい同期的に配信される。
もう一つのキューは InputChannel のソケット。共有メモリでも使わない限りプロセス間には何らかの通信経路が必要だから、
ここにキューがあるのは自然だ。つまりプロセスの中に限ると、主要なパスでは MotionEvent を同期的に配信している。結構がんばってるとおもう。
WindowManagerService
というわけで View のあるプロセスを離れ、 InputChannel の反対側にあるプロセスに話を進めよう。
Window Manager が住むそのプロセスで、誰が InputChannel にデータを送り込むのだろう。
WindowSession の実装である Session クラスは,
InputChannel を初期化する addToDisplay() の処理を
WindowManagerService#addWindow() に委譲している。
その addWindow() はというと:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
InputChannel#openInputChannelPair() で通信の両端となる InputChannel の対を作り、一端を mInputManager に、もう一旦を呼び出し元に返している。
いちおう確認しておくと openInputChannelPair() は InputChannel を socketpair() の糖衣にすぎない。特段すごい何かではない。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
万一 socketpair() などを勉強したい人がいたら Stevens の本 でも読んでおけばいいんじゃないでしょうか。
さて新たに作られた InputChannel の一端を担う mInputManager。クラスは InputManagerService だった。いかにも InputEvent がらみの気配がする名前。
これも C++ backed なクラスで、 registerInputChannel() の実装も
C++ 側 にある。
1 2 3 4 5 6 7 8 | |
backing class たる ‘android::InputManagerが持つ [InputDispatcher`](https://github.com/android/platform_frameworks_base/blob/master/services/input/InputDispatcher.cpp) に丸投げ。
でもこの名前、いかにもイベント配信してそうなクラスじゃないですか…
InputDispatcher
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
この InputDispatcher は InputChannel を Connection オブジェクトでラップした上で mConnectionsByFd に保存し、かつそのファイルデスクリプタを自身の持つ Looper に登録していた。
やはり InputDispatcher … または仲間の Connection がソケットの一端であるのは間違いなさそうだ。
InputDispatcher の定義をみるとイベントループ Looper を持っている。
そしてそれらしいキューもある。
1 2 3 4 5 6 7 8 9 10 | |
この Connection は InputDispatcher 内の nested class. InputChannel に加え、キューも持っている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
そして更によくみると InputDispatcherThread なんてクラスまである。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
InputDispatcher::Connection::outboundQueue, InputDispatcher::inboundQueue, InputDispatcher::mLooper そして InputDispatcherThread。
InputDispatcher は自身のスレッドとイベントループをもち、キューにため込んだデータをいくつかの InputChannel に書き出すようなクラスだ…と想像できる。
疲れてきたので詳しくは調べないけれど、読んでみるとだいたいそんなかんじだった。
InputReader
では InputDispatcher のキューにデータを詰めるのは誰か。
というとすぐ隣に InputReader なるクラスがある。
1 2 3 4 5 6 7 8 9 | |
いかにも怪しい名前のオブジェクト InputDevice を持っている。加えて InputReaderThread まであり、つまりこの InputReader も自分のスレッドを持っている。
そして自分のスレッドで EventHub や InputDevice としてカプセル化された入力デバイスからの入力を待つ。
InputManager, InputReader, InputDispatcher。ざっと眺めたけれど、これらのクラスはいま以上に細かく見ても面白くない。
各クラスやスレッドとキューの関係をながめ、さっさと先に進みたい。
- InputDispatcher:
InputDispatcherは自分のスレッドでLooperをまわし、InputChannelにデータを書き込む。InputDispatcherへの入力はInputDispatcher::mInboundQueueに詰められる。そしてこのキューをとりだし、配送先をみて適切なInputChannel(フォーカスのある Window に紐づいたInputChannel) に書き込む。 - InputReader:
InputReaderも自分でスレッドを持っている。そのスレッドでEventHubからデータを読み出す。読み出したデータはInputDispatcherに通知される。 - InputManager:
InputDispatcherとInputReaderの寿命を管理する。 InputDispatcherとInputReaderの間にはQueuedInputListenerと呼ばれるキューが挟まっている。ただしこのキューにイベントが長居することはない。
図で書いてお茶を濁すとこんなかんじ:
EventHub
InputReader が持つ EventHub はカーネルからユーザランドにタッチを届ける最初のオブジェクト。
ようやく下には Linux しかない階にたどり着いた。コンストラクタからしてそれっぽい。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | |
inotify, fcntl, epoll に pipe… なんとなくサーバのコードを読んでるみたいで落ち着く。
inotify を使っているのはデバイスの動的な追加や削除に対応するため。
Looper を使わず epoll を直接呼んでいるのはなぜ?とかは気にしないでおく。たぶんたいした理由はなかろう。
肝心なデバイスたちを追加するコードは EventHub::scanDirLocked().
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
なんというか、C++ というより C なかんじのコードですな。
openDeviceLocked() の中身はひたすら ioctl() してデバイスの種別を検出する
コードがだらだらと書かれている。読むと疲れるから省略。
こうして開かれたデバイスたちが epoll 経由で監視され、状態を読み出される。それだけ知っていればいい気がする。
input_event
epoll_wait() をラップする EventHub::getEvents() を見ると、
デバイスたちとどんなデータ形式で情報をやり取りするのか垣間みる事ができる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | |
input_event なる構造体を読み出している。
これは Linux が定義する構造体。入力デバイス用ドライバ仕様の一部として文書化されている。
Linux 側だけでなく、あんどろ側でも InputReader 周辺のコードについては簡単な説明がある。
自分のデバイスにあんどろを移植したい人が読む資料のひとつらしい。コード読まなくてもよかったじゃん…
アプリケーションの奥に潜ったつもりが反対側の玄関に出てしまった気分。
今回の主題タッチイベントについてもきちんと説明があり、 しかも結局のところ Linux のドライバの仕様 に従いましょう、という結論のよう。 なるほどこれが Linux をベースにするということかとやや感心した。もうちょっと変な事をやってるもんだと勝手に思ってた・・・。
デバイスファイルというそこそこ標準的なインターフェイスを使っているおかげで、 unlocked なデバイスでは 外部からイベントを注入することもできるらしい。 その仕組みをテストの自動化に使う話などをみかけた。こんなレイヤで自動化をするのが良いアイデアなのかはさておき、おもしろい話ではあるね。
そのほか InputManager の仕事
この InputManager と仲間たちの周辺は面倒な問題をまとめて押し込んだ風情。あちこちで細々とした問題に対処している。
たとえデバイスの傾きに応じ画面の向きが回転すると、デバイスから届く座標を回転変換してからアプリケーションに知らせる。
またあんどろはある時期から画面上の仮想ボタンで物理ボタンを代替できるようになった。その仮想ボタン(virtual key)も InputReader が面倒を見る。
そういう雑多な責務を押し付けられた結果、 InputReader.cpp は 6500 行、
InputDispatcher.cpp は 4500 行にふくれあがっている。気の毒。
まとめなど
というわけで View#onTouchEvent() に MotionEvent がとどくまでの道程を眺めてみた。
スタート地点である View を含むプロセスでは、余分なスレッドに寄り道することなく Looper に便乗した InputEventReceiver が InputChannel から InputMessage を読み出し、
バッチ化した上で ViewRootImpl にイベントをよこす。Binder ではなく InputChannel のような別の経路を使うのは、メインループに処理をくっつけるためでもあろうだろう。
ViewRootImpl が受け取ったイベントは InputStage マイクロフレームワークを通過してから View ツリーに送り込まれる。
ツリーの中では座標変換や衝突判定などをしつつ親から子へイベントが伝播する。
View のあるプロセスにイベントのデータを送りつけるのは Window Manager のサービスが住むプロセス。
IWindowManager binder オブジェクトが View のある … というか Window を持つプロセスに InputChannel を付与する。
InputChannel の実体は socketpair() で作った UNIX ドメインソケットだった。
Window Manager のプロセスには送付先 InputChannel を複数束ねる InputDispatcher と、デバイスファイルを束ねる InputReader がいる。
この2つのオブジェクトはそれぞれ自分のスレッドを持っている。InputManager がこの2つのオブジェクトをまとめた facade として機能している。
InputReader は EventHub オブジェクトにデバイスファイルのデスクリプタを預け、 EventHub は epoll や inotify でこれらのファイルやファイルのディレクトリを監視、
データを読みだす。読み出されたデータは InputReader が InputDispatcher に手渡す。InputDispatcher はそのデータを適当な InputChannel に書き出す。
オブジェクトやプロセスをまたいだイベントの受け渡しには何らかのキューが使われる。キューには処理を非同期化するものとしないものがある。
View のあるプロセスの中に限るとキューは一つ、 ViewRootImpl がもつ QueuedInputEvent だけ。このキューは(多くの場合)処理を非同期化せず、その場で同期的に消化された。
View のあるプロセスと Window Manager のプロセスの間には InputChannel に隠された UNIX ドメインソケットというキューがある。
これは非同期。プロセスをまたぐ以上同期的に動きようがない。
Window Manager の中にはたくさんのキューがある。 InputDispatcher がもつ inboutQueeue, InputDispatcher::Connection の outboundQueue, InputDispatcher と
InputReader をつなぐ QueuedInputListener. 中でも InputDispatcher::inboundQueue はスレッドをまたぐ非同期化に使われている。
あとはカーネルのなかに追加のキューがあっても驚かないけれど、調べていない。ユーザ空間の中では非同期化されるキューは2つだけ。
反応性への配慮という点で、これはがんばってるとおもう。
わからないこと
入力やイベント処理というのは一般に abstraction が leak しやすい分野。ここでも例にもれず読むのに疲れる雑然としたコードがあちこちに顔を出し、読むのは疲れた。
とはいえあんどろ入門という当初の目的には悪くなかった気がする。View ツリー内へのディスパッチをひやかして View のイベントモデルに入門し、
Looper を通じてスレッドモデルをちらりとのぞき、InputChannel の周辺をさまよい Binder と Parcel に触れ、
Window Manager のはじっこを通り過ぎて最後は Linux の表面に降り立った。あんどろよくわからん、という気分は若干薄れた気がする。
とはいえ当然ながらわからないことも沢山ある。Activity や最初の View はどうやって作られたのか。 特に IWindowManager のようなサービスはどのプロセスで動いていて、アプリケーションはその binder オブジェクトをどう手に入れるのか。 そんな bootstrap は全然わかっていない。
Binder といえばスレッドモデルもよくわからない。proxy 経由のメソッド呼び出しはホスト側のどのスレッドに届くのか。
イベントをうけとったあと、画面がどう描かれるのか・・・は、 Graphics System-Level Architecture という よく書かれた資料があり、このおかげでそこそこわかった気になれた。今回タッチイベントについて書こうと思ったのもこの文書に刺激されたから。 まあ素人のラクガキなので比べ物にはならないけどね・・・。
あとはそもそもどうやってアプリを構成するのがよいのかなど常識的な話がわかってない。 すいすい動くアプリはどうしたら作れるのか、とかさ。まあ手を動かさないとわからないことだろうし、ぼちぼちやっていこうとおもいます。
間違いそのほかは気が向いたらついったなどで訂正していただけると助かります、と繰り返し教えを乞うて今日はおしまい。