いいかげんあんどろでも勉強するかと 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 という よく書かれた資料があり、このおかげでそこそこわかった気になれた。今回タッチイベントについて書こうと思ったのもこの文書に刺激されたから。 まあ素人のラクガキなので比べ物にはならないけどね・・・。
あとはそもそもどうやってアプリを構成するのがよいのかなど常識的な話がわかってない。 すいすい動くアプリはどうしたら作れるのか、とかさ。まあ手を動かさないとわからないことだろうし、ぼちぼちやっていこうとおもいます。
間違いそのほかは気が向いたらついったなどで訂正していただけると助かります、と繰り返し教えを乞うて今日はおしまい。