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 用のスタブが自動生成されます。

AIDL によるサービスとの接続

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>

コロン(:) でプロセス名を指定すると専用プロセスで起動します。

実行結果

上記コードを実行すると、次のようになります。

AIDL によるサービスとの接続

デバッガで見ると確かに新しいプロセスが起動していることがわかります。

AIDL によるサービスとの接続

試しにサービス用のプロセスを kill すると、アクティビティ側でサービスとの接続がきれたことが検出されました。

AIDL によるサービスとの接続

ちなみに、試しに 「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 となったからです。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Android 開発入門