AIDL によるインターフェイスの定義とサービスプロセスの IPC
サービスを外部プロセスで実行する場合は、直接には Binder への参照がとれないので IPC による通信を行う必要があります。
ここでは AIDL によってインターフェイスを定義して、サービスプロセスにバインドする例をしめします。
AIDL によるインターフェイス定義と IPC とは?
IPC、AIDL とは?
IPC というのはインタープロセスコミュニケーション (Interprocess Communication, プロセス間通信) の略です。
プロセス間のメソッド呼び出しといえば RPC (Remote Procedure Call) 等がありますが、 RPC では IDL (Interface Definition Language) などでインターフェイスを定義し、 IDL コンパイラでスタブコードを生成するという方法が行われます。
Windows での開発経験がある方は COM のインターフェイスを定義するために、IDL を書いて MIDL コンパイラでスタブ/プロキシコードを生成するということをしたことがあるという方も多いと思います (明示的じゃなくても裏でそんなことをしている場合もありますし)。
Android での IPC では AIDL (Android Interface Definition Language) を使います。AIDL でインターフェイスを定義します。後述の通り、 Eclipse の環境では *.aidl を作成するとそれから自動的にスタブコードを生成してくれます。
AIDL はいつ使うの?
サービスプロセスを専用のプロセスで起動する場合に AIDL でインターフェイス定義する必要があります。
それ以外の場合は IBind を直接取得できるので、IBind によるサービスとアクティビティの接続 で説明した方法が使えます。
AIDL でインターフェイスを定義し、サービスを別プロセスで動かし、さらに IPC をしてみよう
AIDL による IPC インターフェイス定義
ここでは「IBind によるサービスとアクティビティの接続」で行ったことと同様のことをしてみましょう。
サービス側で特別に実装するメソッドは setMessage というメソッド一つです。
AIDL は次のように単純に定義できます。次の内容を IMyIntentService.aidl という名前で保存します。
package com.keicode.android.test;
interface IMyIntentService {
void setMessage(String message);
}
*.aidl という拡張子でファイルを作成すると、AIDL がコンパイルされて IPC 用のスタブが自動生成されます。
onBind でスタブを返すサービスの実装
onBind でスタブを返すサービスの実装は次のようになります。
package com.keicode.android.test;
import android.app.IntentService;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
public class MyIntentService extends IntentService {
final static String TAG = "ServiceTest7";
final IMyIntentService.Stub binder = new IMyIntentService.Stub() {
@Override
public void setMessage(String message) throws RemoteException {
MyIntentService.this.setMessage(message);
}
};
String message;
public MyIntentService() {
super(TAG);
}
public void setMessage(String message){
this.message = message;
}
@Override
public IBinder onBind(Intent intent) {
Log.d(TAG, "onBind");
return binder;
}
@Override
protected void onHandleIntent(Intent intent) {
Log.d(TAG, "onHandleIntent");
Log.d(TAG, "message = [" + message + "]");
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
呼び出し側は次のようになります。
package com.keicode.android.test;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
public class ServiceTest7 extends Activity {
final static String TAG = "ServiceTest7";
Button startButton;
IMyIntentService myService;
Intent serviceIntent;
ServiceConnection serviceConnection = new ServiceConnection(){
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.d(TAG, "onServiceConnected");
try {
myService = IMyIntentService.Stub.asInterface(service);
myService.setMessage("Hello, IMyIntentService!");
startService(serviceIntent);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.d(TAG, "onServiceDisconnected");
myService = null;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
startButton = (Button)findViewById(R.id.start_button);
startButton.setOnClickListener(new OnClickListener(){
@Override
public void onClick(View v) {
serviceIntent = new Intent(
getBaseContext(), MyIntentService.class);
bindService(
serviceIntent, serviceConnection,
Context.BIND_AUTO_CREATE);
}
});
}
}
「IBind によるサービスとアクティビティの接続」との違いに着目してコードを見るとわかりやすいと思います。
サービスを別プロセスで実行する
サービスを専用プロセスで実行するには、AndroidManifest.xml の service 要素にて、android:process を定義します。
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="com.keicode.android.test"
android:versionCode="1"
android:versionName="1.0">
<uses-sdk android:minSdkVersion="8" />
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity
android:name=".ServiceTest7"
android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".MyIntentService"
android:process=":myservicetest7"/>
</application>
</manifest>
コロン(:) でプロセス名を指定すると専用プロセスで起動します。
実行結果
上記コードを実行すると、次のようになります。
デバッガで見ると確かに新しいプロセスが起動していることがわかります。
試しにサービス用のプロセスを kill すると、アクティビティ側でサービスとの接続がきれたことが検出されました。
ちなみに、試しに 「IBind によるサービスとアクティビティの接続」 で作ったサービスを別プロセスで動かすと、 onServiceConnected で IBind から MyIntentService にキャストしようとしたところで ClassCastException が発生しました。
FATAL EXCEPTION: main java.lang.ClassCastException: android.os.BinderProxy at com.keicode.android.test.ServiceTest6$1.onServiceConnected... at android.app.LoadedApk$ServiceDispatcher.doConnected... at android.app.LoadedApk$ServiceDispatcher$RunConnection.run... at android.os.Handler.handleCallback(Handler.java:587) at android.os.Handler.dispatchMessage(Handler.java:92) at android.os.Looper.loop(Looper.java:123) at android.app.ActivityThread.main(ActivityThread.java:3806) at java.lang.reflect.Method.invokeNative(Native Method) at java.lang.reflect.Method.invoke(Method.java:507) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run... at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:597) at dalvik.system.NativeStart.main(Native Method) Force finishing activity com.keicode.android.test/.ServiceTest6
サーバーを別プロセスで実行して IPC となったことで、ここで渡された IBind は MyIntentService クラスで実装したそのものではなく、 プロキシ (android.os.BinderProxy) の IBind となったからです。