Android IPC
Activityと独立したプロセス上のServiceとの間で、大きなデータをやりとりする必要が出てきたの調べて見ました。ActivityからServiceへのリクエストをトリガとして、ServiceからActivityへのデータ連係を開始します。
720pのJPEGと付随するメタデータを30FPSで連携します。JPEGは品質などによってサイズが変わりますが、とりあえず1枚500KBで考えます。
通信速度は心配していないのですが、問題はメモリです。バカ正直に15MB/sでメモリを確保すると、GCさんが張り切ってしまいそうです。
方式の検討
ざっと調べて見たところ、以下のような方式が使えそう。
- Binder (AIDL)
- ファイル
- 共有メモリ
- UNIXドメイソケット
- Pipe
どの方式を採用するにしても、Binderと組み合わせます。
例えば2の方式であれば、ファイルに書き込んだ後にBinderで通知してロードさせるとかです。
全部試すのは手間が掛かるので、試す前にもう少し考えてみます。
まず、2のファイル方式。試すまでも無く、ストレージのI/Oはメモリより遅いですよね。ストレージに書きまくってダメージを与えるもの気が引けます。
3の共有メモリは早そうです。一番良さそうなのですが、Javaから共有メモリにアクセスするAPIが用意されていないようです。ネイティブはなるべく使いたくないので、他がダメだった場合に試すことにします。
4と5は似たような感じですね。ソケットは手間が掛かるので、Pipeを試すことにします。1対多で通信するのであればソケットが良いですが、今回は1対1なので。
というわけで、1のBinderと5のPipeで試します。どちらについても、Binderによるリクエストをトリガとして通信を開始します。
環境
実行環境です。普通にプライベートで使っている端末なので、色々なアプリが入っています。
- Xperia Z4 (Android 6.0)
準備
Service
Serviceは別プロセスで動かすので、android:process
を指定します。
<service
android:name=".BinderService"
android:enabled="true"
android:exported="false"
android:process=".binderservice"/>
メタデータ
適当なデータクラスを用意します。面倒なのでフィールドはpublicで。
フィールドは適当です。実際には、もっと色々なデータを持ちます。
Binderで連携するためのParcelableと、Pipeで連携するためのSerializableも忘れずに実装します。
package example.androidipcexample;
import android.os.Parcel;
import android.os.Parcelable;
public class MetaData implements Parcelable, Serializable {
public final int width;
public final int height;
public MetaData(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.width);
dest.writeInt(this.height);
}
protected MetaData(Parcel in) {
this.width = in.readInt();
this.height = in.readInt();
}
public static final Parcelable.Creator<MetaData> CREATOR = new Parcelable.Creator<MetaData>() {
@Override
public MetaData createFromParcel(Parcel source) {
return new MetaData(source);
}
@Override
public MetaData[] newArray(int size) {
return new MetaData[size];
}
};
}
AIDLも忘れずに。
package example.androidipcexample;
parcelable MetaData;
Binder
コード
I/Fを定義したAIDLを用意します。ActivityからServiceへのリクエストであるIBinderServiceと、ServiceからActivityへのコールバックであるIBinderServiceCallbackの2つです。
package example.androidipcexample;
import example.androidipcexample.MetaData;
interface IBinderServiceCallback {
void render(in MetaData meta, in byte[] jpeg);
}
package example.androidipcexample;
import example.androidipcexample.IBinderServiceCallback;
interface IBinderService {
void start(in IBinderServiceCallback callback);
void stop();
}
リクエストを受けたらスレッドを起動し、データを投げるServiceを作成します。
投げるのは500KBの空データです。
package example.androidipcexample;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
public class BinderService extends Service {
public BinderService() {
}
@Override
public IBinder onBind(Intent intent) {
return new IBinderService.Stub() {
private Thread thread;
@Override
public void start(final IBinderServiceCallback callback) throws RemoteException {
thread = new Thread(new Runnable() {
// 1MBのダミーデータ
private final byte[] jpeg = new byte[500 * 1024];
@Override
public void run() {
try {
while (!Thread.interrupted()) {
long beforeTime = System.currentTimeMillis();
callback.render(new MetaData(1920, 1080), jpeg);
long time = System.currentTimeMillis() - beforeTime;
// 60FPSくらいになるようにSleepする。
if (time < 16) {
Thread.sleep(16 - time);
}
}
} catch (RemoteException | InterruptedException e) {
// 無視
}
}
});
thread.start();
}
@Override
public void stop() throws RemoteException {
thread.interrupt();
}
};
}
}
Activityでは、Serviceにリクエストを投入し、コールバックでデータを受信します。受信したデータは捨てるだけです。
package example.androidipcexample;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
public class BinderActivity extends AppCompatActivity {
private IBinderService service;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_binder);
bindService(new Intent(this, BinderService.class), serviceConnection, BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
if (service != null) {
try {
service.stop();
} catch (RemoteException e) {
// 無視
}
}
unbindService(serviceConnection);
super.onDestroy();
}
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
service = IBinderService.Stub.asInterface(binder);
try {
service.start(new IBinderServiceCallback.Stub() {
@Override
public void render(MetaData meta, byte[] jpeg) throws RemoteException {
if (jpeg.length != 500 * 1024) {
Log.e("test", "データ壊れてる疑惑あり");
}
}
});
} catch (RemoteException e) {
// 虫
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
}
結果
まずはServiceのリソースです。
安定してますね。コールバックメソッドを呼んでいるだけなので、当たり前ですが。
次にActivityのリソースです。
予想通り、メモリが乱高下しています。おそらく、Binderでデータが連係されるたびにメモリを確保しているのでしょう。
Pipe
コード
Binderの時と同様にAIDLを用意します。
PipeのディスクリプタをActivityに渡す必要がありますので、startでParcelFileDescriptorを返します。
package example.androidipcexample;
interface IPipeService {
ParcelFileDescriptor start();
void stop();
}
Serviceでは、ParcelFileDescriptor.createPipeでPipeを作り、書き込みPipeに対してデータを投入します。また、読み込み用のPipeをActivityに返却します。
データは、メタデータ、JPEGサイズ、JPEGの順番に送信します。
package example.androidipcexample;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
public class PipeService extends Service {
public PipeService() {
}
@Override
public IBinder onBind(Intent intent) {
return new IPipeService.Stub() {
private Thread thread;
@Override
public ParcelFileDescriptor start() throws RemoteException {
final ParcelFileDescriptor[] pipeFd;
try {
pipeFd = ParcelFileDescriptor.createPipe();
} catch (IOException e) {
return null;
}
Log.d("test", "start");
thread = new Thread(new Runnable() {
// 1MBのダミーデータ
private final byte[] jpeg = new byte[500 * 1024];
@Override
public void run() {
try (ObjectOutputStream os = new ObjectOutputStream(
new BufferedOutputStream(
new ParcelFileDescriptor.AutoCloseOutputStream(pipeFd[1])
)
)) {
while (!Thread.interrupted()) {
long beforeTime = System.currentTimeMillis();
os.writeObject(new MetaData(1920, 1080));
os.writeInt(jpeg.length);
os.write(jpeg);
long time = System.currentTimeMillis() - beforeTime;
// 60FPSくらいになるようにSleepする。
if (time < 16) {
Thread.sleep(16 - time);
}
}
} catch (IOException | InterruptedException e) {
// 無視
}
}
});
thread.start();
return pipeFd[0];
}
@Override
public void stop() throws RemoteException {
thread.interrupt();
}
};
}
}
Activityは、Serviceから受け取ったディスクリプタから入力ストリームを開きます。
あとは、Serviceが書き込んだ順に読み込むだけです。
ここではJPEGのサイズがわかっている前提であらかじめメモリを確保していますが、本番では、初期サイズを決めてJPEGサイズが超えるたびに再確保するのが良いでしょう。
package example.androidipcexample;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class PipeActivity extends AppCompatActivity {
private IPipeService service;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pipe);
bindService(new Intent(this, PipeService.class), serviceConnection, BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
if (service != null) {
try {
service.stop();
} catch (RemoteException e) {
// 無視
}
}
unbindService(serviceConnection);
super.onDestroy();
}
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
service = IPipeService.Stub.asInterface(binder);
try {
final ParcelFileDescriptor fd = service.start();
new Thread(new Runnable() {
@Override
public void run() {
try (ObjectInputStream is = new ObjectInputStream(
new BufferedInputStream(
new ParcelFileDescriptor.AutoCloseInputStream(fd)
)
)) {
byte[] jpeg = new byte[500 * 1024];
while (true) {
MetaData meta = (MetaData) is.readObject();
int jpegSize = is.readInt();
if (jpegSize != 500 * 1024) {
Log.e("test", "データ壊れてる疑惑あり");
}
is.readFully(jpeg, 0, jpegSize);
}
} catch (IOException | ClassNotFoundException e) {
// 無視
}
}
}).start();
} catch (RemoteException e) {
// 虫
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
}
結果
まずはServiceです。
なんと、予想外にメモリが暴れてます。CPU使用率も高い。
Activityも同様に。
これはちょっと無いですね・・・。
Pipe改
コード
調べて見ると、どうもObjectOutputStream/ObjectInputStreamがダメみたいです。
ObjectOutputStream.writeでバイト配列を書き込むのがイマイチっぽい。
試しに、ObjectOutputStream/ObjectInputStreamを使うのを止めてみます。
とは言っても、MetaDataを分解して送るわけではありません。さすがにそれは面倒なので、MetaDataをObjectOutputStreamでバイト配列に変換し、バイト配列として送信します。
AIDLについては、PipeServiceと同じものを流用します。
MetaDataをバイト配列として送信する都合上、データの並びを少し変えます。メタデータのサイズ、メタデータ、JPEGのサイズ、JPEGの順に送信します。
package example.androidipcexample;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class PipeService2 extends Service {
public PipeService2() {
}
@Override
public IBinder onBind(Intent intent) {
return new IPipeService.Stub() {
private Thread thread;
@Override
public ParcelFileDescriptor start() throws RemoteException {
final ParcelFileDescriptor[] pipeFd;
try {
pipeFd = ParcelFileDescriptor.createPipe();
} catch (IOException e) {
return null;
}
Log.d("test", "start");
thread = new Thread(new Runnable() {
// 1MBのダミーデータ
private final byte[] jpeg = new byte[500 * 1024];
@Override
public void run() {
try (DataOutputStream os = new DataOutputStream(
new BufferedOutputStream(
new ParcelFileDescriptor.AutoCloseOutputStream(pipeFd[1])
)
)) {
while (!Thread.interrupted()) {
long beforeTime = System.currentTimeMillis();
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(new MetaData(1920, 1080));
byte[] metaDataBytes = bos.toByteArray();
os.writeInt(metaDataBytes.length);
os.write(metaDataBytes);
}
os.writeInt(jpeg.length);
os.write(jpeg);
long time = System.currentTimeMillis() - beforeTime;
// 60FPSくらいになるようにSleepする。
if (time < 16) {
Thread.sleep(16 - time);
}
}
} catch (IOException | InterruptedException e) {
// 無視
}
}
});
thread.start();
return pipeFd[0];
}
@Override
public void stop() throws RemoteException {
thread.interrupt();
}
};
}
}
Activityでは、メタデータをバイト配列として受け取るので、そのあたりが少し変わっています。
ここではメタデータのメモリを毎回確保していますが、JPEGと同様にあらかじめ確保しておけば少しメモリに優しくなります。本当に少しですが。
package example.androidipcexample;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class PipeActivity2 extends AppCompatActivity {
private IPipeService service;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_pipe2);
bindService(new Intent(this, PipeService2.class), serviceConnection, BIND_AUTO_CREATE);
}
@Override
protected void onDestroy() {
if (service != null) {
try {
service.stop();
} catch (RemoteException e) {
// 無視
}
}
unbindService(serviceConnection);
super.onDestroy();
}
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder binder) {
service = IPipeService.Stub.asInterface(binder);
try {
final ParcelFileDescriptor fd = service.start();
new Thread(new Runnable() {
@Override
public void run() {
try (DataInputStream is = new DataInputStream(
new BufferedInputStream(
new ParcelFileDescriptor.AutoCloseInputStream(fd)
)
)) {
byte[] jpeg = new byte[500 * 1024];
while (true) {
int metaSize = is.readInt();
byte[] metaBytes = new byte[metaSize];
is.readFully(metaBytes);
try (ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(metaBytes)
)) {
MetaData meta = (MetaData) ois.readObject();
}
int jpegSize = is.readInt();
if (jpegSize != 500 * 1024) {
Log.e("test", "データ壊れてる疑惑あり");
}
is.readFully(jpeg, 0, jpegSize);
}
} catch (IOException | ClassNotFoundException e) {
// 無視
}
}
}).start();
} catch (RemoteException e) {
// 虫
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
}
結果
さて、改善できたでしょうか?
まずはServiceから。
メモリは緩やかな上昇傾向、CPUは低いレベルで安定しています。
良い感じです。
次にActivity。
こちらも同様ですね。
Service/Activityともに、メモリは上がりきったらちゃんと落ちます。
まとめ
何も考えないでPipeを使うと酷いことになりますが、ちゃんと作れば結構良い感じです。
メモリが暴れても良いのなら、Binderを使うのもありかもしれません。
ただし、Binderは大きなデータを送ることが出来ません。500KBなら問題ありませんが、1MBのデータを送ろうとしたら下記のようなエラーが出てしまいました。
E/JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = 1048700)
今回作ったコードは下記に置いておきました。
https://github.com/arenahito/AndroidIPCExample