とある事情で、Androidのプロセス間を数メガ〜200メガバイト/秒ぐらいでデータをやりとりする方法を調べてみた。
プロセス間通信
Androidでプロセス間通信といえば、
- Intent
一番簡単だけど、単発/低頻度で少量のデータのやり取りしか無理 - Messenger(+Handler)
単発/低頻度で少量のデータのやり取りしか無理 - Binderを使う(Javaからならサービス経由のやりとりがBinderを使っているみたいです)
数百〜数千バイト程度を高速にやりとり可能らしい・・・
でも実際に試すと、数十〜数百キロバイトの時点で遅延が大きすぎてだめでした。高頻度なら実用的にはせいぜい百数十バイト程度までにしといたほうが良さそうでした。 - 共有メモリ(Ashmem=Anonymous Shared Memory)
これは別途後で。 - Unixドメインソケット
共有メモリが使えたのと、最終的にもっと違う方法に落ち着いたのでちゃんと確かめてないけど、一般論として、共有メモリよりもオーバーヘッドが大きいですよね。native同士でも比較的容易に使えるのがメリットかな? - TCP/UDPソケット
同上 - (名前付き)パイプ
同上
というのが有るらしいです。
共有メモリ
IntentやMessenger(+Handler)は端から対象外、Binderやサービスでの苦労話は置いといて、共有メモリです。グルグル先生に聞くとAndroidではLinuxに元々あった共有メモリの代わりにAshmenというのを使うらしいです(今はLinuxでも使えるらしいですけど)。
でも、使うための関数群がlibcutils.soに含まれてて、Java/NDKからの使用は未サポート。AOSPからヘッダーと一緒にもって来るとかエミュレータや実機から引っこ抜くとかって話になるみたい。
android.os.MemoryFile発見(^^)v
ちょっと面倒、とか思いつつSDKのAPIレファレンスを眺めてたら、android.os.MemoryFileと言うクラスが有りました。
上のAshmemのJavaのラッパークラスみたいです。\(^o^)/と思って試したんですが、プロセス間をまたぐ方法がよく判りませんでした。どうやらプロセス内でのキャッシュに使うのが主目的みたい。残念(´・ω・`)
libcutils.soとかを持ってこなくても簡単にいけた
しょうが無いので、libcutils.soのソースとandroid.os.MemoryFileのnative側の実装を眺めてみると・・・別にlibcutils.soとそのヘッダーをわざわざ持ってこなくても、NDKに含まれる関数だけで代用できそうです。というか、ほぼまんまコピーしてくるだけでした。C++のクラスにしたので、まるまるコピーではないけど、プロセス内に関しては簡単に動くようになりました。
やっぱりつまづくのはプロセス間
で、問題のプロセス間の共有です。単に名前を同じにしただけではMemoryFileと一緒でうまく行きませんでした。なんかやり方間違ってるのかなぁ。
次に、生成したashmemのnativeのファイルディスクリプタ(=int値)をコピーして、サービス経由で渡してmmapのファイルディスクリプタとして渡してみました。・・・読み書きにエラーは出ないけどデータが読み出せない・・・(T_T) もちろん、MAP_SHAREDフラグ指定だよ。
m(_ _)m ♪〜ParcelFileDescriptor様 降臨〜♪ m(_ _)m
がっかり〜って思ってふて寝しているとふと思いつきました。結局のところファイルディスクリプタがちゃんと渡ってないんだろうって。どうすればプロセス間をまたいで正しいファイルディスクリプタを渡せるかを調べた所、どうやらParcelFileDescriptorというのがキーのようです。
つまり、
- native側で共有メモリ(ashmem)を生成して、mmapでマップする@プロセスA
- 共有メモリのnativeファイルディスクリプタを取得@プロセスA
- nativeファイルディスクリプタ(整数値)をJNI経由でJava側へ引き渡す@プロセスA
- ParcelFileDescriptor#fromFd(nativeファイルディスクリプタ)を呼び出してParcelFileDescriptorのインスタンスを生成する@プロセスA
- ParcelFileDescriptorをサービス(AIDL)経由で別プロセスへ送りつける(プロセスA => プロセスB)
ちゃんと確認はしてないけど、AIDL使わなくてもMessenger/Handlerとかソケットとか経由で渡しても大丈夫みたいです。 - ParcelFileDescriptor#getFDを呼び出して、変換されたnativeファイルディスクリプタを取得する@プロセスB
- JNI経由でnative側へ引き渡す@プロセスB
- mmapでマップする@プロセスB
とすると、プロセスAで生成した共有メモリへプロセスA/プロセスBで読み書き出来るようになりました\(^o^)/
プロセスA/Bそれぞれのnativeファイルディスクリプタを見るとそれぞれ違う値になっていたので、ParcelFileDescriptorを使って送りつける間にシステム側で都合よく変換してくれるみたいです。
MemoryFileの場合は、リフレクションを使えばなんとかnativeのファイルディスクリプタを取得できるので、プロセスBにParcelFileDescriptorとして送りつけることまでは出来たのですが、その後受け側のプロセスでMemoryFileにファイルディスクリプタを引き渡すのが駄目でした。native側でMemoryFileの中身をいじるようなコードを書けば不可能ではなさそうな気もしましたが、それなら、全部移植してしまった方が簡単という結果でした。
それはそうとして
そもそもなんで、そんな大量のデータをプロセス間またがせようとしたかというと、UVCカメラからの映像を取得する別プロセスで動くサービスを実装しようとしたからです。オーバーヘッドを抜きにした実転送量としては、640×480/YUV@30fpsだと約18MB/秒、FullHDだと1920×1080/YUV@30fpsとして約120MB/秒なので、最大百数十MB/秒ぐらいで転送できれば理想です。
試して見た限りでは、同じプロセス内で共有ライブラリを呼び出して実行するのと、別プロセスで動くサービスで取得した映像を共有メモリ経由で取得するのとで大きな速度への影響はなさそうでした。
と言う事でほぼ共有メモリを使う方向で決まりかかっていたんですが、別の方法を思いついた、と言うか気がついてしまいました。
へぇ〜
そんな事できたんだぁって感じですが、何か判ります?
実はですね〜、ParcelFileDescriptorだけではなく、Sufaceもプロセス間をまたいで渡すことが出来るのです(*_*)。共有メモリに夢中で、Surfaceに気が回っていませんでしたが、そもそも最終的にはSurfaceへ描画したいのでした。上で書いたように、ファイルディスクリプタもnativeから取得したそのままの値では引き渡せませんでしたが、ParcelFileDescriptorを経由すればOKでした。しかもParcelFileDescriptorもSufaceも、Parcelableというインターフェースを実装しています。これはもしや、Surfaceでもいけんじゃないかと思って試してみると・・・うまくいきました\(^o^)/
つまり、
- Surfaceを取得(SurfaceViewとかGLSurfaceView、あるいはTextureViewからでもOK)@プロセスB
- サービス経由でSurfaceをプロセスAに渡す(プロセスB => プロセスA)。
- 変換されたSurfaceをnative側へ引き渡す@プロセスA
- native側でSurfaceへ描画する@プロセスA
とします。少なくともプロセス間をまたいでSurfaceを引き渡す部分はほぼJava必須になるけど、共有メモリを使うより超簡単だ〜。
データ更新する場合共有メモリを使うのであれば、
- 共有メモリへフレームデータ書き込み@プロセスA
- プロセスAからプロセスBへフレームデータの更新通知
- 共有メモリからフレームデータを取り出して描画@プロセスB
てな手順を繰返し実行しないといけないですし、共有メモリの排他制御にもパージされちゃってないかも気をつける必要があります。
でもプロセスをまたいでSurfaceを渡しちゃうのであれば、
排他制御はSuraceのLock/UnLockAndPostで片付く(Javaでもnativeでも一緒)
更新通知は、
- プロセスBで内容を読み出す必要がなければ、(nativeでもJavaでも)何もしないでいい
- プロセスBで内容を読み出したい場合は、SurfaceTextureから作ったSurfaceを渡して、SurfaceTexture.OnFrameAvailableListenerを使うのが一番簡単そう(プロセスBがJava必須になるけど)
てな感じで、文章で書くとイマイチかもしれないけど、実装上はかなり楽ちんです。実際にはシステム内部でSurfaceFlingerか何かが似たような処理をしているんでしょうけど。
と言う事で、汎用的なプロセス間の大量データ交換には使えないけど、最終的に描画する映像データなら一気に描画まで出来るのでお得なのかも。
TextureViewまたは自前で生成したSurfaceTextureから取得したSurfaceであれば、GPU上にテクスチャとして直接書き込むことも可能だと思います。
ちょっと心配なのは、手持ちの機種では問題ないものの、他の色んな機種で問題なく動くんやろかってのがあるんだけどね。Surfaceがプロセス間をまたいだ時にシステム側で何をやってるかは未検証だけど、プロセス間をSurfaceに超えさせる事自体はSDKの範囲内の事しかしてないし、違うプロセスに渡したら駄目とか違うプロセスで描画したら駄目とは書いてないので、大丈夫だと思うことにしようと思います。結構昔からSurfaceはParcelableを実装してあるし。でもSDK/OSの想定外の使い方なのかなぁ(^_^;)使う時は無保証、自己責任でお願いします。
ということで
せっかく共有メモリが使えるようになったんだけど結局お蔵入りというか、プロセス間を超えるのは別の方法になってしまいました。共有メモリとしては使ってないけど、最悪クラッシュしてもリークせずにシステム側でクリーンアップしてくれる上に、ローメモリになったらパージまでしてくれる大量のバッファを一括で確保できるようになったから、まぁいっかぁってことで。
ただちょっと気を付けることが
あります。プロセスBで取得した同じSurfaceを複数回プロセスAを送りつけると、元が同じでもその度に違うSurfaceがプロセスA内に生成されます。重複したSurfaceへ描画するのは無駄ですし、後で指定したのを削除したくてもどれがどれと対応しているのかSurfaceだけでは判りません。つまりプロセスBが送ったSurfaceとプロセスAが受け取ったSurfaceの紐付けを別途しないといけません。
例えば、プロセスB内でプレビュー表示用と録画用の2つのSurfaceを生成してプロセスAへ送付、途中で録画用だけを取り除きたい、というような場合、そのままではプロセスAではどれを取り除いたらいいのか判りません。
プロセスAから識別子を返してもいいのですが、可能な限り非同期でやりとりしたいので、プロセスBから識別子をSurfaceと一緒に渡すことにしました。ぶっちゃけプロセスBのSurfaceのhashCodeを渡しているだけですけどね。
ということで、おしまいです。久しぶりにコード無しの短い記事!?になりました(^_^)
お疲れ様でした。